diff --git a/app/src/main/java/protect/card_locker/LoyaltyCard.java b/app/src/main/java/protect/card_locker/LoyaltyCard.java index b4fe334983..7a12d1cab7 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/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 5cc348895c..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,11 +1,13 @@ package protect.card_locker.importexport; import android.content.Context; +import android.database.Cursor; 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 +29,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 +37,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,79 +59,70 @@ 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"); } - - Map idMap = saveAndDeduplicate(context, database, importedData, imageChecksums); - - 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")) { - 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) 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) { - 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)) { - 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); + boolean duplicateFound = false; + + 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) { + 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{ + 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); + } } } @@ -147,51 +138,49 @@ 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) throws IOException { - if (!LoyaltyCard.isDuplicate(context, existing, card)) { - return false; - } - for (ImageLocationType imageLocationType : ImageLocationType.values()) { - String name = Utils.getCardImageFileName(existing.id, imageLocationType); - boolean exists = existingImages.contains(name); - if (exists != imageChecksums.containsKey(name)) { - return false; - } - if (exists) { - File file = Utils.retrieveCardImageAsFile(context, name); - if (!imageChecksums.get(name).equals(Utils.checksum(new FileInputStream(file)))) { - return false; - } - } + public ImportedData importCSV(InputStream input) throws IOException, FormatException, InterruptedException { + BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8)); + + int version = parseVersion(bufferedReader); + switch (version) { + case 1: + return parseV1(bufferedReader, null); + case 2: + return parseV2(bufferedReader, null); + default: + throw new FormatException(String.format("No code to parse version %s", version)); } - return true; } - public ImportedData importCSV(InputStream input) throws IOException, FormatException, InterruptedException { - BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8)); + 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); + 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()) { @@ -207,7 +196,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<>(); @@ -237,7 +226,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 @@ -304,7 +293,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()); @@ -326,7 +315,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; @@ -393,7 +382,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, ""); @@ -490,6 +479,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, @@ -507,11 +516,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 ); }