From a95f5ce84f770a0da8645d6fd03ba75ce35a96ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=BA=C5=A1=20Petro?= Date: Tue, 7 Oct 2025 16:23:21 +0200 Subject: [PATCH 1/7] Fixed img comparison --- .../java/protect/card_locker/LoyaltyCard.java | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/app/src/main/java/protect/card_locker/LoyaltyCard.java b/app/src/main/java/protect/card_locker/LoyaltyCard.java index b4fe334983..08a5c00e4d 100644 --- a/app/src/main/java/protect/card_locker/LoyaltyCard.java +++ b/app/src/main/java/protect/card_locker/LoyaltyCard.java @@ -595,23 +595,7 @@ public static boolean isDuplicate(Context context, final LoyaltyCard a, final Lo b.barcodeType == null ? null : b.barcodeType.format()) && // nullable CatimaBarcode with no overridden .equals(), so we need to check .format() Utils.equals(a.headerColor, b.headerColor) && // nullable Integer a.starStatus == b.starStatus && // non-nullable int - a.archiveStatus == b.archiveStatus && // non-nullable int - nullableBitmapsEqual(a.getImageThumbnail(context), b.getImageThumbnail(context)) && // nullable Bitmap - nullableBitmapsEqual(a.getImageFront(context), b.getImageFront(context)) && // nullable Bitmap - nullableBitmapsEqual(a.getImageBack(context), b.getImageBack(context)); // nullable Bitmap - } - - public static boolean nullableBitmapsEqual(@Nullable Bitmap a, @Nullable Bitmap b) { - if (a == null && b == null) { - return true; - } - - if (a != null && b != null) { - return a.sameAs(b); - } - - // One is null and the other isn't, so it's not equal - return false; + a.archiveStatus == b.archiveStatus; // non-nullable int } @NonNull From 4bf086298567b19321dcdea8fbda08c36d28cb2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=BA=C5=A1=20Petro?= Date: Tue, 7 Oct 2025 17:15:54 +0200 Subject: [PATCH 2/7] Fixed issue: duplicity after importing same zip again --- .../java/protect/card_locker/DBHelper.java | 24 ++++++++++++++++ .../java/protect/card_locker/LoyaltyCard.java | 3 +- .../importexport/CatimaImporter.java | 28 ++++++++++++------- 3 files changed, 43 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/protect/card_locker/DBHelper.java b/app/src/main/java/protect/card_locker/DBHelper.java index 88ad9167bb..07b10c211b 100644 --- a/app/src/main/java/protect/card_locker/DBHelper.java +++ b/app/src/main/java/protect/card_locker/DBHelper.java @@ -561,6 +561,30 @@ public static LoyaltyCard getLoyaltyCard(Context context, SQLiteDatabase databas return card; } + public static List getLoyaltyCardsByCardId(Context context, SQLiteDatabase database, final String cardId) { + List cards = new ArrayList<>(); + + Cursor data = database.query( + LoyaltyCardDbIds.TABLE, + null, + whereAttrs(LoyaltyCardDbIds.CARD_ID), + withArgs(cardId), + null, + null, + null + ); + + if (data.moveToFirst()) { + do { + LoyaltyCard card = LoyaltyCard.fromCursor(context, data); + cards.add(card); + } while (data.moveToNext()); + } + + data.close(); + return cards; + } + public static List getLoyaltyCardGroups(SQLiteDatabase database, final int id) { Cursor data = database.rawQuery("select * from " + LoyaltyCardDbGroups.TABLE + " g " + " LEFT JOIN " + LoyaltyCardDbIdsGroups.TABLE + " ig ON ig." + LoyaltyCardDbIdsGroups.groupID + " = g." + LoyaltyCardDbGroups.ID + diff --git a/app/src/main/java/protect/card_locker/LoyaltyCard.java b/app/src/main/java/protect/card_locker/LoyaltyCard.java index 08a5c00e4d..674e79d45e 100644 --- a/app/src/main/java/protect/card_locker/LoyaltyCard.java +++ b/app/src/main/java/protect/card_locker/LoyaltyCard.java @@ -582,8 +582,7 @@ public static LoyaltyCard fromCursor(Context context, Cursor cursor) { public static boolean isDuplicate(Context context, final LoyaltyCard a, final LoyaltyCard b) { // Note: Bitmap comparing is slow, be careful when calling this method // Skip lastUsed & zoomLevel* - return a.id == b.id && // non-nullable int - a.store.equals(b.store) && // non-nullable String + return a.store.equals(b.store) && // non-nullable String a.note.equals(b.note) && // non-nullable String Utils.equals(a.validFrom, b.validFrom) && // nullable Date Utils.equals(a.expiry, b.expiry) && // nullable Date diff --git a/app/src/main/java/protect/card_locker/importexport/CatimaImporter.java b/app/src/main/java/protect/card_locker/importexport/CatimaImporter.java index 5cc348895c..38591f6556 100644 --- a/app/src/main/java/protect/card_locker/importexport/CatimaImporter.java +++ b/app/src/main/java/protect/card_locker/importexport/CatimaImporter.java @@ -124,11 +124,17 @@ public Map saveAndDeduplicate(Context context, SQLiteDatabase Set existingImages = DBHelper.imageFiles(context, database); for (LoyaltyCard card : data.cards) { - LoyaltyCard existing = DBHelper.getLoyaltyCard(context, database, card.id); - if (existing == null) { - DBHelper.insertLoyaltyCard(database, card.id, card.store, card.note, card.validFrom, card.expiry, card.balance, card.balanceType, - card.cardId, card.barcodeId, card.barcodeType, card.headerColor, card.starStatus, card.lastUsed, card.archiveStatus); - } else if (!isDuplicate(context, existing, card, existingImages, imageChecksums)) { + List candidates = DBHelper.getLoyaltyCardsByCardId(context, database, card.cardId); + boolean duplicateFound = false; + + for (LoyaltyCard existing : candidates) { + if (isDuplicate(context, existing, card, existingImages, imageChecksums)) { + duplicateFound = true; + break; + } + } + + if (!duplicateFound) { long newId = DBHelper.insertLoyaltyCard(database, card.store, card.note, card.validFrom, card.expiry, card.balance, card.balanceType, card.cardId, card.barcodeId, card.barcodeType, card.headerColor, card.starStatus, card.lastUsed, card.archiveStatus); idMap.put(card.id, (int) newId); @@ -156,14 +162,16 @@ public boolean isDuplicate(Context context, final LoyaltyCard existing, final Lo return false; } for (ImageLocationType imageLocationType : ImageLocationType.values()) { - String name = Utils.getCardImageFileName(existing.id, imageLocationType); - boolean exists = existingImages.contains(name); - if (exists != imageChecksums.containsKey(name)) { + String nameExists = Utils.getCardImageFileName(existing.id, imageLocationType); + String nameChecksum = nameExists.replaceFirst("card_\\d+_", "card_" + card.id + "_"); + + boolean exists = existingImages.contains(nameExists); + if (exists != imageChecksums.containsKey(nameChecksum)) { return false; } if (exists) { - File file = Utils.retrieveCardImageAsFile(context, name); - if (!imageChecksums.get(name).equals(Utils.checksum(new FileInputStream(file)))) { + File file = Utils.retrieveCardImageAsFile(context, nameExists); + if (!imageChecksums.get(nameChecksum).equals(Utils.checksum(new FileInputStream(file)))) { return false; } } From 61e0ca422fea15ed79567a0de1b0824c2c2cebbe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=BA=C5=A1=20Petro?= Date: Wed, 15 Oct 2025 22:54:26 +0200 Subject: [PATCH 3/7] fix rewriting imgs --- .../card_locker/importexport/CatimaImporter.java | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/protect/card_locker/importexport/CatimaImporter.java b/app/src/main/java/protect/card_locker/importexport/CatimaImporter.java index 38591f6556..813606a108 100644 --- a/app/src/main/java/protect/card_locker/importexport/CatimaImporter.java +++ b/app/src/main/java/protect/card_locker/importexport/CatimaImporter.java @@ -99,7 +99,9 @@ public void importData(Context context, SQLiteDatabase database, File inputFile, throw new FormatException("No imported data"); } - Map idMap = saveAndDeduplicate(context, database, importedData, imageChecksums); + List duplicatedCardsImages = new ArrayList<>(); + + Map idMap = saveAndDeduplicate(context, database, importedData, imageChecksums, duplicatedCardsImages); if (isZipFile) { // Pass #2: save images @@ -109,7 +111,7 @@ public void importData(Context context, SQLiteDatabase database, File inputFile, while ((localFileHeader = zipInputStream2.getNextEntry()) != null) { String fileName = Uri.parse(localFileHeader.getFileName()).getLastPathSegment(); - if (fileName.endsWith(".png")) { + if (fileName.endsWith(".png") && !duplicatedCardsImages.contains(fileName)) { String newFileName = Utils.getRenamedCardImageFileName(fileName, idMap); Utils.saveCardImage(context, ZipUtils.readImage(zipInputStream2), newFileName); } @@ -119,7 +121,7 @@ public void importData(Context context, SQLiteDatabase database, File inputFile, } } - public Map saveAndDeduplicate(Context context, SQLiteDatabase database, final ImportedData data, final Map imageChecksums) throws IOException { + public Map saveAndDeduplicate(Context context, SQLiteDatabase database, final ImportedData data, final Map imageChecksums, List duplicatedCardsImages) throws IOException { Map idMap = new HashMap<>(); Set existingImages = DBHelper.imageFiles(context, database); @@ -128,7 +130,7 @@ public Map saveAndDeduplicate(Context context, SQLiteDatabase boolean duplicateFound = false; for (LoyaltyCard existing : candidates) { - if (isDuplicate(context, existing, card, existingImages, imageChecksums)) { + if (isDuplicate(context, existing, card, existingImages, imageChecksums, duplicatedCardsImages)) { duplicateFound = true; break; } @@ -157,13 +159,14 @@ public Map saveAndDeduplicate(Context context, SQLiteDatabase return idMap; } - public boolean isDuplicate(Context context, final LoyaltyCard existing, final LoyaltyCard card, final Set existingImages, final Map imageChecksums) throws IOException { + public boolean isDuplicate(Context context, final LoyaltyCard existing, final LoyaltyCard card, final Set existingImages, final Map imageChecksums, List duplicatedCardsImages) throws IOException { if (!LoyaltyCard.isDuplicate(context, existing, card)) { return false; } + String nameChecksum = null; for (ImageLocationType imageLocationType : ImageLocationType.values()) { String nameExists = Utils.getCardImageFileName(existing.id, imageLocationType); - String nameChecksum = nameExists.replaceFirst("card_\\d+_", "card_" + card.id + "_"); + nameChecksum = Utils.getCardImageFileName(card.id, imageLocationType); boolean exists = existingImages.contains(nameExists); if (exists != imageChecksums.containsKey(nameChecksum)) { @@ -176,6 +179,7 @@ public boolean isDuplicate(Context context, final LoyaltyCard existing, final Lo } } } + duplicatedCardsImages.add(nameChecksum); return true; } From f2b2f3c15cdf4174643726c1500bf905428bbaf9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=BA=C5=A1=20Petro?= Date: Sun, 19 Oct 2025 23:34:38 +0200 Subject: [PATCH 4/7] replace imageChecksums with bitmapComparing --- .../java/protect/card_locker/LoyaltyCard.java | 18 +- .../main/java/protect/card_locker/Utils.java | 50 ------ .../importexport/CatimaImporter.java | 167 ++++++++---------- 3 files changed, 91 insertions(+), 144 deletions(-) diff --git a/app/src/main/java/protect/card_locker/LoyaltyCard.java b/app/src/main/java/protect/card_locker/LoyaltyCard.java index 674e79d45e..05e282eaa7 100644 --- a/app/src/main/java/protect/card_locker/LoyaltyCard.java +++ b/app/src/main/java/protect/card_locker/LoyaltyCard.java @@ -594,7 +594,23 @@ public static boolean isDuplicate(Context context, final LoyaltyCard a, final Lo b.barcodeType == null ? null : b.barcodeType.format()) && // nullable CatimaBarcode with no overridden .equals(), so we need to check .format() Utils.equals(a.headerColor, b.headerColor) && // nullable Integer a.starStatus == b.starStatus && // non-nullable int - a.archiveStatus == b.archiveStatus; // non-nullable int + a.archiveStatus == b.archiveStatus && // non-nullable int + nullableBitmapsEqual(a.getImageThumbnail(context), b.getImageThumbnail(context)) && // nullable Bitmap + nullableBitmapsEqual(a.getImageFront(context), b.getImageFront(context)) && // nullable Bitmap + nullableBitmapsEqual(a.getImageBack(context), b.getImageBack(context)); // nullable Bitmap + } + + public static boolean nullableBitmapsEqual(@Nullable Bitmap a, @Nullable Bitmap b) { + if (a == null && b == null) { + return true; + } + + if (a != null && b != null) { + return a.sameAs(b); + } + + return false; + // One is null and the other isn't, so it's not equal } @NonNull diff --git a/app/src/main/java/protect/card_locker/Utils.java b/app/src/main/java/protect/card_locker/Utils.java index 2703dca10b..b58db81659 100644 --- a/app/src/main/java/protect/card_locker/Utils.java +++ b/app/src/main/java/protect/card_locker/Utils.java @@ -74,8 +74,6 @@ import java.io.InputStreamReader; import java.math.BigDecimal; import java.nio.charset.StandardCharsets; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; import java.text.DecimalFormatSymbols; import java.text.NumberFormat; import java.text.DecimalFormat; @@ -91,8 +89,6 @@ import java.util.Locale; import java.util.Map; import java.util.Objects; -import java.util.regex.Matcher; -import java.util.regex.Pattern; import protect.card_locker.preferences.Settings; @@ -100,7 +96,6 @@ public class Utils { private static final String TAG = "Catima"; // Activity request codes - public static final int MAIN_REQUEST = 1; public static final int SELECT_BARCODE_REQUEST = 2; public static final int BARCODE_SCAN = 3; public static final int BARCODE_IMPORT_FROM_IMAGE_FILE = 4; @@ -113,8 +108,6 @@ public class Utils { public static final int CARD_IMAGE_FROM_FILE_BACK = 11; public static final int CARD_IMAGE_FROM_FILE_ICON = 12; - public static final String CARD_IMAGE_FILENAME_REGEX = "^(card_)(\\d+)(_(?:front|back|icon)\\.png)$"; - static final double LUMINANCE_MIDPOINT = 0.5; static final int BITMAP_SIZE_SMALL = 512; @@ -724,31 +717,6 @@ static public String getCardImageFileName(int loyaltyCardId, ImageLocationType t return cardImageFileNameBuilder.toString(); } - /** - * Returns a card image filename (string) with the ID replaced according to the map if the input is a valid card image filename (string), otherwise null. - * - * @param fileName e.g. "card_1_front.png" - * @param idMap e.g. Map.of(1, 2) - * @return String e.g. "card_2_front.png" - */ - static public String getRenamedCardImageFileName(final String fileName, final Map idMap) { - Pattern pattern = Pattern.compile(CARD_IMAGE_FILENAME_REGEX); - Matcher matcher = pattern.matcher(fileName); - if (matcher.matches()) { - StringBuilder cardImageFileNameBuilder = new StringBuilder(); - cardImageFileNameBuilder.append(matcher.group(1)); - try { - int id = Integer.parseInt(matcher.group(2)); - cardImageFileNameBuilder.append(idMap.getOrDefault(id, id)); - } catch (NumberFormatException _e) { - return null; - } - cardImageFileNameBuilder.append(matcher.group(3)); - return cardImageFileNameBuilder.toString(); - } - return null; - } - static public void saveCardImage(Context context, Bitmap bitmap, String fileName) throws FileNotFoundException { if (bitmap == null) { context.deleteFile(fileName); @@ -1132,24 +1100,6 @@ public static int getHeaderColor(Context context, LoyaltyCard loyaltyCard) { return loyaltyCard.headerColor != null ? loyaltyCard.headerColor : LetterBitmap.getDefaultColor(context, loyaltyCard.store); } - public static String checksum(InputStream input) throws IOException { - try { - MessageDigest md = MessageDigest.getInstance("SHA-1"); - byte[] buf = new byte[4096]; - int len; - while ((len = input.read(buf)) != -1) { - md.update(buf, 0, len); - } - StringBuilder sb = new StringBuilder(); - for (byte b : md.digest()) { - sb.append(String.format("%02x", b)); - } - return sb.toString(); - } catch (NoSuchAlgorithmException _e) { - return null; - } - } - public static boolean equals(final Object a, final Object b) { if (a == null && b == null) { return true; diff --git a/app/src/main/java/protect/card_locker/importexport/CatimaImporter.java b/app/src/main/java/protect/card_locker/importexport/CatimaImporter.java index 813606a108..9bac9b6cab 100644 --- a/app/src/main/java/protect/card_locker/importexport/CatimaImporter.java +++ b/app/src/main/java/protect/card_locker/importexport/CatimaImporter.java @@ -2,10 +2,11 @@ import android.content.Context; import android.database.sqlite.SQLiteDatabase; -import android.net.Uri; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; -import net.lingala.zip4j.io.inputstream.ZipInputStream; -import net.lingala.zip4j.model.LocalFileHeader; +import net.lingala.zip4j.ZipFile; +import net.lingala.zip4j.model.FileHeader; import org.apache.commons.csv.CSVFormat; import org.apache.commons.csv.CSVParser; @@ -27,7 +28,6 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Set; import protect.card_locker.CatimaBarcode; import protect.card_locker.DBHelper; @@ -36,7 +36,6 @@ import protect.card_locker.ImageLocationType; import protect.card_locker.LoyaltyCard; import protect.card_locker.Utils; -import protect.card_locker.ZipUtils; /** * Class for importing a database from CSV (Comma Separate Values) @@ -59,78 +58,35 @@ public static class ImportedData { } public void importData(Context context, SQLiteDatabase database, File inputFile, char[] password) throws IOException, FormatException, InterruptedException { - // Pass #1: get hashes and parse CSV - InputStream input1 = new FileInputStream(inputFile); - InputStream bufferedInputStream1 = new BufferedInputStream(input1); - bufferedInputStream1.mark(100); - ZipInputStream zipInputStream1 = new ZipInputStream(bufferedInputStream1, password); - - // First, check if this is a zip file - boolean isZipFile = false; - LocalFileHeader localFileHeader; - Map imageChecksums = new HashMap<>(); ImportedData importedData = null; + ZipFile zipFile = new ZipFile(inputFile, password); - while ((localFileHeader = zipInputStream1.getNextEntry()) != null) { - isZipFile = true; - - String fileName = Uri.parse(localFileHeader.getFileName()).getLastPathSegment(); - if (fileName.equals("catima.csv")) { - importedData = importCSV(zipInputStream1); - } else if (fileName.endsWith(".png")) { - if (!fileName.matches(Utils.CARD_IMAGE_FILENAME_REGEX)) { - throw new FormatException("Unexpected PNG file in import: " + fileName); - } - imageChecksums.put(fileName, Utils.checksum(zipInputStream1)); - } else { - throw new FormatException("Unexpected file in import: " + fileName); - } - } - - if (!isZipFile) { + if (zipFile.isValidZipFile()) { + importedData = importZIP(zipFile); + }else { // This is not a zip file, try importing as bare CSV - bufferedInputStream1.reset(); - importedData = importCSV(bufferedInputStream1); + InputStream input = new FileInputStream(inputFile); + InputStream bufferedInputStream = new BufferedInputStream(input); + bufferedInputStream.mark(100); + importedData = importCSV(bufferedInputStream); } - - input1.close(); + zipFile.close(); if (importedData == null) { throw new FormatException("No imported data"); } - - List duplicatedCardsImages = new ArrayList<>(); - - Map idMap = saveAndDeduplicate(context, database, importedData, imageChecksums, duplicatedCardsImages); - - if (isZipFile) { - // Pass #2: save images - InputStream input2 = new FileInputStream(inputFile); - InputStream bufferedInputStream2 = new BufferedInputStream(input2); - ZipInputStream zipInputStream2 = new ZipInputStream(bufferedInputStream2, password); - - while ((localFileHeader = zipInputStream2.getNextEntry()) != null) { - String fileName = Uri.parse(localFileHeader.getFileName()).getLastPathSegment(); - if (fileName.endsWith(".png") && !duplicatedCardsImages.contains(fileName)) { - String newFileName = Utils.getRenamedCardImageFileName(fileName, idMap); - Utils.saveCardImage(context, ZipUtils.readImage(zipInputStream2), newFileName); - } - } - - input2.close(); - } + saveAndDeduplicate(context, database, importedData); } - public Map saveAndDeduplicate(Context context, SQLiteDatabase database, final ImportedData data, final Map imageChecksums, List duplicatedCardsImages) throws IOException { + public void saveAndDeduplicate(Context context, SQLiteDatabase database, final ImportedData data) throws IOException { Map idMap = new HashMap<>(); - Set existingImages = DBHelper.imageFiles(context, database); for (LoyaltyCard card : data.cards) { List candidates = DBHelper.getLoyaltyCardsByCardId(context, database, card.cardId); boolean duplicateFound = false; for (LoyaltyCard existing : candidates) { - if (isDuplicate(context, existing, card, existingImages, imageChecksums, duplicatedCardsImages)) { + if (isDuplicate(context, existing, card)) { duplicateFound = true; break; } @@ -140,6 +96,10 @@ public Map saveAndDeduplicate(Context context, SQLiteDatabase long newId = DBHelper.insertLoyaltyCard(database, card.store, card.note, card.validFrom, card.expiry, card.balance, card.balanceType, card.cardId, card.barcodeId, card.barcodeType, card.headerColor, card.starStatus, card.lastUsed, card.archiveStatus); idMap.put(card.id, (int) newId); + + Utils.saveCardImage(context, card.getImageThumbnail(context), (int) newId, ImageLocationType.icon); + Utils.saveCardImage(context, card.getImageFront(context), (int) newId, ImageLocationType.front); + Utils.saveCardImage(context, card.getImageBack(context), (int) newId, ImageLocationType.back); } } @@ -155,31 +115,12 @@ public Map saveAndDeduplicate(Context context, SQLiteDatabase cardGroups.add(DBHelper.getGroup(database, groupId)); DBHelper.setLoyaltyCardGroups(database, cardId, cardGroups); } - - return idMap; } - public boolean isDuplicate(Context context, final LoyaltyCard existing, final LoyaltyCard card, final Set existingImages, final Map imageChecksums, List duplicatedCardsImages) throws IOException { + public boolean isDuplicate(Context context, final LoyaltyCard existing, final LoyaltyCard card) { if (!LoyaltyCard.isDuplicate(context, existing, card)) { return false; } - String nameChecksum = null; - for (ImageLocationType imageLocationType : ImageLocationType.values()) { - String nameExists = Utils.getCardImageFileName(existing.id, imageLocationType); - nameChecksum = Utils.getCardImageFileName(card.id, imageLocationType); - - boolean exists = existingImages.contains(nameExists); - if (exists != imageChecksums.containsKey(nameChecksum)) { - return false; - } - if (exists) { - File file = Utils.retrieveCardImageAsFile(context, nameExists); - if (!imageChecksums.get(nameChecksum).equals(Utils.checksum(new FileInputStream(file)))) { - return false; - } - } - } - duplicatedCardsImages.add(nameChecksum); return true; } @@ -189,21 +130,41 @@ public ImportedData importCSV(InputStream input) throws IOException, FormatExcep int version = parseVersion(bufferedReader); switch (version) { case 1: - return parseV1(bufferedReader); + return parseV1(bufferedReader, null); + case 2: + return parseV2(bufferedReader, null); + default: + throw new FormatException(String.format("No code to parse version %s", version)); + } + } + + public ImportedData importZIP(ZipFile zipFile) throws IOException, FormatException, InterruptedException { + FileHeader fileHeader = zipFile.getFileHeader("catima.csv"); + if (fileHeader == null){ + throw new FormatException("No imported data"); + } + + InputStream inputStream = zipFile.getInputStream(fileHeader); + BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8)); + + int version = parseVersion(bufferedReader); + switch (version) { + case 1: + return parseV1(bufferedReader, zipFile); case 2: - return parseV2(bufferedReader); + return parseV2(bufferedReader, zipFile); default: throw new FormatException(String.format("No code to parse version %s", version)); } } - public ImportedData parseV1(BufferedReader input) throws IOException, FormatException, InterruptedException { + public ImportedData parseV1(BufferedReader bufferedInput, ZipFile zipFile) throws IOException, FormatException, InterruptedException { ImportedData data = new ImportedData(new ArrayList<>(), new ArrayList<>(), new ArrayList<>()); - final CSVParser parser = new CSVParser(input, CSVFormat.RFC4180.builder().setHeader().build()); + final CSVParser parser = new CSVParser(bufferedInput, CSVFormat.RFC4180.builder().setHeader().build()); try { for (CSVRecord record : parser) { - LoyaltyCard card = importLoyaltyCard(record); + LoyaltyCard card = importLoyaltyCard(record, zipFile); data.cards.add(card); if (Thread.currentThread().isInterrupted()) { @@ -219,7 +180,7 @@ public ImportedData parseV1(BufferedReader input) throws IOException, FormatExce return data; } - public ImportedData parseV2(BufferedReader input) throws IOException, FormatException, InterruptedException { + public ImportedData parseV2(BufferedReader input, ZipFile zipFile) throws IOException, FormatException, InterruptedException { List cards = new ArrayList<>(); List groups = new ArrayList<>(); List> cardGroups = new ArrayList<>(); @@ -249,7 +210,7 @@ public ImportedData parseV2(BufferedReader input) throws IOException, FormatExce break; case 2: try { - cards = parseV2Cards(stringPart.toString()); + cards = parseV2Cards(stringPart.toString(), zipFile); sectionParsed = true; } catch (FormatException e) { // We may have a multiline field, try again @@ -316,7 +277,7 @@ public List parseV2Groups(String data) throws IOException, FormatExcepti return groups; } - public List parseV2Cards(String data) throws IOException, FormatException, InterruptedException { + public List parseV2Cards(String data, ZipFile zipFile) throws IOException, FormatException, InterruptedException { // Parse cards final CSVParser cardParser = new CSVParser(new StringReader(data), CSVFormat.RFC4180.builder().setHeader().build()); @@ -338,7 +299,7 @@ public List parseV2Cards(String data) throws IOException, FormatExc List cards = new ArrayList<>(); for (CSVRecord record : records) { - LoyaltyCard card = importLoyaltyCard(record); + LoyaltyCard card = importLoyaltyCard(record, zipFile); cards.add(card); } return cards; @@ -405,7 +366,7 @@ private int parseVersion(BufferedReader reader) throws IOException { * Import a single loyalty card into the database using the given * session. */ - private LoyaltyCard importLoyaltyCard(CSVRecord record) throws FormatException { + private LoyaltyCard importLoyaltyCard(CSVRecord record, ZipFile zipFile) throws FormatException, IOException { int id = CSVHelpers.extractInt(DBHelper.LoyaltyCardDbIds.ID, record); String store = CSVHelpers.extractString(DBHelper.LoyaltyCardDbIds.STORE, record, ""); @@ -502,6 +463,26 @@ private LoyaltyCard importLoyaltyCard(CSVRecord record) throws FormatException { // We catch this exception so we can still import old backups } + Bitmap imgIcon = null; + Bitmap imgFront = null; + Bitmap imgBack = null; + + if (zipFile != null) { + FileHeader headerIcon = zipFile.getFileHeader(Utils.getCardImageFileName(id, ImageLocationType.icon)); + FileHeader headerFront = zipFile.getFileHeader(Utils.getCardImageFileName(id, ImageLocationType.front)); + FileHeader headerBack = zipFile.getFileHeader(Utils.getCardImageFileName(id, ImageLocationType.back)); + + if (headerIcon != null) { + imgIcon = BitmapFactory.decodeStream(zipFile.getInputStream(headerIcon)); + } + if (headerFront != null) { + imgFront = BitmapFactory.decodeStream(zipFile.getInputStream(headerFront)); + } + if (headerBack != null) { + imgBack = BitmapFactory.decodeStream(zipFile.getInputStream(headerBack)); + } + } + return new LoyaltyCard( id, store, @@ -519,11 +500,11 @@ private LoyaltyCard importLoyaltyCard(CSVRecord record) throws FormatException { DBHelper.DEFAULT_ZOOM_LEVEL, DBHelper.DEFAULT_ZOOM_LEVEL_WIDTH, archiveStatus, + imgIcon, null, + imgFront, null, - null, - null, - null, + imgBack, null ); } From a8320661871ed9c7e2b0536789eb2eb4da2b16c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=BA=C5=A1=20Petro?= Date: Tue, 21 Oct 2025 23:11:46 +0200 Subject: [PATCH 5/7] fixed test errors --- .../importexport/CatimaImporter.java | 41 +++++++++++++++---- 1 file changed, 32 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/protect/card_locker/importexport/CatimaImporter.java b/app/src/main/java/protect/card_locker/importexport/CatimaImporter.java index 9bac9b6cab..f68167d5dc 100644 --- a/app/src/main/java/protect/card_locker/importexport/CatimaImporter.java +++ b/app/src/main/java/protect/card_locker/importexport/CatimaImporter.java @@ -91,15 +91,22 @@ public void saveAndDeduplicate(Context context, SQLiteDatabase database, final I break; } } - - if (!duplicateFound) { - long newId = DBHelper.insertLoyaltyCard(database, card.store, card.note, card.validFrom, card.expiry, card.balance, card.balanceType, - card.cardId, card.barcodeId, card.barcodeType, card.headerColor, card.starStatus, card.lastUsed, card.archiveStatus); - idMap.put(card.id, (int) newId); - - Utils.saveCardImage(context, card.getImageThumbnail(context), (int) newId, ImageLocationType.icon); - Utils.saveCardImage(context, card.getImageFront(context), (int) newId, ImageLocationType.front); - Utils.saveCardImage(context, card.getImageBack(context), (int) newId, ImageLocationType.back); + LoyaltyCard existing = DBHelper.getLoyaltyCard(context, database, card.id); + + if (candidates.isEmpty()) { + if (existing != null && existing.id == card.id) { + long newId = insertCard(context, database, card, true); + idMap.put(card.id, (int) newId); + }else{ + insertCard(context, database, card, false); + } + }else if (!duplicateFound) { + if (existing != null && existing.id == card.id) { + long newId = insertCard(context, database, card, true); + idMap.put(card.id, (int) newId); + }else{ + insertCard(context, database, card, false); + } } } @@ -124,6 +131,22 @@ public boolean isDuplicate(Context context, final LoyaltyCard existing, final Lo return true; } + private long insertCard(Context context, SQLiteDatabase database, LoyaltyCard card, boolean newCard) throws IOException{ + long newId = 0; + if (newCard){ + newId = DBHelper.insertLoyaltyCard(database, card.store, card.note, card.validFrom, card.expiry, card.balance, card.balanceType, + card.cardId, card.barcodeId, card.barcodeType, card.headerColor, card.starStatus, card.lastUsed, card.archiveStatus); + }else { + DBHelper.insertLoyaltyCard(database, card.id, card.store, card.note, card.validFrom, card.expiry, card.balance, card.balanceType, + card.cardId, card.barcodeId, card.barcodeType, card.headerColor, card.starStatus, card.lastUsed, card.archiveStatus); + } + Utils.saveCardImage(context, card.getImageThumbnail(context), card.id, ImageLocationType.icon); + Utils.saveCardImage(context, card.getImageFront(context), card.id, ImageLocationType.front); + Utils.saveCardImage(context, card.getImageBack(context), card.id, ImageLocationType.back); + + return newId; + } + public ImportedData importCSV(InputStream input) throws IOException, FormatException, InterruptedException { BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8)); From 3dece0b92d1b8fb707b987dd775377237bf55acd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=BA=C5=A1=20Petro?= Date: Wed, 22 Oct 2025 23:14:35 +0200 Subject: [PATCH 6/7] update useless if and some code style --- .../java/protect/card_locker/LoyaltyCard.java | 2 +- .../importexport/CatimaImporter.java | 52 ++++++------------- 2 files changed, 16 insertions(+), 38 deletions(-) diff --git a/app/src/main/java/protect/card_locker/LoyaltyCard.java b/app/src/main/java/protect/card_locker/LoyaltyCard.java index 05e282eaa7..7a12d1cab7 100644 --- a/app/src/main/java/protect/card_locker/LoyaltyCard.java +++ b/app/src/main/java/protect/card_locker/LoyaltyCard.java @@ -609,8 +609,8 @@ public static boolean nullableBitmapsEqual(@Nullable Bitmap a, @Nullable Bitmap return a.sameAs(b); } - return false; // One is null and the other isn't, so it's not equal + return false; } @NonNull diff --git a/app/src/main/java/protect/card_locker/importexport/CatimaImporter.java b/app/src/main/java/protect/card_locker/importexport/CatimaImporter.java index f68167d5dc..55d6bc9035 100644 --- a/app/src/main/java/protect/card_locker/importexport/CatimaImporter.java +++ b/app/src/main/java/protect/card_locker/importexport/CatimaImporter.java @@ -63,7 +63,7 @@ public void importData(Context context, SQLiteDatabase database, File inputFile, if (zipFile.isValidZipFile()) { importedData = importZIP(zipFile); - }else { + } else { // This is not a zip file, try importing as bare CSV InputStream input = new FileInputStream(inputFile); InputStream bufferedInputStream = new BufferedInputStream(input); @@ -86,26 +86,27 @@ public void saveAndDeduplicate(Context context, SQLiteDatabase database, final I boolean duplicateFound = false; for (LoyaltyCard existing : candidates) { - if (isDuplicate(context, existing, card)) { + if (LoyaltyCard.isDuplicate(context, existing, card)) { duplicateFound = true; break; } } - LoyaltyCard existing = DBHelper.getLoyaltyCard(context, database, card.id); - if (candidates.isEmpty()) { + if (!duplicateFound) { + LoyaltyCard existing = DBHelper.getLoyaltyCard(context, database, card.id); if (existing != null && existing.id == card.id) { - long newId = insertCard(context, database, card, true); + long newId = DBHelper.insertLoyaltyCard(database, card.store, card.note, card.validFrom, card.expiry, card.balance, card.balanceType, + card.cardId, card.barcodeId, card.barcodeType, card.headerColor, card.starStatus, card.lastUsed, card.archiveStatus); idMap.put(card.id, (int) newId); + Utils.saveCardImage(context, card.getImageThumbnail(context), (int) newId, ImageLocationType.icon); + Utils.saveCardImage(context, card.getImageFront(context), (int) newId, ImageLocationType.front); + Utils.saveCardImage(context, card.getImageBack(context), (int) newId, ImageLocationType.back); }else{ - insertCard(context, database, card, false); - } - }else if (!duplicateFound) { - if (existing != null && existing.id == card.id) { - long newId = insertCard(context, database, card, true); - idMap.put(card.id, (int) newId); - }else{ - insertCard(context, database, card, false); + DBHelper.insertLoyaltyCard(database, card.id, card.store, card.note, card.validFrom, card.expiry, card.balance, card.balanceType, + card.cardId, card.barcodeId, card.barcodeType, card.headerColor, card.starStatus, card.lastUsed, card.archiveStatus); + Utils.saveCardImage(context, card.getImageThumbnail(context), card.id, ImageLocationType.icon); + Utils.saveCardImage(context, card.getImageFront(context), card.id, ImageLocationType.front); + Utils.saveCardImage(context, card.getImageBack(context), card.id, ImageLocationType.back); } } } @@ -124,29 +125,6 @@ public void saveAndDeduplicate(Context context, SQLiteDatabase database, final I } } - public boolean isDuplicate(Context context, final LoyaltyCard existing, final LoyaltyCard card) { - if (!LoyaltyCard.isDuplicate(context, existing, card)) { - return false; - } - return true; - } - - private long insertCard(Context context, SQLiteDatabase database, LoyaltyCard card, boolean newCard) throws IOException{ - long newId = 0; - if (newCard){ - newId = DBHelper.insertLoyaltyCard(database, card.store, card.note, card.validFrom, card.expiry, card.balance, card.balanceType, - card.cardId, card.barcodeId, card.barcodeType, card.headerColor, card.starStatus, card.lastUsed, card.archiveStatus); - }else { - DBHelper.insertLoyaltyCard(database, card.id, card.store, card.note, card.validFrom, card.expiry, card.balance, card.balanceType, - card.cardId, card.barcodeId, card.barcodeType, card.headerColor, card.starStatus, card.lastUsed, card.archiveStatus); - } - Utils.saveCardImage(context, card.getImageThumbnail(context), card.id, ImageLocationType.icon); - Utils.saveCardImage(context, card.getImageFront(context), card.id, ImageLocationType.front); - Utils.saveCardImage(context, card.getImageBack(context), card.id, ImageLocationType.back); - - return newId; - } - public ImportedData importCSV(InputStream input) throws IOException, FormatException, InterruptedException { BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8)); @@ -163,7 +141,7 @@ public ImportedData importCSV(InputStream input) throws IOException, FormatExcep public ImportedData importZIP(ZipFile zipFile) throws IOException, FormatException, InterruptedException { FileHeader fileHeader = zipFile.getFileHeader("catima.csv"); - if (fileHeader == null){ + if (fileHeader == null) { throw new FormatException("No imported data"); } From 50c29f5dad35c932b04ea24636afe9b5ec2b590f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=BA=C5=A1=20Petro?= Date: Mon, 3 Nov 2025 09:10:58 +0100 Subject: [PATCH 7/7] change getLoyaltyCardsByCardId to getLoyaltyCardCursor --- .../java/protect/card_locker/DBHelper.java | 24 ----------------- .../importexport/CatimaImporter.java | 27 ++++++++++++++----- 2 files changed, 21 insertions(+), 30 deletions(-) diff --git a/app/src/main/java/protect/card_locker/DBHelper.java b/app/src/main/java/protect/card_locker/DBHelper.java index 07b10c211b..88ad9167bb 100644 --- a/app/src/main/java/protect/card_locker/DBHelper.java +++ b/app/src/main/java/protect/card_locker/DBHelper.java @@ -561,30 +561,6 @@ public static LoyaltyCard getLoyaltyCard(Context context, SQLiteDatabase databas return card; } - public static List getLoyaltyCardsByCardId(Context context, SQLiteDatabase database, final String cardId) { - List cards = new ArrayList<>(); - - Cursor data = database.query( - LoyaltyCardDbIds.TABLE, - null, - whereAttrs(LoyaltyCardDbIds.CARD_ID), - withArgs(cardId), - null, - null, - null - ); - - if (data.moveToFirst()) { - do { - LoyaltyCard card = LoyaltyCard.fromCursor(context, data); - cards.add(card); - } while (data.moveToNext()); - } - - data.close(); - return cards; - } - public static List getLoyaltyCardGroups(SQLiteDatabase database, final int id) { Cursor data = database.rawQuery("select * from " + LoyaltyCardDbGroups.TABLE + " g " + " LEFT JOIN " + LoyaltyCardDbIdsGroups.TABLE + " ig ON ig." + LoyaltyCardDbIdsGroups.groupID + " = g." + LoyaltyCardDbGroups.ID + diff --git a/app/src/main/java/protect/card_locker/importexport/CatimaImporter.java b/app/src/main/java/protect/card_locker/importexport/CatimaImporter.java index 55d6bc9035..f5666ac4c8 100644 --- a/app/src/main/java/protect/card_locker/importexport/CatimaImporter.java +++ b/app/src/main/java/protect/card_locker/importexport/CatimaImporter.java @@ -1,6 +1,7 @@ package protect.card_locker.importexport; import android.content.Context; +import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.graphics.Bitmap; import android.graphics.BitmapFactory; @@ -82,16 +83,30 @@ public void saveAndDeduplicate(Context context, SQLiteDatabase database, final I Map idMap = new HashMap<>(); for (LoyaltyCard card : data.cards) { - List candidates = DBHelper.getLoyaltyCardsByCardId(context, database, card.cardId); boolean duplicateFound = false; - for (LoyaltyCard existing : candidates) { - if (LoyaltyCard.isDuplicate(context, existing, card)) { - duplicateFound = true; - break; - } + Cursor cursor = database.query( + DBHelper.LoyaltyCardDbIds.TABLE, + null, + DBHelper.LoyaltyCardDbIds.CARD_ID + " = ?", + new String[]{card.cardId}, + null, + null, + null + ); + + if (cursor.moveToFirst()) { + do { + LoyaltyCard existing = LoyaltyCard.fromCursor(context, cursor); + if (LoyaltyCard.isDuplicate(context, existing, card)) { + duplicateFound = true; + break; + } + } while (cursor.moveToNext()); } + cursor.close(); + if (!duplicateFound) { LoyaltyCard existing = DBHelper.getLoyaltyCard(context, database, card.id); if (existing != null && existing.id == card.id) {