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:
hritikRitss 2025-09-30 22:46:00 +05:30
parent 5f8c8048e6
commit 21c8d4c962
22 changed files with 971 additions and 341 deletions

View File

@ -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);

View 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);
}
}

View File

@ -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) {

View File

@ -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

View File

@ -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);
}

View File

@ -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);
}
}
}
);

View File

@ -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)) {

View File

@ -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 {

View File

@ -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);

View File

@ -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,

View File

@ -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

View File

@ -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);

View File

@ -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");

View File

@ -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
}

View 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());
}
}

View File

@ -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);

View File

@ -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);

View File

@ -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());
}

View File

@ -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)

View File

@ -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);
}

View File

@ -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);

View File

@ -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.