mirror of
https://github.com/CatimaLoyalty/Android.git
synced 2025-12-28 13:21:45 +00:00
feat: Add time selection to date picker and store as ISO datetime
- Implement time picker flow after date selection - Migrate database from millis to ISO 8601 datetime strings - Add DateTimeUtils helper methods with comprehensive tests
This commit is contained in:
parent
5f8c8048e6
commit
21c8d4c962
@ -12,10 +12,10 @@ import android.util.Log;
|
||||
|
||||
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;
|
||||
@ -23,7 +23,7 @@ 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;
|
||||
@ -335,17 +335,70 @@ public class DBHelper extends SQLiteOpenHelper {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -385,6 +438,7 @@ public class DBHelper extends SQLiteOpenHelper {
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
@ -394,18 +448,19 @@ public class DBHelper extends SQLiteOpenHelper {
|
||||
}
|
||||
|
||||
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);
|
||||
@ -428,10 +483,11 @@ public class DBHelper extends SQLiteOpenHelper {
|
||||
|
||||
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
|
||||
@ -439,8 +495,8 @@ public class DBHelper extends SQLiteOpenHelper {
|
||||
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);
|
||||
@ -453,6 +509,7 @@ public class DBHelper extends SQLiteOpenHelper {
|
||||
database.insert(LoyaltyCardDbIds.TABLE, null, contentValues);
|
||||
|
||||
// FTS
|
||||
Log.d("DBHelper", "insertFTS :: note => " + note + " Store: => "+ store);
|
||||
insertFTS(database, id, store, note);
|
||||
|
||||
database.setTransactionSuccessful();
|
||||
@ -463,7 +520,7 @@ public class DBHelper extends SQLiteOpenHelper {
|
||||
|
||||
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) {
|
||||
@ -473,8 +530,8 @@ public class DBHelper extends SQLiteOpenHelper {
|
||||
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);
|
||||
|
||||
84
app/src/main/java/protect/card_locker/DateTimeUtils.java
Normal file
84
app/src/main/java/protect/card_locker/DateTimeUtils.java
Normal file
@ -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);
|
||||
}
|
||||
}
|
||||
@ -10,8 +10,8 @@ import java.math.BigDecimal;
|
||||
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;
|
||||
|
||||
@ -66,8 +66,8 @@ public class ImportURIHelper {
|
||||
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;
|
||||
@ -113,11 +113,11 @@ public class ImportURIHelper {
|
||||
}
|
||||
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);
|
||||
@ -182,10 +182,10 @@ public class ImportURIHelper {
|
||||
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) {
|
||||
|
||||
@ -4,13 +4,14 @@ import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.graphics.Bitmap;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.Instant;
|
||||
import java.util.Currency;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
@ -19,9 +20,9 @@ public class LoyaltyCard {
|
||||
public String store;
|
||||
public String note;
|
||||
@Nullable
|
||||
public Date validFrom;
|
||||
public Instant validFrom;
|
||||
@Nullable
|
||||
public Date expiry;
|
||||
public Instant expiry;
|
||||
public BigDecimal balance;
|
||||
@Nullable
|
||||
public Currency balanceType;
|
||||
@ -121,8 +122,8 @@ public class LoyaltyCard {
|
||||
* @param zoomLevelWidth
|
||||
* @param archiveStatus
|
||||
*/
|
||||
public LoyaltyCard(final int id, final String store, final String note, @Nullable final Date validFrom,
|
||||
@Nullable final Date expiry, final BigDecimal balance, @Nullable final Currency balanceType,
|
||||
public LoyaltyCard(final int id, final String store, final String note, @Nullable final Instant validFrom,
|
||||
@Nullable final Instant expiry, final BigDecimal balance, @Nullable final Currency balanceType,
|
||||
final String cardId, @Nullable final String barcodeId, @Nullable final CatimaBarcode barcodeType,
|
||||
@Nullable final Integer headerColor, final int starStatus,
|
||||
final long lastUsed, final int zoomLevel, final int zoomLevelWidth, final int archiveStatus,
|
||||
@ -216,11 +217,11 @@ public class LoyaltyCard {
|
||||
this.note = note;
|
||||
}
|
||||
|
||||
public void setValidFrom(@Nullable Date validFrom) {
|
||||
public void setValidFrom(@Nullable Instant validFrom) {
|
||||
this.validFrom = validFrom;
|
||||
}
|
||||
|
||||
public void setExpiry(@Nullable Date expiry) {
|
||||
public void setExpiry(@Nullable Instant expiry) {
|
||||
this.expiry = expiry;
|
||||
}
|
||||
|
||||
@ -342,13 +343,13 @@ public class LoyaltyCard {
|
||||
}
|
||||
if (bundle.containsKey(BUNDLE_LOYALTY_CARD_VALID_FROM)) {
|
||||
long tmpValidFrom = bundle.getLong(BUNDLE_LOYALTY_CARD_VALID_FROM);
|
||||
setValidFrom(tmpValidFrom > 0 ? new Date(tmpValidFrom) : null);
|
||||
setValidFrom(tmpValidFrom > 0 ? DateTimeUtils.longToInstant(tmpValidFrom) : null);
|
||||
} else if (requireFull) {
|
||||
throw new IllegalArgumentException("Missing key " + BUNDLE_LOYALTY_CARD_VALID_FROM);
|
||||
}
|
||||
if (bundle.containsKey(BUNDLE_LOYALTY_CARD_EXPIRY)) {
|
||||
long tmpExpiry = bundle.getLong(BUNDLE_LOYALTY_CARD_EXPIRY);
|
||||
setExpiry(tmpExpiry > 0 ? new Date(tmpExpiry) : null);
|
||||
setExpiry(tmpExpiry > 0 ? DateTimeUtils.longToInstant(tmpExpiry) : null);
|
||||
} else if (requireFull) {
|
||||
throw new IllegalArgumentException("Missing key " + BUNDLE_LOYALTY_CARD_EXPIRY);
|
||||
}
|
||||
@ -442,10 +443,10 @@ public class LoyaltyCard {
|
||||
bundle.putString(BUNDLE_LOYALTY_CARD_NOTE, note);
|
||||
}
|
||||
if (!exportIsLimited || exportLimit.contains(BUNDLE_LOYALTY_CARD_VALID_FROM)) {
|
||||
bundle.putLong(BUNDLE_LOYALTY_CARD_VALID_FROM, validFrom != null ? validFrom.getTime() : -1);
|
||||
bundle.putLong(BUNDLE_LOYALTY_CARD_VALID_FROM, validFrom != null ? validFrom.toEpochMilli() : -1);
|
||||
}
|
||||
if (!exportIsLimited || exportLimit.contains(BUNDLE_LOYALTY_CARD_EXPIRY)) {
|
||||
bundle.putLong(BUNDLE_LOYALTY_CARD_EXPIRY, expiry != null ? expiry.getTime() : -1);
|
||||
bundle.putLong(BUNDLE_LOYALTY_CARD_EXPIRY, expiry != null ? expiry.toEpochMilli() : -1);
|
||||
}
|
||||
if (!exportIsLimited || exportLimit.contains(BUNDLE_LOYALTY_CARD_BALANCE)) {
|
||||
bundle.putString(BUNDLE_LOYALTY_CARD_BALANCE, balance.toString());
|
||||
@ -516,16 +517,26 @@ public class LoyaltyCard {
|
||||
public static LoyaltyCard fromCursor(Context context, Cursor cursor) {
|
||||
// id
|
||||
int id = cursor.getInt(cursor.getColumnIndexOrThrow(DBHelper.LoyaltyCardDbIds.ID));
|
||||
Log.d("From_Cursor", "\nfromCursor id: " + id);
|
||||
// store
|
||||
String store = cursor.getString(cursor.getColumnIndexOrThrow(DBHelper.LoyaltyCardDbIds.STORE));
|
||||
// note
|
||||
String note = cursor.getString(cursor.getColumnIndexOrThrow(DBHelper.LoyaltyCardDbIds.NOTE));
|
||||
// validFrom
|
||||
long validFromLong = cursor.getLong(cursor.getColumnIndexOrThrow(DBHelper.LoyaltyCardDbIds.VALID_FROM));
|
||||
Date validFrom = validFromLong > 0 ? new Date(validFromLong) : null;
|
||||
String validFromString = cursor.getString(cursor.getColumnIndexOrThrow(DBHelper.LoyaltyCardDbIds.VALID_FROM));
|
||||
Log.d("From_Cursor", "validFromString: " + validFromString);
|
||||
Instant validFrom = null;
|
||||
if(validFromString != null && !validFromString.isEmpty()){
|
||||
validFrom = Instant.parse(validFromString);
|
||||
}
|
||||
Log.d("From_Cursor", "validFrom: " + validFrom);
|
||||
// expiry
|
||||
long expiryLong = cursor.getLong(cursor.getColumnIndexOrThrow(DBHelper.LoyaltyCardDbIds.EXPIRY));
|
||||
Date expiry = expiryLong > 0 ? new Date(expiryLong) : null;
|
||||
String expiryString = cursor.getString(cursor.getColumnIndexOrThrow(DBHelper.LoyaltyCardDbIds.EXPIRY));
|
||||
Log.d("From_Cursor", "expiryString: " + expiryString + "\n");
|
||||
Instant expiry = null;
|
||||
if(expiryString != null && !expiryString.isEmpty()){
|
||||
expiry = Instant.parse(expiryString);
|
||||
}
|
||||
// balance
|
||||
BigDecimal balance = new BigDecimal(cursor.getString(cursor.getColumnIndexOrThrow(DBHelper.LoyaltyCardDbIds.BALANCE)));
|
||||
// balanceType
|
||||
|
||||
@ -1,13 +1,11 @@
|
||||
package protect.card_locker;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.Resources;
|
||||
import android.database.Cursor;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.util.SparseBooleanArray;
|
||||
import android.util.TypedValue;
|
||||
import android.view.HapticFeedbackConstants;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
@ -26,7 +24,6 @@ import com.google.android.material.card.MaterialCardView;
|
||||
import com.google.android.material.color.MaterialColors;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.text.DateFormat;
|
||||
import java.util.ArrayList;
|
||||
|
||||
import protect.card_locker.databinding.LoyaltyCardLayoutBinding;
|
||||
@ -112,13 +109,13 @@ public class LoyaltyCardCursorAdapter extends BaseCursorAdapter<LoyaltyCardCurso
|
||||
}
|
||||
|
||||
if (mLoyaltyCardListDisplayOptions.showingValidity() && loyaltyCard.validFrom != null) {
|
||||
inputHolder.setExtraField(inputHolder.mValidFromField, DateFormat.getDateInstance(DateFormat.MEDIUM).format(loyaltyCard.validFrom), Utils.isNotYetValid(loyaltyCard.validFrom) ? Color.RED : null, showDivider);
|
||||
inputHolder.setExtraField(inputHolder.mValidFromField, DateTimeUtils.formatMedium(loyaltyCard.validFrom), DateTimeUtils.isNotYetValid(loyaltyCard.validFrom) ? Color.RED : null, showDivider);
|
||||
} else {
|
||||
inputHolder.setExtraField(inputHolder.mValidFromField, null, null, false);
|
||||
}
|
||||
|
||||
if (mLoyaltyCardListDisplayOptions.showingValidity() && loyaltyCard.expiry != null) {
|
||||
inputHolder.setExtraField(inputHolder.mExpiryField, DateFormat.getDateInstance(DateFormat.MEDIUM).format(loyaltyCard.expiry), Utils.hasExpired(loyaltyCard.expiry) ? Color.RED : null, showDivider);
|
||||
inputHolder.setExtraField(inputHolder.mExpiryField, DateTimeUtils.formatMedium(loyaltyCard.expiry), DateTimeUtils.hasExpired(loyaltyCard.expiry) ? Color.RED : null, showDivider);
|
||||
} else {
|
||||
inputHolder.setExtraField(inputHolder.mExpiryField, null, null, false);
|
||||
}
|
||||
|
||||
@ -44,9 +44,6 @@ import androidx.appcompat.widget.AppCompatTextView;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.core.content.FileProvider;
|
||||
import androidx.core.graphics.Insets;
|
||||
import androidx.core.view.ViewCompat;
|
||||
import androidx.core.view.WindowInsetsCompat;
|
||||
import androidx.exifinterface.media.ExifInterface;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
|
||||
@ -60,6 +57,8 @@ import com.google.android.material.datepicker.MaterialDatePicker;
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
import com.google.android.material.floatingactionbutton.FloatingActionButton;
|
||||
import com.google.android.material.tabs.TabLayout;
|
||||
import com.google.android.material.timepicker.MaterialTimePicker;
|
||||
import com.google.android.material.timepicker.TimeFormat;
|
||||
import com.jaredrummler.android.colorpicker.ColorPickerDialog;
|
||||
import com.jaredrummler.android.colorpicker.ColorPickerDialogListener;
|
||||
import com.yalantis.ucrop.UCrop;
|
||||
@ -70,34 +69,40 @@ import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.InvalidObjectException;
|
||||
import java.math.BigDecimal;
|
||||
import java.text.DateFormat;
|
||||
import java.text.ParseException;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalTime;
|
||||
import java.time.ZoneId;
|
||||
import java.time.ZoneOffset;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Calendar;
|
||||
import java.util.Collections;
|
||||
import java.util.Currency;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.NoSuchElementException;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.Callable;
|
||||
|
||||
import protect.card_locker.async.TaskHandler;
|
||||
import protect.card_locker.databinding.LayoutChipChoiceBinding;
|
||||
import protect.card_locker.databinding.LoyaltyCardEditActivityBinding;
|
||||
import protect.card_locker.viewmodels.LoyaltyCardDateType;
|
||||
import protect.card_locker.viewmodels.LoyaltyCardEditActivityViewModel;
|
||||
import protect.card_locker.viewmodels.LoyaltyCardEditUiAction;
|
||||
|
||||
public class LoyaltyCardEditActivity extends CatimaAppCompatActivity implements BarcodeImageWriterResultCallback, ColorPickerDialogListener {
|
||||
private static final String TAG = "Catima";
|
||||
protected LoyaltyCardEditActivityViewModel viewModel;
|
||||
private LoyaltyCardEditActivityBinding binding;
|
||||
|
||||
private static final String PICK_DATE_REQUEST_KEY = "pick_date_request";
|
||||
private static final String NEWLY_PICKED_DATE_ARGUMENT_KEY = "newly_picked_date";
|
||||
public static final String PICK_DATE_TIME_REQUEST_KEY = "PICK_DATE_TIME_REQUEST";
|
||||
public static final String NEWLY_PICKED_DATE_TIME_ARGUMENT_KEY = "NEWLY_PICKED_DATE_TIME_ISO";
|
||||
public static final String NEWLY_PICKED_DATE_TIME_TYPE_KEY = "NEWLY_PICKED_DATE_TIME_TYPE_KEY";
|
||||
|
||||
private final String TEMP_CAMERA_IMAGE_NAME = LoyaltyCardEditActivity.class.getSimpleName() + "_camera_image.jpg";
|
||||
private final String TEMP_CROP_IMAGE_NAME = LoyaltyCardEditActivity.class.getSimpleName() + "_crop_image.png";
|
||||
@ -181,13 +186,13 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity implements
|
||||
viewModel.setHasChanged(true);
|
||||
}
|
||||
|
||||
protected void setLoyaltyCardValidFrom(@Nullable Date validFrom) {
|
||||
protected void setLoyaltyCardValidFrom(@Nullable Instant validFrom) {
|
||||
viewModel.getLoyaltyCard().setValidFrom(validFrom);
|
||||
|
||||
viewModel.setHasChanged(true);
|
||||
}
|
||||
|
||||
protected void setLoyaltyCardExpiry(@Nullable Date expiry) {
|
||||
protected void setLoyaltyCardExpiry(@Nullable Instant expiry) {
|
||||
viewModel.getLoyaltyCard().setExpiry(expiry);
|
||||
|
||||
viewModel.setHasChanged(true);
|
||||
@ -367,9 +372,9 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity implements
|
||||
}
|
||||
});
|
||||
|
||||
addDateFieldTextChangedListener(validFromField, R.string.anyDate, R.string.chooseValidFromDate, LoyaltyCardField.validFrom);
|
||||
addDateFieldTextChangedListener(validFromField, R.string.anyDate, R.string.chooseValidFromDate, LoyaltyCardDateType.VALID_FROM);
|
||||
|
||||
addDateFieldTextChangedListener(expiryField, R.string.never, R.string.chooseExpiryDate, LoyaltyCardField.expiry);
|
||||
addDateFieldTextChangedListener(expiryField, R.string.never, R.string.chooseExpiryDate, LoyaltyCardDateType.EXPIRY);
|
||||
|
||||
setMaterialDatePickerResultListener();
|
||||
|
||||
@ -767,8 +772,8 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity implements
|
||||
|
||||
storeFieldEdit.setText(viewModel.getLoyaltyCard().store);
|
||||
noteFieldEdit.setText(viewModel.getLoyaltyCard().note);
|
||||
formatDateField(this, validFromField, viewModel.getLoyaltyCard().validFrom);
|
||||
formatDateField(this, expiryField, viewModel.getLoyaltyCard().expiry);
|
||||
formatDateField(this, validFromField, DateTimeUtils.instantToZonedDateTime(viewModel.getLoyaltyCard().validFrom));
|
||||
formatDateField(this, expiryField, DateTimeUtils.instantToZonedDateTime(viewModel.getLoyaltyCard().expiry));
|
||||
cardIdFieldView.setText(viewModel.getLoyaltyCard().cardId);
|
||||
String barcodeId = viewModel.getLoyaltyCard().barcodeId;
|
||||
barcodeIdField.setText(barcodeId != null && !barcodeId.isEmpty() ? barcodeId : getString(R.string.sameAsCardId));
|
||||
@ -913,7 +918,12 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity implements
|
||||
}
|
||||
}
|
||||
|
||||
protected void addDateFieldTextChangedListener(AutoCompleteTextView dateField, @StringRes int defaultOptionStringId, @StringRes int chooseDateOptionStringId, LoyaltyCardField loyaltyCardField) {
|
||||
protected void addDateFieldTextChangedListener(
|
||||
AutoCompleteTextView dateField,
|
||||
@StringRes int defaultOptionStringId,
|
||||
@StringRes int chooseDateOptionStringId,
|
||||
LoyaltyCardDateType loyaltyCardField
|
||||
) {
|
||||
dateField.addTextChangedListener(new SimpleTextWatcher() {
|
||||
CharSequence lastValue;
|
||||
|
||||
@ -927,10 +937,10 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity implements
|
||||
if (s.toString().equals(getString(defaultOptionStringId))) {
|
||||
dateField.setTag(null);
|
||||
switch (loyaltyCardField) {
|
||||
case validFrom:
|
||||
case LoyaltyCardDateType.VALID_FROM:
|
||||
setLoyaltyCardValidFrom(null);
|
||||
break;
|
||||
case expiry:
|
||||
case LoyaltyCardDateType.EXPIRY:
|
||||
setLoyaltyCardExpiry(null);
|
||||
break;
|
||||
default:
|
||||
@ -940,13 +950,26 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity implements
|
||||
if (!lastValue.toString().equals(getString(chooseDateOptionStringId))) {
|
||||
dateField.setText(lastValue);
|
||||
}
|
||||
showDatePicker(
|
||||
|
||||
ZonedDateTime selectedDateTime = (ZonedDateTime) dateField.getTag();
|
||||
ZonedDateTime validFromDateTime = (ZonedDateTime) validFromField.getTag();
|
||||
ZonedDateTime expiryDateTime = (ZonedDateTime) expiryField.getTag();
|
||||
|
||||
// Determine the min and max constraints based on the field being edited.
|
||||
// This logic is identical to before, but more readable.
|
||||
ZonedDateTime minDateTime = (loyaltyCardField == LoyaltyCardDateType.EXPIRY)
|
||||
? validFromDateTime
|
||||
: null;
|
||||
|
||||
ZonedDateTime maxDateTime = (loyaltyCardField == LoyaltyCardDateType.VALID_FROM)
|
||||
? expiryDateTime
|
||||
: null;
|
||||
|
||||
showDateTimePicker(
|
||||
loyaltyCardField,
|
||||
(Date) dateField.getTag(),
|
||||
// if the expiry date is being set, set date picker's minDate to the 'valid from' date
|
||||
loyaltyCardField == LoyaltyCardField.expiry ? (Date) validFromField.getTag() : null,
|
||||
// if the 'valid from' date is being set, set date picker's maxDate to the expiry date
|
||||
loyaltyCardField == LoyaltyCardField.validFrom ? (Date) expiryField.getTag() : null
|
||||
selectedDateTime,
|
||||
minDateTime,
|
||||
maxDateTime
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -962,7 +985,7 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity implements
|
||||
});
|
||||
}
|
||||
|
||||
protected static void formatDateField(Context context, EditText textField, Date date) {
|
||||
protected static void formatDateField(Context context, EditText textField, ZonedDateTime date) {
|
||||
textField.setTag(date);
|
||||
|
||||
if (date == null) {
|
||||
@ -976,7 +999,7 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity implements
|
||||
}
|
||||
textField.setText(text);
|
||||
} else {
|
||||
textField.setText(DateFormat.getDateInstance(DateFormat.LONG).format(date));
|
||||
textField.setText(date.format(DateTimeUtils.longDateShortTimeFormatter));
|
||||
}
|
||||
}
|
||||
|
||||
@ -1129,7 +1152,7 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity implements
|
||||
Intent contentIntent = new Intent(Intent.ACTION_GET_CONTENT);
|
||||
contentIntent.setType("image/*");
|
||||
Intent chooserIntent = Intent.createChooser(photoPickerIntent, getString(R.string.addFromImage));
|
||||
chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, new Intent[] { contentIntent });
|
||||
chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, new Intent[]{contentIntent});
|
||||
|
||||
try {
|
||||
mPhotoPickerLauncher.launch(chooserIntent);
|
||||
@ -1312,97 +1335,150 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity implements
|
||||
// Nothing to do, no change made
|
||||
}
|
||||
|
||||
private void showDatePicker(
|
||||
LoyaltyCardField loyaltyCardField,
|
||||
@Nullable Date selectedDate,
|
||||
@Nullable Date minDate,
|
||||
@Nullable Date maxDate
|
||||
/**
|
||||
* Shows pickers to select a date and time.
|
||||
*
|
||||
* @param selectedDateTime The currently selected ZonedDateTime, or null.
|
||||
*/
|
||||
void showDateTimePicker(
|
||||
LoyaltyCardDateType loyaltyCardField,
|
||||
@Nullable ZonedDateTime selectedDateTime,
|
||||
@Nullable ZonedDateTime minDateTime,
|
||||
@Nullable ZonedDateTime maxDateTime
|
||||
) {
|
||||
// Create a new instance of MaterialDatePicker and return it
|
||||
long startDate = minDate != null ? minDate.getTime() : getDefaultMinDateOfDatePicker();
|
||||
long endDate = maxDate != null ? maxDate.getTime() : getDefaultMaxDateOfDatePicker();
|
||||
|
||||
CalendarConstraints.DateValidator dateValidator;
|
||||
switch (loyaltyCardField) {
|
||||
case validFrom:
|
||||
dateValidator = DateValidatorPointBackward.before(endDate);
|
||||
break;
|
||||
case expiry:
|
||||
dateValidator = DateValidatorPointForward.from(startDate);
|
||||
break;
|
||||
default:
|
||||
throw new AssertionError("Unexpected field: " + loyaltyCardField);
|
||||
// For MaterialDatePicker, we still need to work with milliseconds since epoch (UTC).
|
||||
// We get this from an Instant.
|
||||
long selectionMillis ;
|
||||
if(selectedDateTime != null){
|
||||
LocalDate localDate = selectedDateTime.toLocalDate();
|
||||
ZonedDateTime startOfDay = localDate.atStartOfDay(selectedDateTime.getZone());
|
||||
selectionMillis = startOfDay.toInstant().toEpochMilli();
|
||||
} else {
|
||||
selectionMillis = MaterialDatePicker.todayInUtcMilliseconds();
|
||||
}
|
||||
|
||||
// Use min/max if provided, otherwise use a default range
|
||||
long startMillis = minDateTime != null
|
||||
? minDateTime.toInstant().toEpochMilli()
|
||||
: getDefaultMinDateOfDatePicker(); // Assuming you have a default
|
||||
|
||||
long endMillis = maxDateTime != null
|
||||
? maxDateTime.toInstant().toEpochMilli()
|
||||
: getDefaultMaxDateOfDatePicker(); // Assuming you have a default
|
||||
|
||||
// The existing validation logic works perfectly, as it operates on milliseconds.
|
||||
CalendarConstraints.DateValidator dateValidator = switch (loyaltyCardField) {
|
||||
case LoyaltyCardDateType.VALID_FROM ->
|
||||
// The 'valid from' date cannot be after the expiry date
|
||||
DateValidatorPointBackward.before(endMillis);
|
||||
case LoyaltyCardDateType.EXPIRY ->
|
||||
// The expiry date cannot be before the 'valid from' date
|
||||
DateValidatorPointForward.from(startMillis);
|
||||
};
|
||||
|
||||
CalendarConstraints calendarConstraints = new CalendarConstraints.Builder()
|
||||
.setValidator(dateValidator)
|
||||
.setStart(startDate)
|
||||
.setEnd(endDate)
|
||||
.setStart(startMillis)
|
||||
.setEnd(endMillis)
|
||||
.build();
|
||||
|
||||
// Use the selected date as the default date in the picker
|
||||
final Calendar calendar = Calendar.getInstance();
|
||||
if (selectedDate != null) {
|
||||
calendar.setTime(selectedDate);
|
||||
}
|
||||
|
||||
MaterialDatePicker<Long> materialDatePicker = MaterialDatePicker.Builder.datePicker()
|
||||
.setSelection(calendar.getTimeInMillis())
|
||||
MaterialDatePicker<Long> datePicker = MaterialDatePicker.Builder.datePicker()
|
||||
.setTitleText("Select Date")
|
||||
.setSelection(selectionMillis)
|
||||
.setCalendarConstraints(calendarConstraints)
|
||||
.build();
|
||||
|
||||
// Required to handle configuration changes
|
||||
// See https://github.com/material-components/material-components-android/issues/1688
|
||||
viewModel.setTempLoyaltyCardField(loyaltyCardField);
|
||||
getSupportFragmentManager().addFragmentOnAttachListener((fragmentManager, fragment) -> {
|
||||
if (fragment instanceof MaterialDatePicker && Objects.equals(fragment.getTag(), PICK_DATE_REQUEST_KEY)) {
|
||||
((MaterialDatePicker<Long>) fragment).addOnPositiveButtonClickListener(selection -> {
|
||||
Bundle args = new Bundle();
|
||||
args.putLong(NEWLY_PICKED_DATE_ARGUMENT_KEY, selection);
|
||||
getSupportFragmentManager().setFragmentResult(PICK_DATE_REQUEST_KEY, args);
|
||||
});
|
||||
}
|
||||
viewModel.onAction(new LoyaltyCardEditUiAction.SetLoyaltyCardDateType(loyaltyCardField));
|
||||
datePicker.addOnPositiveButtonClickListener(utcSelectionMillis -> {
|
||||
// The date is selected, now show the time picker.
|
||||
showTimePicker(loyaltyCardField, utcSelectionMillis, selectedDateTime);
|
||||
});
|
||||
|
||||
materialDatePicker.show(getSupportFragmentManager(), PICK_DATE_REQUEST_KEY);
|
||||
datePicker.show(getSupportFragmentManager(), "DATE_PICKER");
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows the MaterialTimePicker after a date has been selected.
|
||||
*
|
||||
* @param utcDateSelectionMillis The UTC millisecond timestamp for the chosen date.
|
||||
* @param initialDateTime The original ZonedDateTime to pre-fill the time.
|
||||
*/
|
||||
void showTimePicker(LoyaltyCardDateType dateType, long utcDateSelectionMillis, @Nullable ZonedDateTime initialDateTime) {
|
||||
// Default to current time or use the time from the initial selection
|
||||
LocalTime initialTime = initialDateTime != null
|
||||
? initialDateTime.toLocalTime()
|
||||
: LocalTime.now();
|
||||
|
||||
MaterialTimePicker timePicker = new MaterialTimePicker.Builder()
|
||||
.setTimeFormat(TimeFormat.CLOCK_12H)
|
||||
.setHour(initialTime.getHour())
|
||||
.setMinute(initialTime.getMinute())
|
||||
.setTitleText("Select Time")
|
||||
.build();
|
||||
|
||||
timePicker.addOnPositiveButtonClickListener(dialog -> {
|
||||
// 1. Get the selected date part
|
||||
// Convert the UTC millis from the date picker to a LocalDate in the device's timezone
|
||||
Instant selectedInstant = Instant.ofEpochMilli(utcDateSelectionMillis);
|
||||
|
||||
LocalDate selectedDate = selectedInstant.atZone(ZoneOffset.UTC).toLocalDate();
|
||||
|
||||
// 2. Get the selected time part
|
||||
LocalTime selectedTime = LocalTime.of(timePicker.getHour(), timePicker.getMinute());
|
||||
|
||||
// 3. Combine them into a ZonedDateTime
|
||||
ZonedDateTime finalZonedDateTime = ZonedDateTime.of(selectedDate, selectedTime, ZoneId.systemDefault());
|
||||
|
||||
// 4. Send the result back as an ISO 8601 string (the standard format)
|
||||
// This is safer than sending a long, which can be misinterpreted.
|
||||
Bundle args = new Bundle();
|
||||
args.putString(NEWLY_PICKED_DATE_TIME_ARGUMENT_KEY, finalZonedDateTime.toString());
|
||||
args.putInt(NEWLY_PICKED_DATE_TIME_TYPE_KEY, dateType.ordinal());
|
||||
getSupportFragmentManager().setFragmentResult(PICK_DATE_TIME_REQUEST_KEY, args);
|
||||
});
|
||||
|
||||
timePicker.show(getSupportFragmentManager(), "TIME_PICKER");
|
||||
}
|
||||
|
||||
// Required to handle configuration changes
|
||||
// See https://github.com/material-components/material-components-android/issues/1688
|
||||
private void setMaterialDatePickerResultListener() {
|
||||
MaterialDatePicker<Long> fragment = (MaterialDatePicker<Long>) getSupportFragmentManager().findFragmentByTag(PICK_DATE_REQUEST_KEY);
|
||||
MaterialDatePicker<Long> fragment = (MaterialDatePicker<Long>) getSupportFragmentManager().findFragmentByTag(PICK_DATE_TIME_REQUEST_KEY);
|
||||
if (fragment != null) {
|
||||
fragment.addOnPositiveButtonClickListener(selection -> {
|
||||
Bundle args = new Bundle();
|
||||
args.putLong(NEWLY_PICKED_DATE_ARGUMENT_KEY, selection);
|
||||
getSupportFragmentManager().setFragmentResult(PICK_DATE_REQUEST_KEY, args);
|
||||
args.putLong(NEWLY_PICKED_DATE_TIME_ARGUMENT_KEY, selection);
|
||||
getSupportFragmentManager().setFragmentResult(PICK_DATE_TIME_REQUEST_KEY, args);
|
||||
});
|
||||
}
|
||||
|
||||
getSupportFragmentManager().setFragmentResultListener(
|
||||
PICK_DATE_REQUEST_KEY,
|
||||
PICK_DATE_TIME_REQUEST_KEY,
|
||||
this,
|
||||
(requestKey, result) -> {
|
||||
long selection = result.getLong(NEWLY_PICKED_DATE_ARGUMENT_KEY);
|
||||
String selectedDateTimeIso = result.getString(NEWLY_PICKED_DATE_TIME_ARGUMENT_KEY);
|
||||
int selectedDateType = result.getInt(NEWLY_PICKED_DATE_TIME_TYPE_KEY);
|
||||
|
||||
Date newDate = new Date(selection);
|
||||
if (selectedDateTimeIso != null) {
|
||||
ZonedDateTime finalDateTime = ZonedDateTime.parse(selectedDateTimeIso);
|
||||
Instant newDate = finalDateTime.toInstant();
|
||||
|
||||
LoyaltyCardField tempLoyaltyCardField = viewModel.getTempLoyaltyCardField();
|
||||
if (tempLoyaltyCardField == null) {
|
||||
throw new AssertionError("tempLoyaltyCardField is null unexpectedly!");
|
||||
}
|
||||
LoyaltyCardDateType tempLoyaltyCardField = LoyaltyCardDateType.getEntries().get(selectedDateType);
|
||||
|
||||
switch (tempLoyaltyCardField) {
|
||||
case validFrom:
|
||||
formatDateField(LoyaltyCardEditActivity.this, validFromField, newDate);
|
||||
setLoyaltyCardValidFrom(newDate);
|
||||
break;
|
||||
case expiry:
|
||||
formatDateField(LoyaltyCardEditActivity.this, expiryField, newDate);
|
||||
setLoyaltyCardExpiry(newDate);
|
||||
break;
|
||||
default:
|
||||
throw new AssertionError("Unexpected field: " + tempLoyaltyCardField);
|
||||
switch (tempLoyaltyCardField) {
|
||||
case LoyaltyCardDateType.VALID_FROM:
|
||||
formatDateField(LoyaltyCardEditActivity.this, validFromField, finalDateTime);
|
||||
setLoyaltyCardValidFrom(newDate);
|
||||
break;
|
||||
case LoyaltyCardDateType.EXPIRY:
|
||||
formatDateField(LoyaltyCardEditActivity.this, expiryField, finalDateTime);
|
||||
setLoyaltyCardExpiry(newDate);
|
||||
break;
|
||||
default:
|
||||
throw new AssertionError("Unexpected field: " + tempLoyaltyCardField);
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
@ -2,7 +2,6 @@ package protect.card_locker;
|
||||
|
||||
import android.content.ActivityNotFoundException;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.ActivityInfo;
|
||||
import android.content.res.ColorStateList;
|
||||
import android.content.res.Configuration;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
@ -57,11 +56,11 @@ import com.google.zxing.BarcodeFormat;
|
||||
import java.io.File;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.math.BigDecimal;
|
||||
import java.text.DateFormat;
|
||||
import java.text.ParseException;
|
||||
import java.time.Instant;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
@ -425,9 +424,9 @@ public class LoyaltyCardViewActivity extends CatimaAppCompatActivity implements
|
||||
infoText.append(getString(R.string.balanceSentence, Utils.formatBalance(this, loyaltyCard.balance, loyaltyCard.balanceType)));
|
||||
}
|
||||
|
||||
appendDateInfo(infoText, loyaltyCard.validFrom, (Utils::isNotYetValid), R.string.validFromSentence, R.string.validFromSentence);
|
||||
appendDateInfo(infoText, loyaltyCard.validFrom, (DateTimeUtils::isNotYetValid), R.string.validFromSentence, R.string.validFromSentence);
|
||||
|
||||
appendDateInfo(infoText, loyaltyCard.expiry, (Utils::hasExpired), R.string.expiryStateSentenceExpired, R.string.expiryStateSentence);
|
||||
appendDateInfo(infoText, loyaltyCard.expiry, (DateTimeUtils::hasExpired), R.string.expiryStateSentenceExpired, R.string.expiryStateSentence);
|
||||
|
||||
infoTextview.setText(infoText);
|
||||
|
||||
@ -436,9 +435,10 @@ public class LoyaltyCardViewActivity extends CatimaAppCompatActivity implements
|
||||
infoDialog.create().show();
|
||||
}
|
||||
|
||||
private void appendDateInfo(SpannableStringBuilder infoText, Date date, Predicate<Date> dateCheck, @StringRes int dateCheckTrueString, @StringRes int dateCheckFalseString) {
|
||||
private void appendDateInfo(SpannableStringBuilder infoText, Instant date, Predicate<Instant> dateCheck, @StringRes int dateCheckTrueString, @StringRes int dateCheckFalseString) {
|
||||
if (date != null) {
|
||||
String formattedDate = DateFormat.getDateInstance(DateFormat.LONG).format(date);
|
||||
ZonedDateTime zoneDate = DateTimeUtils.instantToZonedDateTime(date);
|
||||
String formattedDate = zoneDate == null ? "" : zoneDate.format(DateTimeUtils.longDateShortTimeFormatter);
|
||||
|
||||
padSpannableString(infoText);
|
||||
if (dateCheck.test(date)) {
|
||||
|
||||
@ -16,10 +16,10 @@ import java.io.IOException
|
||||
import java.math.BigDecimal
|
||||
import java.text.DateFormat
|
||||
import java.text.ParseException
|
||||
import java.time.Instant
|
||||
import java.time.ZonedDateTime
|
||||
import java.time.format.DateTimeParseException
|
||||
import java.util.Currency
|
||||
import java.util.Date
|
||||
|
||||
class PkpassParser(context: Context, uri: Uri?) {
|
||||
private var mContext = context
|
||||
@ -30,8 +30,8 @@ class PkpassParser(context: Context, uri: Uri?) {
|
||||
|
||||
private var store: String? = null
|
||||
private var note: String? = null
|
||||
private var validFrom: Date? = null
|
||||
private var expiry: Date? = null
|
||||
private var validFrom: Instant? = null
|
||||
private var expiry: Instant? = null
|
||||
private val balance: BigDecimal = BigDecimal(0)
|
||||
private val balanceType: Currency? = null
|
||||
// FIXME: Some cards may not have any barcodes, but Catima doesn't accept null card ID
|
||||
@ -200,8 +200,15 @@ class PkpassParser(context: Context, uri: Uri?) {
|
||||
return Color.rgb(red, green, blue)
|
||||
}
|
||||
|
||||
private fun parseDateTime(dateTime: String): Date {
|
||||
return Date.from(ZonedDateTime.parse(dateTime).toInstant())
|
||||
private fun parseDateTime(dateTime: String?): ZonedDateTime? {
|
||||
if(dateTime.isNullOrEmpty()) return null
|
||||
Log.d("PARSE", "parseDateTime: $dateTime")
|
||||
return try {
|
||||
ZonedDateTime.parse(dateTime)
|
||||
} catch (_: IllegalArgumentException) {
|
||||
// The string was not in the correct ISO 8601 format
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseLanguageStrings(data: String): Map<String, String> {
|
||||
@ -256,11 +263,11 @@ class PkpassParser(context: Context, uri: Uri?) {
|
||||
noteText.append(getTranslation(jsonObject.getString("description"), locale))
|
||||
|
||||
try {
|
||||
validFrom = parseDateTime(jsonObject.getString("relevantDate"))
|
||||
validFrom = parseDateTime(jsonObject.getString("relevantDate"))?.toInstant()
|
||||
} catch (ignored: JSONException) {}
|
||||
|
||||
try {
|
||||
expiry = parseDateTime(jsonObject.getString("expirationDate"))
|
||||
expiry = parseDateTime(jsonObject.getString("expirationDate"))?.toInstant()
|
||||
} catch (ignored: JSONException) {}
|
||||
|
||||
try {
|
||||
|
||||
@ -53,9 +53,9 @@ import com.google.android.material.color.DynamicColors;
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
import com.google.android.material.floatingactionbutton.FloatingActionButton;
|
||||
import com.google.zxing.BinaryBitmap;
|
||||
import com.google.zxing.DecodeHintType;
|
||||
import com.google.zxing.LuminanceSource;
|
||||
import com.google.zxing.MultiFormatReader;
|
||||
import com.google.zxing.DecodeHintType;
|
||||
import com.google.zxing.NotFoundException;
|
||||
import com.google.zxing.RGBLuminanceSource;
|
||||
import com.google.zxing.Result;
|
||||
@ -76,15 +76,14 @@ import java.math.BigDecimal;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.text.DecimalFormat;
|
||||
import java.text.DecimalFormatSymbols;
|
||||
import java.text.NumberFormat;
|
||||
import java.text.DecimalFormat;
|
||||
import java.text.ParseException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Calendar;
|
||||
import java.util.Collections;
|
||||
import java.util.Currency;
|
||||
import java.util.Date;
|
||||
import java.util.EnumMap;
|
||||
import java.util.GregorianCalendar;
|
||||
import java.util.List;
|
||||
@ -516,31 +515,6 @@ public class Utils {
|
||||
builder.show();
|
||||
}
|
||||
|
||||
static public Boolean isNotYetValid(Date validFromDate) {
|
||||
// The note in `hasExpired` does not apply here, since the bug was fixed before this feature was added.
|
||||
return validFromDate.after(getStartOfToday().getTime());
|
||||
}
|
||||
|
||||
static public Boolean hasExpired(Date expiryDate) {
|
||||
// Note: In #1083 it was discovered that `DatePickerFragment` may sometimes store the expiryDate
|
||||
// at 12:00 PM instead of 12:00 AM in the DB. While this has been fixed and the 12-hour difference
|
||||
// is not a problem for the way the comparison currently works, it's good to keep in mind such
|
||||
// dates may exist in the DB in case the comparison changes in the future and the new one relies
|
||||
// on both dates being set at 12:00 AM.
|
||||
return expiryDate.before(getStartOfToday().getTime());
|
||||
}
|
||||
|
||||
static private Calendar getStartOfToday() {
|
||||
// today
|
||||
Calendar date = new GregorianCalendar();
|
||||
// reset hour, minutes, seconds and millis
|
||||
date.set(Calendar.HOUR_OF_DAY, 0);
|
||||
date.set(Calendar.MINUTE, 0);
|
||||
date.set(Calendar.SECOND, 0);
|
||||
date.set(Calendar.MILLISECOND, 0);
|
||||
return date;
|
||||
}
|
||||
|
||||
static public String formatBalance(Context context, BigDecimal value, Currency currency) {
|
||||
NumberFormat numberFormat = NumberFormat.getInstance();
|
||||
numberFormat.setGroupingUsed(false);
|
||||
|
||||
@ -61,23 +61,24 @@ public class CatimaExporter implements Exporter {
|
||||
zipOutputStream.closeEntry();
|
||||
|
||||
// Loop over all cards again
|
||||
Cursor cardCursor = DBHelper.getLoyaltyCardCursor(database);
|
||||
while (cardCursor.moveToNext()) {
|
||||
// For each card
|
||||
LoyaltyCard card = LoyaltyCard.fromCursor(context, cardCursor);
|
||||
try(Cursor cardCursor = DBHelper.getLoyaltyCardCursor(database)){
|
||||
while (cardCursor.moveToNext()) {
|
||||
// For each card
|
||||
LoyaltyCard card = LoyaltyCard.fromCursor(context, cardCursor);
|
||||
|
||||
// For each image
|
||||
for (ImageLocationType imageLocationType : ImageLocationType.values()) {
|
||||
// If it exists, add to the .zip file
|
||||
Bitmap image = card.getImageForImageLocationType(context, imageLocationType);
|
||||
if (image != null) {
|
||||
ZipParameters imageZipParameters = createZipParameters(Utils.getCardImageFileName(card.id, imageLocationType), password);
|
||||
zipOutputStream.putNextEntry(imageZipParameters);
|
||||
InputStream imageInputStream = new ByteArrayInputStream(Utils.bitmapToByteArray(image));
|
||||
while ((readLen = imageInputStream.read(readBuffer)) != -1) {
|
||||
zipOutputStream.write(readBuffer, 0, readLen);
|
||||
// For each image
|
||||
for (ImageLocationType imageLocationType : ImageLocationType.values()) {
|
||||
// If it exists, add to the .zip file
|
||||
Bitmap image = card.getImageForImageLocationType(context, imageLocationType);
|
||||
if (image != null) {
|
||||
ZipParameters imageZipParameters = createZipParameters(Utils.getCardImageFileName(card.id, imageLocationType), password);
|
||||
zipOutputStream.putNextEntry(imageZipParameters);
|
||||
InputStream imageInputStream = new ByteArrayInputStream(Utils.bitmapToByteArray(image));
|
||||
while ((readLen = imageInputStream.read(readBuffer)) != -1) {
|
||||
zipOutputStream.write(readBuffer, 0, readLen);
|
||||
}
|
||||
zipOutputStream.closeEntry();
|
||||
}
|
||||
zipOutputStream.closeEntry();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -147,8 +148,8 @@ public class CatimaExporter implements Exporter {
|
||||
printer.printRecord(card.id,
|
||||
card.store,
|
||||
card.note,
|
||||
card.validFrom != null ? card.validFrom.getTime() : "",
|
||||
card.expiry != null ? card.expiry.getTime() : "",
|
||||
card.validFrom != null ? card.validFrom.toString() : "",
|
||||
card.expiry != null ? card.expiry.toString() : "",
|
||||
card.balance,
|
||||
card.balanceType,
|
||||
card.cardId,
|
||||
|
||||
@ -21,9 +21,10 @@ import java.io.InputStreamReader;
|
||||
import java.io.StringReader;
|
||||
import java.math.BigDecimal;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Instant;
|
||||
import java.time.format.DateTimeParseException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Currency;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@ -175,14 +176,12 @@ public class CatimaImporter implements Importer {
|
||||
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8));
|
||||
|
||||
int version = parseVersion(bufferedReader);
|
||||
switch (version) {
|
||||
case 1:
|
||||
return parseV1(bufferedReader);
|
||||
case 2:
|
||||
return parseV2(bufferedReader);
|
||||
default:
|
||||
throw new FormatException(String.format("No code to parse version %s", version));
|
||||
}
|
||||
return switch (version) {
|
||||
case 1 -> parseV1(bufferedReader);
|
||||
case 2 -> parseV2(bufferedReader);
|
||||
default ->
|
||||
throw new FormatException(String.format("No code to parse version %s", version));
|
||||
};
|
||||
}
|
||||
|
||||
public ImportedData parseV1(BufferedReader input) throws IOException, FormatException, InterruptedException {
|
||||
@ -403,26 +402,24 @@ public class CatimaImporter implements Importer {
|
||||
|
||||
String note = CSVHelpers.extractString(DBHelper.LoyaltyCardDbIds.NOTE, record, "");
|
||||
|
||||
Date validFrom = null;
|
||||
Long validFromLong;
|
||||
try {
|
||||
validFromLong = CSVHelpers.extractLong(DBHelper.LoyaltyCardDbIds.VALID_FROM, record);
|
||||
} catch (FormatException ignored) {
|
||||
validFromLong = null;
|
||||
}
|
||||
Instant validFrom = null;
|
||||
String validFromLong = CSVHelpers.extractString(DBHelper.LoyaltyCardDbIds.VALID_FROM, record, null);
|
||||
if (validFromLong != null) {
|
||||
validFrom = new Date(validFromLong);
|
||||
try {
|
||||
validFrom = Instant.parse(validFromLong);
|
||||
} catch (DateTimeParseException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
Date expiry = null;
|
||||
Long expiryLong;
|
||||
try {
|
||||
expiryLong = CSVHelpers.extractLong(DBHelper.LoyaltyCardDbIds.EXPIRY, record);
|
||||
} catch (FormatException ignored) {
|
||||
expiryLong = null;
|
||||
}
|
||||
Instant expiry = null;
|
||||
String expiryLong = CSVHelpers.extractString(DBHelper.LoyaltyCardDbIds.EXPIRY, record, null);
|
||||
if (expiryLong != null) {
|
||||
expiry = new Date(expiryLong);
|
||||
try {
|
||||
expiry = Instant.parse(expiryLong);
|
||||
} catch (DateTimeParseException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
// These fields did not exist in versions 1.8.1 and before
|
||||
|
||||
@ -28,53 +28,41 @@ public class MultiFormatImporter {
|
||||
* the database.
|
||||
*/
|
||||
public static ImportExportResult importData(Context context, SQLiteDatabase database, InputStream input, DataFormat format, char[] password) {
|
||||
Importer importer = null;
|
||||
Importer importer = switch (format) {
|
||||
case Catima -> new CatimaImporter();
|
||||
case Fidme -> new FidmeImporter();
|
||||
case VoucherVault -> new VoucherVaultImporter();
|
||||
};
|
||||
|
||||
switch (format) {
|
||||
case Catima:
|
||||
importer = new CatimaImporter();
|
||||
break;
|
||||
case Fidme:
|
||||
importer = new FidmeImporter();
|
||||
break;
|
||||
case VoucherVault:
|
||||
importer = new VoucherVaultImporter();
|
||||
break;
|
||||
}
|
||||
String error;
|
||||
File inputFile;
|
||||
|
||||
String error = null;
|
||||
if (importer != null) {
|
||||
File inputFile;
|
||||
try {
|
||||
inputFile = Utils.copyToTempFile(context, input, TEMP_ZIP_NAME);
|
||||
database.beginTransaction();
|
||||
try {
|
||||
inputFile = Utils.copyToTempFile(context, input, TEMP_ZIP_NAME);
|
||||
database.beginTransaction();
|
||||
try {
|
||||
importer.importData(context, database, inputFile, password);
|
||||
database.setTransactionSuccessful();
|
||||
return new ImportExportResult(ImportExportResultType.Success);
|
||||
} catch (ZipException e) {
|
||||
if (e.getType().equals(ZipException.Type.WRONG_PASSWORD)) {
|
||||
return new ImportExportResult(ImportExportResultType.BadPassword);
|
||||
} else {
|
||||
Log.e(TAG, "Failed to import data", e);
|
||||
error = e.toString();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
importer.importData(context, database, inputFile, password);
|
||||
database.setTransactionSuccessful();
|
||||
return new ImportExportResult(ImportExportResultType.Success);
|
||||
} catch (ZipException e) {
|
||||
if (e.getType().equals(ZipException.Type.WRONG_PASSWORD)) {
|
||||
return new ImportExportResult(ImportExportResultType.BadPassword);
|
||||
} else {
|
||||
Log.e(TAG, "Failed to import data", e);
|
||||
error = e.toString();
|
||||
} finally {
|
||||
database.endTransaction();
|
||||
if (!inputFile.delete()) {
|
||||
Log.w(TAG, "Failed to delete temporary ZIP file (should not be a problem) " + inputFile);
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "Failed to copy ZIP file", e);
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Failed to import data", e);
|
||||
error = e.toString();
|
||||
} finally {
|
||||
database.endTransaction();
|
||||
if (!inputFile.delete()) {
|
||||
Log.w(TAG, "Failed to delete temporary ZIP file (should not be a problem) " + inputFile);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
error = "Unsupported data format imported: " + format.name();
|
||||
Log.e(TAG, error);
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "Failed to copy ZIP file", e);
|
||||
error = e.toString();
|
||||
}
|
||||
|
||||
return new ImportExportResult(ImportExportResultType.GenericFailure, error);
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
package protect.card_locker.importexport;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.graphics.Color;
|
||||
@ -20,12 +19,13 @@ import java.io.InputStreamReader;
|
||||
import java.math.BigDecimal;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.text.ParseException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
import java.time.format.DateTimeParseException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Currency;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.TimeZone;
|
||||
|
||||
import protect.card_locker.CatimaBarcode;
|
||||
import protect.card_locker.DBHelper;
|
||||
@ -76,11 +76,21 @@ public class VoucherVaultImporter implements Importer {
|
||||
|
||||
String store = jsonCard.getString("description");
|
||||
|
||||
Date expiry = null;
|
||||
Instant expiry = null;
|
||||
if (!jsonCard.isNull("expires")) {
|
||||
@SuppressLint("SimpleDateFormat") SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS");
|
||||
dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
|
||||
expiry = dateFormat.parse(jsonCard.getString("expires"));
|
||||
try {
|
||||
String expiryString = jsonCard.getString("expires");
|
||||
|
||||
// Parse the string as a LocalDateTime since it has no timezone info
|
||||
LocalDateTime localDateTime = LocalDateTime.parse(expiryString);
|
||||
|
||||
// Convert it to an Instant, specifying it represents a time in UTC
|
||||
expiry = localDateTime.toInstant(ZoneOffset.UTC);
|
||||
|
||||
} catch (JSONException | DateTimeParseException e) {
|
||||
// Handle cases where the key is missing or the date format is invalid
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
BigDecimal balance = new BigDecimal("0");
|
||||
|
||||
@ -2,11 +2,21 @@ package protect.card_locker.viewmodels
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.lifecycle.ViewModel
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import protect.card_locker.LoyaltyCard
|
||||
import protect.card_locker.LoyaltyCardField
|
||||
import protect.card_locker.async.TaskHandler
|
||||
|
||||
data class LoyaltyCardEditState(
|
||||
val loyaltyCardDateType: LoyaltyCardDateType? = null,
|
||||
)
|
||||
class LoyaltyCardEditActivityViewModel : ViewModel() {
|
||||
|
||||
private val _state = MutableStateFlow(LoyaltyCardEditState())
|
||||
val state : StateFlow<LoyaltyCardEditState> = _state.asStateFlow()
|
||||
|
||||
var initialized: Boolean = false
|
||||
var hasChanged: Boolean = false
|
||||
|
||||
@ -24,4 +34,20 @@ class LoyaltyCardEditActivityViewModel : ViewModel() {
|
||||
var tempLoyaltyCardField: LoyaltyCardField? = null
|
||||
|
||||
var loyaltyCard: LoyaltyCard = LoyaltyCard()
|
||||
|
||||
fun onAction(action: LoyaltyCardEditUiAction) {
|
||||
when (action) {
|
||||
is LoyaltyCardEditUiAction.SetLoyaltyCardDateType -> {
|
||||
_state.value = _state.value.copy(loyaltyCardDateType = action.loyaltyCardDateType)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum class LoyaltyCardDateType {
|
||||
VALID_FROM, EXPIRY
|
||||
}
|
||||
|
||||
sealed interface LoyaltyCardEditUiAction {
|
||||
data class SetLoyaltyCardDateType(val loyaltyCardDateType: LoyaltyCardDateType) : LoyaltyCardEditUiAction
|
||||
}
|
||||
278
app/src/test/java/protect/card_locker/DateTimeUtilsTest.java
Normal file
278
app/src/test/java/protect/card_locker/DateTimeUtilsTest.java
Normal file
@ -0,0 +1,278 @@
|
||||
package protect.card_locker;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.robolectric.RobolectricTestRunner;
|
||||
import org.robolectric.annotation.Config;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDate;
|
||||
import java.time.ZoneId;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
|
||||
@RunWith(RobolectricTestRunner.class)
|
||||
@Config(sdk = {28})
|
||||
public class DateTimeUtilsTest {
|
||||
|
||||
@Test
|
||||
public void testLongToInstant() {
|
||||
// Given
|
||||
long epochMillis = 1609459200000L; // 2021-01-01 00:00:00 UTC
|
||||
|
||||
// When
|
||||
Instant result = DateTimeUtils.longToInstant(epochMillis);
|
||||
|
||||
// Then
|
||||
assertNotNull(result);
|
||||
assertEquals(epochMillis, result.toEpochMilli());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLongToInstant_withNull() {
|
||||
// Given
|
||||
Long nullMillis = null;
|
||||
|
||||
// When
|
||||
Instant result = DateTimeUtils.longToInstant(nullMillis);
|
||||
|
||||
// Then
|
||||
assertNull(result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInstantToZonedDateTime() {
|
||||
// Given
|
||||
Instant instant = Instant.parse("2024-01-15T10:30:00Z");
|
||||
|
||||
// When
|
||||
ZonedDateTime result = DateTimeUtils.instantToZonedDateTime(instant);
|
||||
|
||||
// Then
|
||||
assertNotNull(result);
|
||||
assertEquals(instant, result.toInstant());
|
||||
assertEquals(ZoneId.systemDefault(), result.getZone());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInstantToZonedDateTime_withNull() {
|
||||
// When
|
||||
ZonedDateTime result = DateTimeUtils.instantToZonedDateTime(null);
|
||||
|
||||
// Then
|
||||
assertNull(result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testIsNotYetValid_withFutureDate() {
|
||||
// Given - A date in the future (tomorrow)
|
||||
Instant futureDate = Instant.now().plus(1, ChronoUnit.DAYS);
|
||||
|
||||
// When
|
||||
boolean result = DateTimeUtils.isNotYetValid(futureDate);
|
||||
|
||||
// Then
|
||||
assertTrue("Future date should be 'not yet valid'", result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testIsNotYetValid_withPastDate() {
|
||||
// Given - A date in the past (yesterday)
|
||||
Instant pastDate = Instant.now().minus(1, ChronoUnit.DAYS);
|
||||
|
||||
// When
|
||||
boolean result = DateTimeUtils.isNotYetValid(pastDate);
|
||||
|
||||
// Then
|
||||
assertFalse("Past date should not be 'not yet valid'", result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testIsNotYetValid_withTodayStart() {
|
||||
// Given - Start of today
|
||||
LocalDate today = LocalDate.now(ZoneId.systemDefault());
|
||||
ZonedDateTime startOfToday = today.atStartOfDay(ZoneId.systemDefault());
|
||||
Instant todayStart = startOfToday.toInstant();
|
||||
|
||||
// When
|
||||
boolean result = DateTimeUtils.isNotYetValid(todayStart);
|
||||
|
||||
// Then
|
||||
assertFalse("Start of today should not be 'not yet valid'", result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testIsNotYetValid_withNull() {
|
||||
// When
|
||||
boolean result = DateTimeUtils.isNotYetValid(null);
|
||||
|
||||
// Then
|
||||
assertFalse("Null date should not be 'not yet valid'", result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testHasExpired_withPastDate() {
|
||||
// Given - A date in the past (yesterday)
|
||||
Instant pastDate = Instant.now().minus(1, ChronoUnit.DAYS);
|
||||
|
||||
// When
|
||||
boolean result = DateTimeUtils.hasExpired(pastDate);
|
||||
|
||||
// Then
|
||||
assertTrue("Past date should be expired", result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testHasExpired_withFutureDate() {
|
||||
// Given - A date in the future (tomorrow)
|
||||
Instant futureDate = Instant.now().plus(1, ChronoUnit.DAYS);
|
||||
|
||||
// When
|
||||
boolean result = DateTimeUtils.hasExpired(futureDate);
|
||||
|
||||
// Then
|
||||
assertFalse("Future date should not be expired", result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testHasExpired_withTodayStart() {
|
||||
// Given - Start of today
|
||||
LocalDate today = LocalDate.now(ZoneId.systemDefault());
|
||||
ZonedDateTime startOfToday = today.atStartOfDay(ZoneId.systemDefault());
|
||||
Instant todayStart = startOfToday.toInstant();
|
||||
|
||||
// When
|
||||
boolean result = DateTimeUtils.hasExpired(todayStart);
|
||||
|
||||
// Then
|
||||
assertTrue("Start of today should be considered expired", result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testHasExpired_withNull() {
|
||||
// When
|
||||
boolean result = DateTimeUtils.hasExpired(null);
|
||||
|
||||
// Then
|
||||
assertFalse("Null expiry should never expire", result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFormatMedium_withValidInstant() {
|
||||
// Given
|
||||
Instant instant = Instant.parse("2024-01-15T10:30:00Z");
|
||||
|
||||
// When
|
||||
String result = DateTimeUtils.formatMedium(instant);
|
||||
|
||||
// Then
|
||||
assertNotNull(result);
|
||||
assertFalse(result.isEmpty());
|
||||
|
||||
// Verify it follows the expected pattern by parsing it back
|
||||
ZonedDateTime zdt = Instant.parse("2024-01-15T10:30:00Z")
|
||||
.atZone(ZoneId.systemDefault());
|
||||
String expected = zdt.format(DateTimeFormatter.ofLocalizedDateTime(
|
||||
java.time.format.FormatStyle.MEDIUM,
|
||||
java.time.format.FormatStyle.SHORT
|
||||
));
|
||||
assertEquals(expected, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFormatMedium_withNull() {
|
||||
// When
|
||||
String result = DateTimeUtils.formatMedium(null);
|
||||
|
||||
// Then
|
||||
assertNull(result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFormatLong_withValidInstant() {
|
||||
// Given
|
||||
Instant instant = Instant.parse("2024-01-15T10:30:00Z");
|
||||
|
||||
// When
|
||||
String result = DateTimeUtils.formatLong(instant);
|
||||
|
||||
// Then
|
||||
assertNotNull(result);
|
||||
assertFalse(result.isEmpty());
|
||||
|
||||
// Verify it follows the expected pattern by parsing it back
|
||||
ZonedDateTime zdt = Instant.parse("2024-01-15T10:30:00Z")
|
||||
.atZone(ZoneId.systemDefault());
|
||||
String expected = zdt.format(DateTimeFormatter.ofLocalizedDateTime(
|
||||
java.time.format.FormatStyle.LONG,
|
||||
java.time.format.FormatStyle.SHORT
|
||||
));
|
||||
assertEquals(expected, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFormatLong_withNull() {
|
||||
// When
|
||||
String result = DateTimeUtils.formatLong(null);
|
||||
|
||||
// Then
|
||||
assertNull(result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDateTimeFormattersInitialization() {
|
||||
// Verify that the formatters are properly initialized
|
||||
assertNotNull(DateTimeUtils.longDateShortTimeFormatter);
|
||||
assertNotNull(DateTimeUtils.mediumDateShortTimeFormatter);
|
||||
|
||||
// Test that formatters work correctly
|
||||
Instant instant = Instant.parse("2024-01-15T10:30:00Z");
|
||||
ZonedDateTime zdt = instant.atZone(ZoneId.systemDefault());
|
||||
|
||||
String longFormat = zdt.format(DateTimeUtils.longDateShortTimeFormatter);
|
||||
String mediumFormat = zdt.format(DateTimeUtils.mediumDateShortTimeFormatter);
|
||||
|
||||
assertNotNull(longFormat);
|
||||
assertNotNull(mediumFormat);
|
||||
assertFalse(longFormat.isEmpty());
|
||||
assertFalse(mediumFormat.isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testIsNotYetValid_RelativeToNow() {
|
||||
Instant now = Instant.now();
|
||||
|
||||
// Test with timestamps relative to current moment
|
||||
Instant oneMinuteAgo = now.minus(1, ChronoUnit.MINUTES);
|
||||
Instant oneMinuteLater = now.plus(1, ChronoUnit.MINUTES);
|
||||
Instant oneHourLater = now.plus(1, ChronoUnit.HOURS);
|
||||
|
||||
// One minute ago should be valid (not "not yet valid")
|
||||
assertFalse(DateTimeUtils.isNotYetValid(oneMinuteAgo));
|
||||
|
||||
// Future times should be "not yet valid"
|
||||
assertTrue(DateTimeUtils.isNotYetValid(oneMinuteLater));
|
||||
assertTrue(DateTimeUtils.isNotYetValid(oneHourLater));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testConsistencyBetweenMethods() {
|
||||
// Test that the methods work consistently together
|
||||
long epochMillis = 1609459200000L; // 2021-01-01 00:00:00 UTC
|
||||
|
||||
Instant instant = DateTimeUtils.longToInstant(epochMillis);
|
||||
ZonedDateTime zonedDateTime = DateTimeUtils.instantToZonedDateTime(instant);
|
||||
|
||||
assertNotNull(instant);
|
||||
assertNotNull(zonedDateTime);
|
||||
assertEquals(epochMillis, instant.toEpochMilli());
|
||||
assertEquals(instant, zonedDateTime.toInstant());
|
||||
}
|
||||
}
|
||||
@ -17,6 +17,7 @@ import android.os.Looper;
|
||||
|
||||
import com.google.zxing.BarcodeFormat;
|
||||
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
@ -37,10 +38,11 @@ import java.io.InputStream;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Currency;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
|
||||
@ -54,6 +56,7 @@ import protect.card_locker.importexport.MultiFormatImporter;
|
||||
@RunWith(RobolectricTestRunner.class)
|
||||
public class ImportExportTest {
|
||||
private Activity activity;
|
||||
private DBHelper mDbHelper;
|
||||
private SQLiteDatabase mDatabase;
|
||||
|
||||
private final String BARCODE_DATA = "428311627547";
|
||||
@ -64,7 +67,17 @@ public class ImportExportTest {
|
||||
ShadowLog.stream = System.out;
|
||||
|
||||
activity = Robolectric.setupActivity(MainActivity.class);
|
||||
mDatabase = TestHelpers.getEmptyDb(activity).getWritableDatabase();
|
||||
mDbHelper = TestHelpers.getEmptyDb(activity);
|
||||
mDatabase = mDbHelper.getWritableDatabase();
|
||||
}
|
||||
|
||||
@After
|
||||
public void tearDown() {
|
||||
// This method runs after every test and is the perfect place to clean up.
|
||||
// Closing the helper will also close the underlying database connection.
|
||||
if (mDbHelper != null) {
|
||||
mDbHelper.close();
|
||||
}
|
||||
}
|
||||
|
||||
private void addLoyaltyCardsFiveStarred() {
|
||||
@ -107,7 +120,7 @@ public class ImportExportTest {
|
||||
assertEquals(Integer.valueOf(0), card.headerColor);
|
||||
assertEquals(0, card.starStatus);
|
||||
|
||||
id = DBHelper.insertLoyaltyCard(mDatabase, "Past", "", null, new Date((long) 1), new BigDecimal("0"), null, BARCODE_DATA, null, BARCODE_TYPE, 0, 0, null,0);
|
||||
id = DBHelper.insertLoyaltyCard(mDatabase, "Past", "", null, Instant.ofEpochMilli(1L), new BigDecimal("0"), null, BARCODE_DATA, null, BARCODE_TYPE, 0, 0, null,0);
|
||||
result = (id != -1);
|
||||
assertTrue(result);
|
||||
|
||||
@ -115,7 +128,7 @@ public class ImportExportTest {
|
||||
assertEquals("Past", card.store);
|
||||
assertEquals("", card.note);
|
||||
assertEquals(null, card.validFrom);
|
||||
assertTrue(card.expiry.before(new Date()));
|
||||
assertTrue(card.expiry.isBefore(Instant.now()));
|
||||
assertEquals(new BigDecimal("0"), card.balance);
|
||||
assertEquals(null, card.balanceType);
|
||||
assertEquals(BARCODE_DATA, card.cardId);
|
||||
@ -124,7 +137,7 @@ public class ImportExportTest {
|
||||
assertEquals(Integer.valueOf(0), card.headerColor);
|
||||
assertEquals(0, card.starStatus);
|
||||
|
||||
id = DBHelper.insertLoyaltyCard(mDatabase, "Today", "", null, new Date(), new BigDecimal("0"), null, BARCODE_DATA, null, BARCODE_TYPE, 0, 0, null,0);
|
||||
id = DBHelper.insertLoyaltyCard(mDatabase, "Today", "", null, Instant.now(), new BigDecimal("0"), null, BARCODE_DATA, null, BARCODE_TYPE, 0, 0, null,0);
|
||||
result = (id != -1);
|
||||
assertTrue(result);
|
||||
|
||||
@ -132,8 +145,8 @@ public class ImportExportTest {
|
||||
assertEquals("Today", card.store);
|
||||
assertEquals("", card.note);
|
||||
assertEquals(null, card.validFrom);
|
||||
assertTrue(card.expiry.before(new Date(new Date().getTime() + 86400)));
|
||||
assertTrue(card.expiry.after(new Date(new Date().getTime() - 86400)));
|
||||
assertTrue(card.expiry.isBefore(Instant.now().plus(1L, ChronoUnit.DAYS)));
|
||||
assertTrue(card.expiry.isAfter(Instant.now().minus(1L, ChronoUnit.DAYS)));
|
||||
assertEquals(new BigDecimal("0"), card.balance);
|
||||
assertEquals(null, card.balanceType);
|
||||
assertEquals(BARCODE_DATA, card.cardId);
|
||||
@ -144,7 +157,7 @@ public class ImportExportTest {
|
||||
|
||||
// This will break after 19 January 2038
|
||||
// If someone is still maintaining this code base by then: I love you
|
||||
id = DBHelper.insertLoyaltyCard(mDatabase, "Future", "", null, new Date(2147483648000L), new BigDecimal("0"), null, BARCODE_DATA, null, BARCODE_TYPE, 0, 0, null,0);
|
||||
id = DBHelper.insertLoyaltyCard(mDatabase, "Future", "", null, Instant.ofEpochMilli(2147483648000L), new BigDecimal("0"), null, BARCODE_DATA, null, BARCODE_TYPE, 0, 0, null,0);
|
||||
result = (id != -1);
|
||||
assertTrue(result);
|
||||
|
||||
@ -152,7 +165,7 @@ public class ImportExportTest {
|
||||
assertEquals("Future", card.store);
|
||||
assertEquals("", card.note);
|
||||
assertEquals(null, card.validFrom);
|
||||
assertTrue(card.expiry.after(new Date(new Date().getTime() + 86400)));
|
||||
assertTrue(card.expiry.isAfter(Instant.now().plus(1L, ChronoUnit.DAYS)));
|
||||
assertEquals(new BigDecimal("0"), card.balance);
|
||||
assertEquals(null, card.balanceType);
|
||||
assertEquals(BARCODE_DATA, card.cardId);
|
||||
@ -830,7 +843,7 @@ public class ImportExportTest {
|
||||
HashMap<Integer, Bitmap> loyaltyCardIconImages = new HashMap<>();
|
||||
|
||||
// Create card 1
|
||||
int loyaltyCardId = (int) DBHelper.insertLoyaltyCard(mDatabase, "Card 1", "Note 1", new Date(1601510400), new Date(1618053234), new BigDecimal("100"), Currency.getInstance("USD"), "1234", "5432", CatimaBarcode.fromBarcode(BarcodeFormat.QR_CODE), 1, 0, null,0);
|
||||
int loyaltyCardId = (int) DBHelper.insertLoyaltyCard(mDatabase, "Card 1", "Note 1", Instant.ofEpochMilli(1601510400000L), Instant.ofEpochMilli(1618053234000L), new BigDecimal("100"), Currency.getInstance("USD"), "1234", "5432", CatimaBarcode.fromBarcode(BarcodeFormat.QR_CODE), 1, 0, null,0);
|
||||
loyaltyCardHashMap.put(loyaltyCardId, DBHelper.getLoyaltyCard(activity.getApplicationContext(), mDatabase, loyaltyCardId));
|
||||
DBHelper.insertGroup(mDatabase, "One");
|
||||
List<Group> groups = Arrays.asList(DBHelper.getGroup(mDatabase, "One"));
|
||||
@ -954,8 +967,8 @@ public class ImportExportTest {
|
||||
|
||||
assertEquals("Card 1", card1.store);
|
||||
assertEquals("Note 1", card1.note);
|
||||
assertEquals(new Date(1601510400), card1.validFrom);
|
||||
assertEquals(new Date(1618053234), card1.expiry);
|
||||
assertEquals(Instant.parse("2020-10-01T00:00:00Z"), card1.validFrom);
|
||||
assertEquals(Instant.parse("2021-04-10T11:13:54Z"), card1.expiry);
|
||||
assertEquals(new BigDecimal("100"), card1.balance);
|
||||
assertEquals(Currency.getInstance("USD"), card1.balanceType);
|
||||
assertEquals("1234", card1.cardId);
|
||||
@ -989,7 +1002,7 @@ public class ImportExportTest {
|
||||
assertEquals("Department Store", card2.store);
|
||||
assertEquals("", card2.note);
|
||||
assertEquals(null, card2.validFrom);
|
||||
assertEquals(new Date(1618041729), card2.expiry);
|
||||
assertEquals(Instant.parse("2021-04-10T08:02:09Z"), card2.expiry);
|
||||
assertEquals(new BigDecimal("0"), card2.balance);
|
||||
assertEquals(null, card2.balanceType);
|
||||
assertEquals("A", card2.cardId);
|
||||
@ -1151,7 +1164,7 @@ public class ImportExportTest {
|
||||
assertEquals("Department Store", card.store);
|
||||
assertEquals("", card.note);
|
||||
assertEquals(null, card.validFrom);
|
||||
assertEquals(new Date(1616716800000L), card.expiry);
|
||||
assertEquals(Instant.ofEpochMilli(1616716800000L), card.expiry);
|
||||
assertEquals(new BigDecimal("3.5"), card.balance);
|
||||
assertEquals(Currency.getInstance("USD"), card.balanceType);
|
||||
assertEquals("26846363", card.cardId);
|
||||
|
||||
@ -20,8 +20,8 @@ import org.robolectric.RobolectricTestRunner;
|
||||
import java.io.InvalidObjectException;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.Instant;
|
||||
import java.util.Currency;
|
||||
import java.util.Date;
|
||||
|
||||
@RunWith(RobolectricTestRunner.class)
|
||||
public class ImportURITest {
|
||||
@ -39,7 +39,7 @@ public class ImportURITest {
|
||||
@Test
|
||||
public void ensureNoDataLoss() throws InvalidObjectException, UnsupportedEncodingException {
|
||||
// Generate card
|
||||
Date date = new Date();
|
||||
Instant date = Instant.now();
|
||||
|
||||
DBHelper.insertLoyaltyCard(mDatabase, "store", "This note contains evil symbols like & and = that will break the parser if not escaped right $#!%()*+;:á", date, date, new BigDecimal("100"), null, BarcodeFormat.UPC_E.toString(), BarcodeFormat.UPC_A.toString(), CatimaBarcode.fromBarcode(BarcodeFormat.QR_CODE), Color.BLACK, 1, null,0);
|
||||
|
||||
|
||||
@ -26,6 +26,7 @@ import android.graphics.drawable.BitmapDrawable;
|
||||
import android.graphics.drawable.ColorDrawable;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.os.Looper;
|
||||
import android.view.Menu;
|
||||
import android.view.View;
|
||||
import android.view.WindowInsets;
|
||||
@ -41,16 +42,21 @@ import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.constraintlayout.widget.ConstraintLayout;
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
|
||||
import com.google.android.material.bottomappbar.BottomAppBar;
|
||||
import com.google.android.material.datepicker.MaterialDatePicker;
|
||||
import com.google.android.material.floatingactionbutton.FloatingActionButton;
|
||||
import com.google.android.material.tabs.TabLayout;
|
||||
import com.google.android.material.textfield.MaterialAutoCompleteTextView;
|
||||
import com.google.android.material.textfield.TextInputLayout;
|
||||
import com.google.android.material.timepicker.MaterialTimePicker;
|
||||
import com.google.zxing.BarcodeFormat;
|
||||
import com.google.zxing.client.android.Intents;
|
||||
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
@ -65,12 +71,15 @@ import org.robolectric.shadows.ShadowLog;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.math.BigDecimal;
|
||||
import java.text.DateFormat;
|
||||
import java.text.ParseException;
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalTime;
|
||||
import java.time.ZoneId;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.Currency;
|
||||
import java.util.Date;
|
||||
|
||||
import protect.card_locker.viewmodels.LoyaltyCardDateType;
|
||||
|
||||
@RunWith(RobolectricTestRunner.class)
|
||||
public class LoyaltyCardViewActivityTest {
|
||||
@ -84,7 +93,6 @@ public class LoyaltyCardViewActivityTest {
|
||||
ADD_CARD,
|
||||
VIEW_CARD,
|
||||
UPDATE_CARD,
|
||||
;
|
||||
}
|
||||
|
||||
enum FieldTypeView {
|
||||
@ -93,10 +101,23 @@ public class LoyaltyCardViewActivityTest {
|
||||
ImageView
|
||||
}
|
||||
|
||||
Context context;
|
||||
ActivityController<LoyaltyCardEditActivity> activityController;
|
||||
LoyaltyCardEditActivity activity;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
// Output logs emitted during tests so they may be accessed
|
||||
ShadowLog.stream = System.out;
|
||||
context = ApplicationProvider.getApplicationContext();
|
||||
activityController = Robolectric.buildActivity(LoyaltyCardEditActivity.class);
|
||||
activity = activityController.get();
|
||||
activityController.setup();
|
||||
}
|
||||
|
||||
@After
|
||||
public void tearDown() {
|
||||
activityController.destroy();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -176,14 +197,14 @@ public class LoyaltyCardViewActivityTest {
|
||||
if (validFrom.equals(activity.getApplicationContext().getString(R.string.anyDate))) {
|
||||
assertEquals(null, card.validFrom);
|
||||
} else {
|
||||
assertEquals(DateFormat.getDateInstance().parse(validFrom), card.validFrom);
|
||||
assertEquals(Instant.parse(validFrom), card.validFrom);
|
||||
}
|
||||
|
||||
// The special "Never" string shouldn't actually be written to the loyalty card
|
||||
if (expiry.equals(activity.getApplicationContext().getString(R.string.never))) {
|
||||
assertEquals(null, card.expiry);
|
||||
} else {
|
||||
assertEquals(DateFormat.getDateInstance().parse(expiry), card.expiry);
|
||||
assertEquals(Instant.parse(expiry), card.expiry);
|
||||
}
|
||||
|
||||
// The special "Points" string shouldn't actually be written to the loyalty card
|
||||
@ -345,7 +366,7 @@ public class LoyaltyCardViewActivityTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
@Config(qualifiers="de")
|
||||
@Config(qualifiers = "de")
|
||||
public void noCrashOnRegionlessLocale() {
|
||||
ActivityController activityController = Robolectric.buildActivity(LoyaltyCardEditActivity.class).create();
|
||||
|
||||
@ -410,17 +431,17 @@ public class LoyaltyCardViewActivityTest {
|
||||
final ImageView backImageView = activity.findViewById(R.id.backImage);
|
||||
|
||||
Currency currency = Currency.getInstance("EUR");
|
||||
Date validFromDate = Date.from(Instant.now().minus(20, ChronoUnit.DAYS));
|
||||
Date expiryDate = new Date();
|
||||
ZonedDateTime validFromDate = ZonedDateTime.now().minusDays(20);
|
||||
ZonedDateTime expiryDate = ZonedDateTime.now();
|
||||
Bitmap frontBitmap = BitmapFactory.decodeResource(context.getResources(), R.drawable.circle);
|
||||
Bitmap backBitmap = BitmapFactory.decodeResource(context.getResources(), R.drawable.ic_done);
|
||||
|
||||
storeField.setText("correct store");
|
||||
noteField.setText("correct note");
|
||||
LoyaltyCardEditActivity.formatDateField(context, validFromField, validFromDate);
|
||||
activity.setLoyaltyCardValidFrom(validFromDate);
|
||||
activity.setLoyaltyCardValidFrom(validFromDate.toInstant());
|
||||
LoyaltyCardEditActivity.formatDateField(context, expiryField, expiryDate);
|
||||
activity.setLoyaltyCardExpiry(expiryDate);
|
||||
activity.setLoyaltyCardExpiry(expiryDate.toInstant());
|
||||
balanceField.setText("100");
|
||||
balanceTypeField.setText(currency.getSymbol());
|
||||
cardIdField.setText("12345678");
|
||||
@ -432,7 +453,7 @@ public class LoyaltyCardViewActivityTest {
|
||||
shadowOf(getMainLooper()).idle();
|
||||
|
||||
// Check if changed
|
||||
checkAllFields(activity, newCard ? ViewMode.ADD_CARD : ViewMode.UPDATE_CARD, "correct store", "correct note", DateFormat.getDateInstance(DateFormat.LONG).format(validFromDate), DateFormat.getDateInstance(DateFormat.LONG).format(expiryDate), "100.00", currency.getSymbol(), "12345678", "87654321", CatimaBarcode.fromBarcode(BarcodeFormat.QR_CODE).prettyName(), frontBitmap, backBitmap);
|
||||
checkAllFields(activity, newCard ? ViewMode.ADD_CARD : ViewMode.UPDATE_CARD, "correct store", "correct note", validFromDate.format(DateTimeUtils.longDateShortTimeFormatter), expiryDate.format(DateTimeUtils.longDateShortTimeFormatter), "100.00", currency.getSymbol(), "12345678", "87654321", CatimaBarcode.fromBarcode(BarcodeFormat.QR_CODE).prettyName(), frontBitmap, backBitmap);
|
||||
|
||||
// Resume
|
||||
activityController.pause();
|
||||
@ -441,7 +462,7 @@ public class LoyaltyCardViewActivityTest {
|
||||
shadowOf(getMainLooper()).idle();
|
||||
|
||||
// Check if no changes lost
|
||||
checkAllFields(activity, newCard ? ViewMode.ADD_CARD : ViewMode.UPDATE_CARD, "correct store", "correct note", DateFormat.getDateInstance(DateFormat.LONG).format(validFromDate), DateFormat.getDateInstance(DateFormat.LONG).format(expiryDate), "100.00", currency.getSymbol(), "12345678", "87654321", CatimaBarcode.fromBarcode(BarcodeFormat.QR_CODE).prettyName(), frontBitmap, backBitmap);
|
||||
checkAllFields(activity, newCard ? ViewMode.ADD_CARD : ViewMode.UPDATE_CARD, "correct store", "correct note", validFromDate.format(DateTimeUtils.longDateShortTimeFormatter), expiryDate.format(DateTimeUtils.longDateShortTimeFormatter), "100.00", currency.getSymbol(), "12345678", "87654321", CatimaBarcode.fromBarcode(BarcodeFormat.QR_CODE).prettyName(), frontBitmap, backBitmap);
|
||||
|
||||
// Rotate to landscape
|
||||
activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
|
||||
@ -449,7 +470,7 @@ public class LoyaltyCardViewActivityTest {
|
||||
shadowOf(getMainLooper()).idle();
|
||||
|
||||
// Check if no changes lost
|
||||
checkAllFields(activity, newCard ? ViewMode.ADD_CARD : ViewMode.UPDATE_CARD, "correct store", "correct note", DateFormat.getDateInstance(DateFormat.LONG).format(validFromDate), DateFormat.getDateInstance(DateFormat.LONG).format(expiryDate), "100.00", currency.getSymbol(), "12345678", "87654321", CatimaBarcode.fromBarcode(BarcodeFormat.QR_CODE).prettyName(), frontBitmap, backBitmap);
|
||||
checkAllFields(activity, newCard ? ViewMode.ADD_CARD : ViewMode.UPDATE_CARD, "correct store", "correct note", validFromDate.format(DateTimeUtils.longDateShortTimeFormatter), expiryDate.format(DateTimeUtils.longDateShortTimeFormatter), "100.00", currency.getSymbol(), "12345678", "87654321", CatimaBarcode.fromBarcode(BarcodeFormat.QR_CODE).prettyName(), frontBitmap, backBitmap);
|
||||
|
||||
// Rotate to portrait
|
||||
shadowOf(getMainLooper()).idle();
|
||||
@ -457,7 +478,7 @@ public class LoyaltyCardViewActivityTest {
|
||||
activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
|
||||
|
||||
// Check if no changes lost
|
||||
checkAllFields(activity, newCard ? ViewMode.ADD_CARD : ViewMode.UPDATE_CARD, "correct store", "correct note", DateFormat.getDateInstance(DateFormat.LONG).format(validFromDate), DateFormat.getDateInstance(DateFormat.LONG).format(expiryDate), "100.00", currency.getSymbol(), "12345678", "87654321", CatimaBarcode.fromBarcode(BarcodeFormat.QR_CODE).prettyName(), frontBitmap, backBitmap);
|
||||
checkAllFields(activity, newCard ? ViewMode.ADD_CARD : ViewMode.UPDATE_CARD, "correct store", "correct note", validFromDate.format(DateTimeUtils.longDateShortTimeFormatter), expiryDate.format(DateTimeUtils.longDateShortTimeFormatter), "100.00", currency.getSymbol(), "12345678", "87654321", CatimaBarcode.fromBarcode(BarcodeFormat.QR_CODE).prettyName(), frontBitmap, backBitmap);
|
||||
}
|
||||
}
|
||||
|
||||
@ -726,14 +747,83 @@ public class LoyaltyCardViewActivityTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void startWithLoyaltyCardNoExpirySetExpiry() throws IOException {
|
||||
final Context context = ApplicationProvider.getApplicationContext();
|
||||
SQLiteDatabase database = TestHelpers.getEmptyDb(context).getWritableDatabase();
|
||||
public void testShowDateTimePicker() {
|
||||
LoyaltyCardDateType dateType = LoyaltyCardDateType.VALID_FROM;
|
||||
ZonedDateTime selectedDateTime = ZonedDateTime.now();
|
||||
ZonedDateTime minDateTime = null;
|
||||
ZonedDateTime maxDateTime = null;
|
||||
|
||||
activity.showDateTimePicker(
|
||||
dateType, selectedDateTime, minDateTime, maxDateTime
|
||||
);
|
||||
|
||||
shadowOf(Looper.getMainLooper()).idle();
|
||||
|
||||
// Verify date picker is shown
|
||||
FragmentManager fm = activity.getSupportFragmentManager();
|
||||
MaterialDatePicker<?> datePicker = (MaterialDatePicker<?>) fm.findFragmentByTag("DATE_PICKER");
|
||||
|
||||
assertNotNull(datePicker);
|
||||
assertTrue(datePicker.isVisible());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTimePickerShowsAfterDateSelection() {
|
||||
// Given
|
||||
LoyaltyCardDateType dateType = LoyaltyCardDateType.VALID_FROM;
|
||||
long utcDateSelectionMillis = System.currentTimeMillis();
|
||||
ZonedDateTime initialDateTime = ZonedDateTime.now();
|
||||
|
||||
// When
|
||||
activity.showTimePicker(dateType, utcDateSelectionMillis, initialDateTime);
|
||||
|
||||
shadowOf(Looper.getMainLooper()).idle();
|
||||
|
||||
// Then - Verify time picker is shown
|
||||
FragmentManager fragmentManager = activity.getSupportFragmentManager();
|
||||
MaterialTimePicker timePicker = (MaterialTimePicker)
|
||||
fragmentManager.findFragmentByTag("TIME_PICKER");
|
||||
|
||||
assertNotNull(timePicker);
|
||||
assertTrue(timePicker.isVisible());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDatePickerPositiveButtonShowsTimePicker() {
|
||||
LoyaltyCardDateType dateType = LoyaltyCardDateType.VALID_FROM;
|
||||
ZonedDateTime selectedDateTime = ZonedDateTime.now();
|
||||
ZonedDateTime minDateTime = null;
|
||||
ZonedDateTime maxDateTime = null;
|
||||
|
||||
activity.showDateTimePicker(
|
||||
dateType, selectedDateTime, minDateTime, maxDateTime
|
||||
);
|
||||
|
||||
shadowOf(Looper.getMainLooper()).idle();
|
||||
|
||||
FragmentManager fm = activity.getSupportFragmentManager();
|
||||
MaterialDatePicker<?> datePicker = (MaterialDatePicker<?>) fm.findFragmentByTag("DATE_PICKER");
|
||||
Dialog dialog = datePicker.getDialog();
|
||||
assertNotNull(dialog);
|
||||
datePicker.getDialog().findViewById(com.google.android.material.R.id.confirm_button).performClick();
|
||||
|
||||
shadowOf(Looper.getMainLooper()).idle();
|
||||
|
||||
MaterialTimePicker timePicker = (MaterialTimePicker)
|
||||
fm.findFragmentByTag("TIME_PICKER");
|
||||
|
||||
assertNotNull(timePicker);
|
||||
assertTrue(timePicker.isVisible());
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void startWithLoyaltyCardNoExpirySetExpiry() {
|
||||
SQLiteDatabase database = TestHelpers.getEmptyDb(context).getWritableDatabase();
|
||||
long cardId = DBHelper.insertLoyaltyCard(database, "store", "note", null, null, new BigDecimal("0"), null, EAN_BARCODE_DATA, null, EAN_BARCODE_TYPE, Color.BLACK, 0, null, 0);
|
||||
|
||||
ActivityController activityController = createActivityWithLoyaltyCard(true, (int) cardId);
|
||||
Activity activity = (Activity) activityController.get();
|
||||
activityController = createActivityWithLoyaltyCard(true, (int) cardId);
|
||||
activity = activityController.get();
|
||||
|
||||
activityController.start();
|
||||
activityController.visible();
|
||||
@ -741,19 +831,38 @@ public class LoyaltyCardViewActivityTest {
|
||||
|
||||
checkAllFields(activity, ViewMode.UPDATE_CARD, "store", "note", context.getString(R.string.anyDate), context.getString(R.string.never), "0", context.getString(R.string.points), EAN_BARCODE_DATA, context.getString(R.string.sameAsCardId), EAN_BARCODE_TYPE.prettyName(), null, null);
|
||||
|
||||
ZonedDateTime selectedDateTime = ZonedDateTime.now();
|
||||
|
||||
// Set date to today
|
||||
MaterialAutoCompleteTextView expiryField = activity.findViewById(R.id.expiryField);
|
||||
expiryField.setText(expiryField.getAdapter().getItem(1).toString(), false);
|
||||
|
||||
shadowOf(getMainLooper()).idle();
|
||||
shadowOf(Looper.getMainLooper()).idle();
|
||||
|
||||
Dialog datePickerDialog = ShadowDialog.getLatestDialog();
|
||||
assertNotNull(datePickerDialog);
|
||||
datePickerDialog.findViewById(com.google.android.material.R.id.confirm_button).performClick();
|
||||
FragmentManager fm = activity.getSupportFragmentManager();
|
||||
MaterialDatePicker<?> datePicker = (MaterialDatePicker<?>) fm.findFragmentByTag("DATE_PICKER");
|
||||
Dialog dialog = datePicker.getDialog();
|
||||
assertNotNull(dialog);
|
||||
datePicker.getDialog().findViewById(com.google.android.material.R.id.confirm_button).performClick();
|
||||
|
||||
shadowOf(Looper.getMainLooper()).idle();
|
||||
|
||||
MaterialTimePicker timePicker = (MaterialTimePicker) fm.findFragmentByTag("TIME_PICKER");
|
||||
assertNotNull(timePicker);
|
||||
assertTrue(timePicker.isVisible());
|
||||
|
||||
timePicker.setHour(20);
|
||||
timePicker.setMinute(30);
|
||||
|
||||
timePicker.getDialog().findViewById(com.google.android.material.R.id.material_timepicker_ok_button).performClick();
|
||||
|
||||
shadowOf(getMainLooper()).idle();
|
||||
|
||||
checkAllFields(activity, ViewMode.UPDATE_CARD, "store", "note", context.getString(R.string.anyDate), DateFormat.getDateInstance(DateFormat.LONG).format(new Date()), "0", context.getString(R.string.points), EAN_BARCODE_DATA, context.getString(R.string.sameAsCardId), EAN_BARCODE_TYPE.prettyName(), null, null);
|
||||
LocalDate selectedDate = selectedDateTime.toLocalDate();
|
||||
LocalTime selectedTime = LocalTime.of(timePicker.getHour(), timePicker.getMinute());
|
||||
ZonedDateTime selectedDateAndTime = ZonedDateTime.of(selectedDate, selectedTime, ZoneId.systemDefault());
|
||||
|
||||
checkAllFields(activity, ViewMode.UPDATE_CARD, "store", "note", context.getString(R.string.anyDate), selectedDateAndTime.format(DateTimeUtils.longDateShortTimeFormatter), "0", context.getString(R.string.points), EAN_BARCODE_DATA, context.getString(R.string.sameAsCardId), EAN_BARCODE_TYPE.prettyName(), null, null);
|
||||
|
||||
database.close();
|
||||
}
|
||||
@ -763,7 +872,7 @@ public class LoyaltyCardViewActivityTest {
|
||||
final Context context = ApplicationProvider.getApplicationContext();
|
||||
SQLiteDatabase database = TestHelpers.getEmptyDb(context).getWritableDatabase();
|
||||
|
||||
long cardId = DBHelper.insertLoyaltyCard(database, "store", "note", null, new Date(), new BigDecimal("0"), null, EAN_BARCODE_DATA, null, EAN_BARCODE_TYPE, Color.BLACK, 0, null, 0);
|
||||
long cardId = DBHelper.insertLoyaltyCard(database, "store", "note", null, Instant.now(), new BigDecimal("0"), null, EAN_BARCODE_DATA, null, EAN_BARCODE_TYPE, Color.BLACK, 0, null, 0);
|
||||
|
||||
ActivityController activityController = createActivityWithLoyaltyCard(true, (int) cardId);
|
||||
Activity activity = (Activity) activityController.get();
|
||||
@ -772,7 +881,7 @@ public class LoyaltyCardViewActivityTest {
|
||||
activityController.visible();
|
||||
activityController.resume();
|
||||
|
||||
checkAllFields(activity, ViewMode.UPDATE_CARD, "store", "note", context.getString(R.string.anyDate), DateFormat.getDateInstance(DateFormat.LONG).format(new Date()), "0", context.getString(R.string.points), EAN_BARCODE_DATA, context.getString(R.string.sameAsCardId), EAN_BARCODE_TYPE.prettyName(), null, null);
|
||||
checkAllFields(activity, ViewMode.UPDATE_CARD, "store", "note", context.getString(R.string.anyDate), DateTimeUtils.formatLong(Instant.now()), "0", context.getString(R.string.points), EAN_BARCODE_DATA, context.getString(R.string.sameAsCardId), EAN_BARCODE_TYPE.prettyName(), null, null);
|
||||
|
||||
// Set date to never
|
||||
MaterialAutoCompleteTextView expiryField = activity.findViewById(R.id.expiryField);
|
||||
@ -826,7 +935,7 @@ public class LoyaltyCardViewActivityTest {
|
||||
|
||||
shadowOf(getMainLooper()).idle();
|
||||
|
||||
checkAllFields(activity, ViewMode.UPDATE_CARD, "store", "note", DateFormat.getDateInstance(DateFormat.LONG).format(new Date()), DateFormat.getDateInstance(DateFormat.LONG).format(new Date()), "10.00", "€", EAN_BARCODE_DATA, null, EAN_BARCODE_TYPE.toString(), null, null);
|
||||
checkAllFields(activity, ViewMode.UPDATE_CARD, "store", "note", DateTimeUtils.formatLong(Instant.now()), DateTimeUtils.formatLong(Instant.now()), "10.00", "€", EAN_BARCODE_DATA, null, EAN_BARCODE_TYPE.toString(), null, null);
|
||||
|
||||
database.close();
|
||||
}
|
||||
@ -1348,9 +1457,9 @@ public class LoyaltyCardViewActivityTest {
|
||||
|
||||
@Test
|
||||
public void importCard() {
|
||||
Date date = new Date();
|
||||
Instant date = Instant.now();
|
||||
|
||||
Uri importUri = Uri.parse("https://catima.app/share#store%3DExample%2BStore%26note%3D%26validfrom%3D" + date.getTime() + "%26expiry%3D" + date.getTime() + "%26balance%3D10.00%26balancetype%3DUSD%26cardid%3D123456%26barcodetype%3DAZTEC%26headercolor%3D-416706");
|
||||
Uri importUri = Uri.parse("https://catima.app/share#store%3DExample%2BStore%26note%3D%26validfrom%3D" + date.toString() + "%26expiry%3D" + date + "%26balance%3D10.00%26balancetype%3DUSD%26cardid%3D123456%26barcodetype%3DAZTEC%26headercolor%3D-416706");
|
||||
|
||||
Intent intent = new Intent();
|
||||
intent.setData(importUri);
|
||||
@ -1366,7 +1475,7 @@ public class LoyaltyCardViewActivityTest {
|
||||
|
||||
shadowOf(getMainLooper()).idle();
|
||||
|
||||
checkAllFields(activity, ViewMode.ADD_CARD, "Example Store", "", DateFormat.getDateInstance(DateFormat.LONG).format(date), DateFormat.getDateInstance(DateFormat.LONG).format(date), "10.00", "$", "123456", context.getString(R.string.sameAsCardId), "Aztec", null, null);
|
||||
checkAllFields(activity, ViewMode.ADD_CARD, "Example Store", "", DateTimeUtils.formatLong(date), DateTimeUtils.formatLong(date), "10.00", "$", "123456", context.getString(R.string.sameAsCardId), "Aztec", null, null);
|
||||
assertEquals(-416706, ((ColorDrawable) activity.findViewById(R.id.thumbnail).getBackground()).getColor());
|
||||
}
|
||||
|
||||
|
||||
@ -14,7 +14,7 @@ import org.robolectric.RobolectricTestRunner
|
||||
import org.robolectric.shadows.ShadowContentResolver
|
||||
import org.robolectric.shadows.ShadowLog
|
||||
import java.math.BigDecimal
|
||||
import java.util.Date
|
||||
import java.time.Instant
|
||||
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
class PkpassTest {
|
||||
@ -94,7 +94,7 @@ class PkpassTest {
|
||||
"0180 6 320 320 ( 0:00 Uhr - 24:00 Uhr )\n" +
|
||||
"\n" +
|
||||
"(0,20 € pro Anruf aus dem Festnetz der Deutschen Telekom - Mobilfunk maximal 0,60 € pro Anruf).", parsedCard.note)
|
||||
Assert.assertEquals(Date(1567911600000), parsedCard.validFrom)
|
||||
Assert.assertEquals(Instant.ofEpochMilli(1567911600000), parsedCard.validFrom)
|
||||
Assert.assertEquals(null, parsedCard.expiry)
|
||||
Assert.assertEquals(BigDecimal(0), parsedCard.balance)
|
||||
Assert.assertEquals(null, parsedCard.balanceType)
|
||||
@ -160,7 +160,7 @@ class PkpassTest {
|
||||
"\n" +
|
||||
"Eurowings shall not be liable for any items which passengers are prohibited from carrying in their hand baggage for security reasons and are required to surrender at the security checkpoint.\n" +
|
||||
"Contact: https://mobile.eurowings.com/booking/StaticContactInfo.aspx?culture=en-GB&back=home", parsedCard.note)
|
||||
Assert.assertEquals(Date(1567911600000), parsedCard.validFrom)
|
||||
Assert.assertEquals(Instant.ofEpochMilli(1567911600000), parsedCard.validFrom)
|
||||
Assert.assertEquals(null, parsedCard.expiry)
|
||||
Assert.assertEquals(BigDecimal(0), parsedCard.balance)
|
||||
Assert.assertEquals(null, parsedCard.balanceType)
|
||||
@ -257,7 +257,7 @@ class PkpassTest {
|
||||
"0180 6 320 320 ( 0:00 Uhr - 24:00 Uhr )\n" +
|
||||
"\n" +
|
||||
"(0,20 € pro Anruf aus dem Festnetz der Deutschen Telekom - Mobilfunk maximal 0,60 € pro Anruf).", parsedCard.note)
|
||||
Assert.assertEquals(Date(1567911600000), parsedCard.validFrom)
|
||||
Assert.assertEquals(Instant.ofEpochMilli(1567911600000), parsedCard.validFrom)
|
||||
Assert.assertEquals(null, parsedCard.expiry)
|
||||
Assert.assertEquals(BigDecimal(0), parsedCard.balance)
|
||||
Assert.assertEquals(null, parsedCard.balanceType)
|
||||
@ -323,7 +323,7 @@ class PkpassTest {
|
||||
"\n" +
|
||||
"Eurowings shall not be liable for any items which passengers are prohibited from carrying in their hand baggage for security reasons and are required to surrender at the security checkpoint.\n" +
|
||||
"Contact: https://mobile.eurowings.com/booking/StaticContactInfo.aspx?culture=en-GB&back=home", parsedCard.note)
|
||||
Assert.assertEquals(Date(1567911600000), parsedCard.validFrom)
|
||||
Assert.assertEquals(Instant.ofEpochMilli(1567911600000), parsedCard.validFrom)
|
||||
Assert.assertEquals(null, parsedCard.expiry)
|
||||
Assert.assertEquals(BigDecimal(0), parsedCard.balance)
|
||||
Assert.assertEquals(null, parsedCard.balanceType)
|
||||
@ -366,7 +366,7 @@ class PkpassTest {
|
||||
Assert.assertEquals(-1, parsedCard.id)
|
||||
Assert.assertEquals("EUROWINGS", parsedCard.store)
|
||||
Assert.assertEquals("Eurowings Boarding Pass", parsedCard.note)
|
||||
Assert.assertEquals(Date(1567911600000), parsedCard.validFrom)
|
||||
Assert.assertEquals(Instant.ofEpochMilli(1567911600000), parsedCard.validFrom)
|
||||
Assert.assertEquals(null, parsedCard.expiry)
|
||||
Assert.assertEquals(BigDecimal(0), parsedCard.balance)
|
||||
Assert.assertEquals(null, parsedCard.balanceType)
|
||||
@ -388,7 +388,7 @@ class PkpassTest {
|
||||
Assert.assertEquals(-1, parsedCard.id)
|
||||
Assert.assertEquals("EUROWINGS", parsedCard.store)
|
||||
Assert.assertEquals("Eurowings Boarding Pass", parsedCard.note)
|
||||
Assert.assertEquals(Date(1567911600000), parsedCard.validFrom)
|
||||
Assert.assertEquals(Instant.ofEpochMilli(1567911600000), parsedCard.validFrom)
|
||||
Assert.assertEquals(null, parsedCard.expiry)
|
||||
Assert.assertEquals(BigDecimal(0), parsedCard.balance)
|
||||
Assert.assertEquals(null, parsedCard.balanceType)
|
||||
@ -478,7 +478,7 @@ class PkpassTest {
|
||||
"0180 6 320 320 ( 0:00 Uhr - 24:00 Uhr )\n" +
|
||||
"\n" +
|
||||
"(0,20 € pro Anruf aus dem Festnetz der Deutschen Telekom - Mobilfunk maximal 0,60 € pro Anruf).", parsedCard.note)
|
||||
Assert.assertEquals(Date(1567911600000), parsedCard.validFrom)
|
||||
Assert.assertEquals(Instant.ofEpochMilli(1567911600000), parsedCard.validFrom)
|
||||
Assert.assertEquals(null, parsedCard.expiry)
|
||||
Assert.assertEquals(BigDecimal(0), parsedCard.balance)
|
||||
Assert.assertEquals(null, parsedCard.balanceType)
|
||||
@ -544,7 +544,7 @@ class PkpassTest {
|
||||
"\n" +
|
||||
"Eurowings shall not be liable for any items which passengers are prohibited from carrying in their hand baggage for security reasons and are required to surrender at the security checkpoint.\n" +
|
||||
"Contact: https://mobile.eurowings.com/booking/StaticContactInfo.aspx?culture=en-GB&back=home", parsedCard.note)
|
||||
Assert.assertEquals(Date(1567911600000), parsedCard.validFrom)
|
||||
Assert.assertEquals(Instant.ofEpochMilli(1567911600000), parsedCard.validFrom)
|
||||
Assert.assertEquals(null, parsedCard.expiry)
|
||||
Assert.assertEquals(BigDecimal(0), parsedCard.balance)
|
||||
Assert.assertEquals(null, parsedCard.balanceType)
|
||||
|
||||
@ -21,20 +21,23 @@ public class TestHelpers {
|
||||
SQLiteDatabase database = db.getWritableDatabase();
|
||||
|
||||
// Make sure no files remain
|
||||
Cursor cursor = DBHelper.getLoyaltyCardCursor(database);
|
||||
cursor.moveToFirst();
|
||||
while (!cursor.isAfterLast()) {
|
||||
int cardID = cursor.getColumnIndex(DBHelper.LoyaltyCardDbIds.ID);
|
||||
// Use a try-with-resources block to automatically close the cursor
|
||||
try (Cursor cursor = DBHelper.getLoyaltyCardCursor(database)) {
|
||||
// A simpler loop pattern is while(cursor.moveToNext())
|
||||
while (cursor.moveToNext()) {
|
||||
// Note: getColumnIndex is expensive, it's better to get it once before the loop
|
||||
int cardIDColumnIndex = cursor.getColumnIndex(DBHelper.LoyaltyCardDbIds.ID);
|
||||
int cardID = cursor.getInt(cardIDColumnIndex);
|
||||
|
||||
for (ImageLocationType imageLocationType : ImageLocationType.values()) {
|
||||
try {
|
||||
Utils.saveCardImage(context.getApplicationContext(), null, cardID, imageLocationType);
|
||||
} catch (FileNotFoundException ignored) {
|
||||
for (ImageLocationType imageLocationType : ImageLocationType.values()) {
|
||||
try {
|
||||
// It's generally better to pass the application context
|
||||
Utils.saveCardImage(context.getApplicationContext(), null, cardID, imageLocationType);
|
||||
} catch (FileNotFoundException ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cursor.moveToNext();
|
||||
}
|
||||
} // cursor.close() is automatically called here, even if an exception occurs!
|
||||
|
||||
// Make sure DB is empty
|
||||
database.execSQL("delete from " + DBHelper.LoyaltyCardDbIds.TABLE);
|
||||
@ -55,7 +58,7 @@ public class TestHelpers {
|
||||
for (int index = cardsToAdd; index > 0; index--) {
|
||||
String storeName = String.format("store, \"%4d", index);
|
||||
String note = String.format("note, \"%4d", index);
|
||||
long id = DBHelper.insertLoyaltyCard(mDatabase, storeName, note, null, null, new BigDecimal(String.valueOf(index)), null, BARCODE_DATA, null, BARCODE_TYPE, index, 0, null,0);
|
||||
long id = DBHelper.insertLoyaltyCard(mDatabase, storeName, note, null, null, new BigDecimal(String.valueOf(index)), null, BARCODE_DATA, null, BARCODE_TYPE, index, 0, null, 0);
|
||||
boolean result = (id != -1);
|
||||
assertTrue(result);
|
||||
}
|
||||
|
||||
@ -24,7 +24,6 @@ import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Currency;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
@ -87,8 +86,8 @@ public class CardsContentProviderTest {
|
||||
|
||||
final String store = "the best store";
|
||||
final String note = "this is a note";
|
||||
final Date validFrom = Date.from(Instant.ofEpochMilli(1687112209000L));
|
||||
final Date expiry = Date.from(Instant.ofEpochMilli(1687112277000L));
|
||||
final Instant validFrom = Instant.ofEpochMilli(1687112209000L);
|
||||
final Instant expiry = Instant.ofEpochMilli(1687112277000L);
|
||||
final BigDecimal balance = new BigDecimal("123.20");
|
||||
final Currency balanceType = Currency.getInstance("EUR");
|
||||
final String cardId = "a-card-id";
|
||||
@ -126,8 +125,8 @@ public class CardsContentProviderTest {
|
||||
final int actualId = cursor.getInt(cursor.getColumnIndexOrThrow("_id"));
|
||||
final String actualName = cursor.getString(cursor.getColumnIndexOrThrow("store"));
|
||||
final String actualNote = cursor.getString(cursor.getColumnIndexOrThrow("note"));
|
||||
final long actualValidFrom = cursor.getLong(cursor.getColumnIndexOrThrow("validfrom"));
|
||||
final long actualExpiry = cursor.getLong(cursor.getColumnIndexOrThrow("expiry"));
|
||||
final String actualValidFrom = cursor.getString(cursor.getColumnIndexOrThrow("validfrom"));
|
||||
final String actualExpiry = cursor.getString(cursor.getColumnIndexOrThrow("expiry"));
|
||||
final BigDecimal actualBalance = new BigDecimal(cursor.getString(cursor.getColumnIndexOrThrow("balance")));
|
||||
final String actualBalanceType = cursor.getString(cursor.getColumnIndexOrThrow("balancetype"));
|
||||
final String actualCardId = cursor.getString(cursor.getColumnIndexOrThrow("cardid"));
|
||||
@ -141,8 +140,8 @@ public class CardsContentProviderTest {
|
||||
assertEquals("Id", 1, actualId);
|
||||
assertEquals("Name", store, actualName);
|
||||
assertEquals("Note", note, actualNote);
|
||||
assertEquals("ValidFrom", validFrom.getTime(), actualValidFrom);
|
||||
assertEquals("Expiry", expiry.getTime(), actualExpiry);
|
||||
assertEquals("ValidFrom", validFrom.toString(), actualValidFrom);
|
||||
assertEquals("Expiry", expiry.toString(), actualExpiry);
|
||||
assertEquals("Balance", balance, actualBalance);
|
||||
assertEquals("BalanceTypeColumn", balanceType.toString(), actualBalanceType);
|
||||
assertEquals("CardId", cardId, actualCardId);
|
||||
|
||||
@ -6,9 +6,9 @@ Food
|
||||
Fashion
|
||||
|
||||
_id,store,note,validfrom,expiry,balance,balancetype,cardid,barcodeid,headercolor,barcodetype,starstatus
|
||||
1,Card 1,Note 1,1601510400,1618053234,100,USD,1234,5432,1,QR_CODE,0,
|
||||
1,Card 1,Note 1,2020-10-01T00:00:00Z,2021-04-10T11:13:54Z,100,USD,1234,5432,1,QR_CODE,0,
|
||||
8,Clothes Store,Note about store,,,0,,a,,-5317,,0,
|
||||
2,Department Store,,,1618041729,0,,A,,-9977996,,0,
|
||||
2,Department Store,,,2021-04-10T08:02:09Z,0,,A,,-9977996,,0,
|
||||
3,Grocery Store,"Multiline note about grocery store
|
||||
|
||||
with blank line",,,150,,dhd,,-9977996,,0,
|
||||
|
||||
|
Can't render this file because it has a wrong number of fields in line 8.
|
Loading…
Reference in New Issue
Block a user