mirror of
https://github.com/CatimaLoyalty/Android.git
synced 2026-04-04 15:43:46 -04:00
Merge branch 'main' of github.com:CatimaLoyalty/Android
This commit is contained in:
@@ -16,13 +16,18 @@ import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Currency;
|
||||
import java.util.Date;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
public class DBHelper extends SQLiteOpenHelper {
|
||||
public static final String DATABASE_NAME = "Catima.db";
|
||||
public static final int ORIGINAL_DATABASE_VERSION = 1;
|
||||
public static final int DATABASE_VERSION = 16;
|
||||
|
||||
// NB: changing this value requires a migration
|
||||
public static final int DEFAULT_ZOOM_LEVEL = 100;
|
||||
|
||||
public static class LoyaltyCardDbGroups {
|
||||
public static final String TABLE = "groups";
|
||||
public static final String ID = "_id";
|
||||
@@ -106,7 +111,7 @@ public class DBHelper extends SQLiteOpenHelper {
|
||||
LoyaltyCardDbIds.BARCODE_TYPE + " TEXT," +
|
||||
LoyaltyCardDbIds.STAR_STATUS + " INTEGER DEFAULT '0'," +
|
||||
LoyaltyCardDbIds.LAST_USED + " INTEGER DEFAULT '0', " +
|
||||
LoyaltyCardDbIds.ZOOM_LEVEL + " INTEGER DEFAULT '100', " +
|
||||
LoyaltyCardDbIds.ZOOM_LEVEL + " INTEGER DEFAULT '" + DEFAULT_ZOOM_LEVEL + "', " +
|
||||
LoyaltyCardDbIds.ARCHIVE_STATUS + " INTEGER DEFAULT '0' )");
|
||||
|
||||
// create associative table for cards in groups
|
||||
@@ -323,6 +328,21 @@ public class DBHelper extends SQLiteOpenHelper {
|
||||
}
|
||||
}
|
||||
|
||||
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.toLoyaltyCard(cardCursor);
|
||||
for (ImageLocationType imageLocationType : ImageLocationType.values()) {
|
||||
String name = Utils.getCardImageFileName(card.id, imageLocationType);
|
||||
if (Utils.retrieveCardImageAsFile(context, name).exists()) {
|
||||
files.add(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
private static ContentValues generateFTSContentValues(final int id, final String store, final String note) {
|
||||
// FTS on Android is severely limited and can only search for word starting with a certain string
|
||||
// So for each word, we grab every single substring
|
||||
|
||||
@@ -14,30 +14,28 @@ public class LoyaltyCard implements Parcelable {
|
||||
public final int id;
|
||||
public final String store;
|
||||
public final String note;
|
||||
@Nullable
|
||||
public final Date validFrom;
|
||||
@Nullable
|
||||
public final Date expiry;
|
||||
public final BigDecimal balance;
|
||||
@Nullable
|
||||
public final Currency balanceType;
|
||||
public final String cardId;
|
||||
|
||||
@Nullable
|
||||
public final String barcodeId;
|
||||
|
||||
@Nullable
|
||||
public final CatimaBarcode barcodeType;
|
||||
|
||||
@Nullable
|
||||
public final Integer headerColor;
|
||||
|
||||
public final int starStatus;
|
||||
public final int archiveStatus;
|
||||
public final long lastUsed;
|
||||
public int zoomLevel;
|
||||
|
||||
public LoyaltyCard(final int id, final String store, final String note, final Date validFrom,
|
||||
final Date expiry, final BigDecimal balance, final Currency balanceType,
|
||||
final String cardId, @Nullable final String barcodeId,
|
||||
@Nullable final CatimaBarcode barcodeType,
|
||||
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,
|
||||
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 archiveStatus) {
|
||||
this.id = id;
|
||||
@@ -145,6 +143,24 @@ public class LoyaltyCard implements Parcelable {
|
||||
return new LoyaltyCard(id, store, note, validFrom, expiry, balance, balanceType, cardId, barcodeId, barcodeType, headerColor, starred, lastUsed, zoomLevel, archived);
|
||||
}
|
||||
|
||||
public static boolean isDuplicate(final LoyaltyCard a, final LoyaltyCard b) {
|
||||
// Skip lastUsed & zoomLevel
|
||||
return a.id == b.id && // non-nullable int
|
||||
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
|
||||
a.balance.equals(b.balance) && // non-nullable BigDecimal
|
||||
Utils.equals(a.balanceType, b.balanceType) && // nullable Currency
|
||||
a.cardId.equals(b.cardId) && // non-nullable String
|
||||
Utils.equals(a.barcodeId, b.barcodeId) && // nullable String
|
||||
Utils.equals(a.barcodeType == null ? null : a.barcodeType.format(),
|
||||
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
|
||||
}
|
||||
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return id;
|
||||
|
||||
@@ -50,6 +50,8 @@ import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.math.BigDecimal;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.text.NumberFormat;
|
||||
import java.text.ParseException;
|
||||
import java.util.Calendar;
|
||||
@@ -58,6 +60,8 @@ import java.util.Date;
|
||||
import java.util.GregorianCalendar;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import protect.card_locker.preferences.Settings;
|
||||
|
||||
@@ -76,6 +80,8 @@ public class Utils {
|
||||
public static final int CARD_IMAGE_FROM_FILE_BACK = 9;
|
||||
public static final int CARD_IMAGE_FROM_FILE_ICON = 10;
|
||||
|
||||
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;
|
||||
@@ -380,6 +386,31 @@ public class Utils {
|
||||
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<Integer, Integer> 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);
|
||||
@@ -481,6 +512,18 @@ public class Utils {
|
||||
return new File(context.getCacheDir() + "/" + name);
|
||||
}
|
||||
|
||||
public static File copyToTempFile(Context context, InputStream input, String name) throws IOException {
|
||||
File file = createTempFile(context, name);
|
||||
try (input; FileOutputStream out = new FileOutputStream(file)) {
|
||||
byte[] buf = new byte[4096];
|
||||
int len;
|
||||
while ((len = input.read(buf)) != -1) {
|
||||
out.write(buf, 0, len);
|
||||
}
|
||||
return file;
|
||||
}
|
||||
}
|
||||
|
||||
public static String saveTempImage(Context context, Bitmap in, String name, Bitmap.CompressFormat format) {
|
||||
File image = createTempFile(context, name);
|
||||
try (FileOutputStream out = new FileOutputStream(image)) {
|
||||
@@ -630,4 +673,31 @@ public class Utils {
|
||||
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;
|
||||
} else if (a == null || b == null) {
|
||||
return false;
|
||||
}
|
||||
return a.equals(b);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,8 @@ import org.apache.commons.csv.CSVRecord;
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
@@ -22,12 +24,17 @@ import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Currency;
|
||||
import java.util.Date;
|
||||
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;
|
||||
import protect.card_locker.FormatException;
|
||||
import protect.card_locker.Group;
|
||||
import protect.card_locker.ImageLocationType;
|
||||
import protect.card_locker.LoyaltyCard;
|
||||
import protect.card_locker.Utils;
|
||||
import protect.card_locker.ZipUtils;
|
||||
|
||||
@@ -39,24 +46,42 @@ import protect.card_locker.ZipUtils;
|
||||
* A header is expected for the each table showing the names of the columns.
|
||||
*/
|
||||
public class CatimaImporter implements Importer {
|
||||
public void importData(Context context, SQLiteDatabase database, InputStream input, char[] password) throws IOException, FormatException, InterruptedException {
|
||||
InputStream bufferedInputStream = new BufferedInputStream(input);
|
||||
bufferedInputStream.mark(100);
|
||||
public static class ImportedData {
|
||||
public final List<LoyaltyCard> cards;
|
||||
public final List<String> groups;
|
||||
public final List<Map.Entry<Integer, String>> cardGroups;
|
||||
|
||||
ImportedData(final List<LoyaltyCard> cards, final List<String> groups, final List<Map.Entry<Integer, String>> cardGroups) {
|
||||
this.cards = cards;
|
||||
this.groups = groups;
|
||||
this.cardGroups = cardGroups;
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
ZipInputStream zipInputStream = new ZipInputStream(bufferedInputStream, password);
|
||||
|
||||
boolean isZipFile = false;
|
||||
|
||||
LocalFileHeader localFileHeader;
|
||||
while ((localFileHeader = zipInputStream.getNextEntry()) != null) {
|
||||
Map<String, String> imageChecksums = new HashMap<>();
|
||||
ImportedData importedData = null;
|
||||
|
||||
while ((localFileHeader = zipInputStream1.getNextEntry()) != null) {
|
||||
isZipFile = true;
|
||||
|
||||
String fileName = Uri.parse(localFileHeader.getFileName()).getLastPathSegment();
|
||||
if (fileName.equals("catima.csv")) {
|
||||
importCSV(context, database, zipInputStream);
|
||||
importedData = importCSV(zipInputStream1);
|
||||
} else if (fileName.endsWith(".png")) {
|
||||
Utils.saveCardImage(context, ZipUtils.readImage(zipInputStream), fileName);
|
||||
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);
|
||||
}
|
||||
@@ -64,35 +89,110 @@ public class CatimaImporter implements Importer {
|
||||
|
||||
if (!isZipFile) {
|
||||
// This is not a zip file, try importing as bare CSV
|
||||
bufferedInputStream.reset();
|
||||
importCSV(context, database, bufferedInputStream);
|
||||
bufferedInputStream1.reset();
|
||||
importedData = importCSV(bufferedInputStream1);
|
||||
}
|
||||
|
||||
input.close();
|
||||
input1.close();
|
||||
|
||||
if (importedData == null) {
|
||||
throw new FormatException("No imported data");
|
||||
}
|
||||
|
||||
Map<Integer, Integer> 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();
|
||||
}
|
||||
}
|
||||
|
||||
public void importCSV(Context context, SQLiteDatabase database, InputStream input) throws IOException, FormatException, InterruptedException {
|
||||
public Map<Integer, Integer> saveAndDeduplicate(Context context, SQLiteDatabase database, final ImportedData data, final Map<String, String> imageChecksums) throws IOException {
|
||||
Map<Integer, Integer> idMap = new HashMap<>();
|
||||
Set<String> existingImages = DBHelper.imageFiles(context, database);
|
||||
|
||||
for (LoyaltyCard card : data.cards) {
|
||||
LoyaltyCard existing = DBHelper.getLoyaltyCard(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);
|
||||
}
|
||||
}
|
||||
|
||||
for (String group : data.groups) {
|
||||
DBHelper.insertGroup(database, group);
|
||||
}
|
||||
|
||||
for (Map.Entry<Integer, String> entry : data.cardGroups) {
|
||||
int cardId = idMap.getOrDefault(entry.getKey(), entry.getKey());
|
||||
String groupId = entry.getValue();
|
||||
// For existing & newly imported cards, add the groups from the import to the internal state
|
||||
List<Group> cardGroups = DBHelper.getLoyaltyCardGroups(database, cardId);
|
||||
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<String> existingImages, final Map<String, String> imageChecksums) throws IOException {
|
||||
if (!LoyaltyCard.isDuplicate(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;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
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:
|
||||
parseV1(database, bufferedReader);
|
||||
break;
|
||||
return parseV1(bufferedReader);
|
||||
case 2:
|
||||
parseV2(context, database, bufferedReader);
|
||||
break;
|
||||
return parseV2(bufferedReader);
|
||||
default:
|
||||
throw new FormatException(String.format("No code to parse version %s", version));
|
||||
}
|
||||
}
|
||||
|
||||
public void parseV1(SQLiteDatabase database, BufferedReader input) throws IOException, FormatException, InterruptedException {
|
||||
public ImportedData parseV1(BufferedReader input) 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());
|
||||
|
||||
try {
|
||||
for (CSVRecord record : parser) {
|
||||
importLoyaltyCard(database, record);
|
||||
LoyaltyCard card = importLoyaltyCard(record);
|
||||
data.cards.add(card);
|
||||
|
||||
if (Thread.currentThread().isInterrupted()) {
|
||||
throw new InterruptedException();
|
||||
@@ -103,9 +203,15 @@ public class CatimaImporter implements Importer {
|
||||
} catch (IllegalArgumentException | IllegalStateException e) {
|
||||
throw new FormatException("Issue parsing CSV data", e);
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
public void parseV2(Context context, SQLiteDatabase database, BufferedReader input) throws IOException, FormatException, InterruptedException {
|
||||
public ImportedData parseV2(BufferedReader input) throws IOException, FormatException, InterruptedException {
|
||||
List<LoyaltyCard> cards = new ArrayList<>();
|
||||
List<String> groups = new ArrayList<>();
|
||||
List<Map.Entry<Integer, String>> cardGroups = new ArrayList<>();
|
||||
|
||||
int part = 0;
|
||||
StringBuilder stringPart = new StringBuilder();
|
||||
|
||||
@@ -123,7 +229,7 @@ public class CatimaImporter implements Importer {
|
||||
break;
|
||||
case 1:
|
||||
try {
|
||||
parseV2Groups(database, stringPart.toString());
|
||||
groups = parseV2Groups(stringPart.toString());
|
||||
sectionParsed = true;
|
||||
} catch (FormatException e) {
|
||||
// We may have a multiline field, try again
|
||||
@@ -131,7 +237,7 @@ public class CatimaImporter implements Importer {
|
||||
break;
|
||||
case 2:
|
||||
try {
|
||||
parseV2Cards(context, database, stringPart.toString());
|
||||
cards = parseV2Cards(stringPart.toString());
|
||||
sectionParsed = true;
|
||||
} catch (FormatException e) {
|
||||
// We may have a multiline field, try again
|
||||
@@ -139,7 +245,7 @@ public class CatimaImporter implements Importer {
|
||||
break;
|
||||
case 3:
|
||||
try {
|
||||
parseV2CardGroups(database, stringPart.toString());
|
||||
cardGroups = parseV2CardGroups(stringPart.toString());
|
||||
sectionParsed = true;
|
||||
} catch (FormatException e) {
|
||||
// We may have a multiline field, try again
|
||||
@@ -166,9 +272,11 @@ public class CatimaImporter implements Importer {
|
||||
} catch (FormatException e) {
|
||||
throw new FormatException("Issue parsing CSV data", e);
|
||||
}
|
||||
|
||||
return new ImportedData(cards, groups, cardGroups);
|
||||
}
|
||||
|
||||
public void parseV2Groups(SQLiteDatabase database, String data) throws IOException, FormatException, InterruptedException {
|
||||
public List<String> parseV2Groups(String data) throws IOException, FormatException, InterruptedException {
|
||||
// Parse groups
|
||||
final CSVParser groupParser = new CSVParser(new StringReader(data), CSVFormat.RFC4180.builder().setHeader().build());
|
||||
|
||||
@@ -188,12 +296,15 @@ public class CatimaImporter implements Importer {
|
||||
groupParser.close();
|
||||
}
|
||||
|
||||
List<String> groups = new ArrayList<>();
|
||||
for (CSVRecord record : records) {
|
||||
importGroup(database, record);
|
||||
String group = importGroup(record);
|
||||
groups.add(group);
|
||||
}
|
||||
return groups;
|
||||
}
|
||||
|
||||
public void parseV2Cards(Context context, SQLiteDatabase database, String data) throws IOException, FormatException, InterruptedException {
|
||||
public List<LoyaltyCard> parseV2Cards(String data) throws IOException, FormatException, InterruptedException {
|
||||
// Parse cards
|
||||
final CSVParser cardParser = new CSVParser(new StringReader(data), CSVFormat.RFC4180.builder().setHeader().build());
|
||||
|
||||
@@ -213,12 +324,15 @@ public class CatimaImporter implements Importer {
|
||||
cardParser.close();
|
||||
}
|
||||
|
||||
List<LoyaltyCard> cards = new ArrayList<>();
|
||||
for (CSVRecord record : records) {
|
||||
importLoyaltyCard(database, record);
|
||||
LoyaltyCard card = importLoyaltyCard(record);
|
||||
cards.add(card);
|
||||
}
|
||||
return cards;
|
||||
}
|
||||
|
||||
public void parseV2CardGroups(SQLiteDatabase database, String data) throws IOException, FormatException, InterruptedException {
|
||||
public List<Map.Entry<Integer, String>> parseV2CardGroups(String data) throws IOException, FormatException, InterruptedException {
|
||||
// Parse card group mappings
|
||||
final CSVParser cardGroupParser = new CSVParser(new StringReader(data), CSVFormat.RFC4180.builder().setHeader().build());
|
||||
|
||||
@@ -238,9 +352,12 @@ public class CatimaImporter implements Importer {
|
||||
cardGroupParser.close();
|
||||
}
|
||||
|
||||
List<Map.Entry<Integer, String>> cardGroups = new ArrayList<>();
|
||||
for (CSVRecord record : records) {
|
||||
importCardGroupMapping(database, record);
|
||||
Map.Entry<Integer, String> entry = importCardGroupMapping(record);
|
||||
cardGroups.add(entry);
|
||||
}
|
||||
return cardGroups;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -276,8 +393,7 @@ public class CatimaImporter implements Importer {
|
||||
* Import a single loyalty card into the database using the given
|
||||
* session.
|
||||
*/
|
||||
private void importLoyaltyCard(SQLiteDatabase database, CSVRecord record)
|
||||
throws FormatException {
|
||||
private LoyaltyCard importLoyaltyCard(CSVRecord record) throws FormatException {
|
||||
int id = CSVHelpers.extractInt(DBHelper.LoyaltyCardDbIds.ID, record);
|
||||
|
||||
String store = CSVHelpers.extractString(DBHelper.LoyaltyCardDbIds.STORE, record, "");
|
||||
@@ -374,28 +490,28 @@ public class CatimaImporter implements Importer {
|
||||
// We catch this exception so we can still import old backups
|
||||
}
|
||||
|
||||
DBHelper.insertLoyaltyCard(database, id, store, note, validFrom, expiry, balance, balanceType, cardId, barcodeId, barcodeType, headerColor, starStatus, lastUsed, archiveStatus);
|
||||
return new LoyaltyCard(id, store, note, validFrom, expiry, balance, balanceType, cardId, barcodeId, barcodeType, headerColor, starStatus, lastUsed, DBHelper.DEFAULT_ZOOM_LEVEL, archiveStatus);
|
||||
}
|
||||
|
||||
/**
|
||||
* Import a single group into the database using the given
|
||||
* session.
|
||||
*/
|
||||
private void importGroup(SQLiteDatabase database, CSVRecord record) throws FormatException {
|
||||
private String importGroup(CSVRecord record) throws FormatException {
|
||||
String id = CSVHelpers.extractString(DBHelper.LoyaltyCardDbGroups.ID, record, null);
|
||||
|
||||
if (id == null) {
|
||||
throw new FormatException("Group has no ID: " + record);
|
||||
}
|
||||
|
||||
DBHelper.insertGroup(database, id);
|
||||
return id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Import a single card to group mapping into the database using the given
|
||||
* session.
|
||||
*/
|
||||
private void importCardGroupMapping(SQLiteDatabase database, CSVRecord record) throws FormatException {
|
||||
private Map.Entry<Integer, String> importCardGroupMapping(CSVRecord record) throws FormatException {
|
||||
int cardId = CSVHelpers.extractInt(DBHelper.LoyaltyCardDbIdsGroups.cardID, record);
|
||||
String groupId = CSVHelpers.extractString(DBHelper.LoyaltyCardDbIdsGroups.groupID, record, null);
|
||||
|
||||
@@ -403,8 +519,6 @@ public class CatimaImporter implements Importer {
|
||||
throw new FormatException("Group has no ID: " + record);
|
||||
}
|
||||
|
||||
List<Group> cardGroups = DBHelper.getLoyaltyCardGroups(database, cardId);
|
||||
cardGroups.add(DBHelper.getGroup(database, groupId));
|
||||
DBHelper.setLoyaltyCardGroups(database, cardId, cardGroups);
|
||||
return Map.entry(cardId, groupId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,8 @@ import org.apache.commons.csv.CSVParser;
|
||||
import org.apache.commons.csv.CSVRecord;
|
||||
import org.json.JSONException;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.StringReader;
|
||||
@@ -31,7 +33,8 @@ import protect.card_locker.Utils;
|
||||
* A header is expected for the each table showing the names of the columns.
|
||||
*/
|
||||
public class FidmeImporter implements Importer {
|
||||
public void importData(Context context, SQLiteDatabase database, InputStream input, char[] password) throws IOException, FormatException, JSONException, ParseException {
|
||||
public void importData(Context context, SQLiteDatabase database, File inputFile, char[] password) throws IOException, FormatException, JSONException, ParseException {
|
||||
InputStream input = new FileInputStream(inputFile);
|
||||
// We actually retrieve a .zip file
|
||||
ZipInputStream zipInputStream = new ZipInputStream(input, password);
|
||||
|
||||
@@ -70,6 +73,7 @@ public class FidmeImporter implements Importer {
|
||||
}
|
||||
|
||||
zipInputStream.close();
|
||||
input.close();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -5,8 +5,8 @@ import android.database.sqlite.SQLiteDatabase;
|
||||
|
||||
import org.json.JSONException;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.text.ParseException;
|
||||
|
||||
import protect.card_locker.FormatException;
|
||||
@@ -23,5 +23,5 @@ public interface Importer {
|
||||
* @throws IOException
|
||||
* @throws FormatException
|
||||
*/
|
||||
void importData(Context context, SQLiteDatabase database, InputStream input, char[] password) throws IOException, FormatException, InterruptedException, JSONException, ParseException;
|
||||
void importData(Context context, SQLiteDatabase database, File inputFile, char[] password) throws IOException, FormatException, InterruptedException, JSONException, ParseException;
|
||||
}
|
||||
|
||||
@@ -6,10 +6,15 @@ import android.util.Log;
|
||||
|
||||
import net.lingala.zip4j.exception.ZipException;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
import protect.card_locker.Utils;
|
||||
|
||||
public class MultiFormatImporter {
|
||||
private static final String TAG = "Catima";
|
||||
private static final String TEMP_ZIP_NAME = MultiFormatImporter.class.getSimpleName() + ".zip";
|
||||
|
||||
/**
|
||||
* Attempts to import data from the input stream of the
|
||||
@@ -42,23 +47,33 @@ public class MultiFormatImporter {
|
||||
|
||||
String error = null;
|
||||
if (importer != null) {
|
||||
database.beginTransaction();
|
||||
File inputFile;
|
||||
try {
|
||||
importer.importData(context, database, input, 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 {
|
||||
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) {
|
||||
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 (Exception e) {
|
||||
Log.e(TAG, "Failed to import data", e);
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "Failed to copy ZIP file", e);
|
||||
error = e.toString();
|
||||
} finally {
|
||||
database.endTransaction();
|
||||
}
|
||||
} else {
|
||||
error = "Unsupported data format imported: " + format.name();
|
||||
|
||||
@@ -16,6 +16,8 @@ import org.apache.commons.csv.CSVRecord;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
@@ -42,7 +44,7 @@ import protect.card_locker.ZipUtils;
|
||||
public class StocardImporter implements Importer {
|
||||
private static final String TAG = "Catima";
|
||||
|
||||
public void importData(Context context, SQLiteDatabase database, InputStream input, char[] password) throws IOException, FormatException, JSONException, ParseException {
|
||||
public void importData(Context context, SQLiteDatabase database, File inputFile, char[] password) throws IOException, FormatException, JSONException, ParseException {
|
||||
HashMap<String, HashMap<String, Object>> loyaltyCardHashMap = new HashMap<>();
|
||||
HashMap<String, HashMap<String, Object>> providers = new HashMap<>();
|
||||
|
||||
@@ -62,6 +64,7 @@ public class StocardImporter implements Importer {
|
||||
throw new FormatException("Issue parsing CSV data", e);
|
||||
}
|
||||
|
||||
InputStream input = new FileInputStream(inputFile);
|
||||
ZipInputStream zipInputStream = new ZipInputStream(input, password);
|
||||
|
||||
String[] providersFileName = null;
|
||||
@@ -245,6 +248,7 @@ public class StocardImporter implements Importer {
|
||||
}
|
||||
|
||||
zipInputStream.close();
|
||||
input.close();
|
||||
}
|
||||
|
||||
private boolean startsWith(String[] full, String[] start, int minExtraLength) {
|
||||
|
||||
@@ -12,6 +12,8 @@ import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
@@ -36,7 +38,8 @@ import protect.card_locker.Utils;
|
||||
* A header is expected for the each table showing the names of the columns.
|
||||
*/
|
||||
public class VoucherVaultImporter implements Importer {
|
||||
public void importData(Context context, SQLiteDatabase database, InputStream input, char[] password) throws IOException, FormatException, JSONException, ParseException {
|
||||
public void importData(Context context, SQLiteDatabase database, File inputFile, char[] password) throws IOException, FormatException, JSONException, ParseException {
|
||||
InputStream input = new FileInputStream(inputFile);
|
||||
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8));
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
@@ -130,5 +133,6 @@ public class VoucherVaultImporter implements Importer {
|
||||
}
|
||||
|
||||
bufferedReader.close();
|
||||
input.close();
|
||||
}
|
||||
}
|
||||
@@ -196,6 +196,35 @@ public class ImportExportTest {
|
||||
cursor.close();
|
||||
}
|
||||
|
||||
private void checkLoyaltyCardsAndDuplicates(int numCards) {
|
||||
Cursor cursor = DBHelper.getLoyaltyCardCursor(mDatabase);
|
||||
|
||||
while (cursor.moveToNext()) {
|
||||
LoyaltyCard card = LoyaltyCard.toLoyaltyCard(cursor);
|
||||
|
||||
// ID goes up for duplicates (b/c the cursor orders by store), down for originals
|
||||
int index = card.id > numCards ? card.id - numCards : numCards - card.id + 1;
|
||||
// balance is doubled for modified originals
|
||||
int balance = card.id > numCards ? index : index * 2;
|
||||
|
||||
String expectedStore = String.format("store, \"%4d", index);
|
||||
String expectedNote = String.format("note, \"%4d", index);
|
||||
|
||||
assertEquals(expectedStore, card.store);
|
||||
assertEquals(expectedNote, card.note);
|
||||
assertEquals(null, card.validFrom);
|
||||
assertEquals(null, card.expiry);
|
||||
assertEquals(new BigDecimal(String.valueOf(balance)), card.balance);
|
||||
assertEquals(null, card.balanceType);
|
||||
assertEquals(BARCODE_DATA, card.cardId);
|
||||
assertEquals(null, card.barcodeId);
|
||||
assertEquals(BARCODE_TYPE.format(), card.barcodeType.format());
|
||||
assertEquals(Integer.valueOf(index), card.headerColor);
|
||||
assertEquals(0, card.starStatus);
|
||||
}
|
||||
cursor.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that all of the cards follow the pattern
|
||||
* specified in addLoyaltyCardsSomeStarred(), and are in sequential order
|
||||
@@ -477,6 +506,40 @@ public class ImportExportTest {
|
||||
TestHelpers.getEmptyDb(activity);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void importExistingCardsAfterModification() throws IOException {
|
||||
final int NUM_CARDS = 10;
|
||||
|
||||
TestHelpers.addLoyaltyCards(mDatabase, NUM_CARDS);
|
||||
|
||||
ByteArrayOutputStream outData = new ByteArrayOutputStream();
|
||||
OutputStreamWriter outStream = new OutputStreamWriter(outData);
|
||||
|
||||
// Export into CSV data
|
||||
ImportExportResult result = MultiFormatExporter.exportData(activity.getApplicationContext(), mDatabase, outData, DataFormat.Catima, null);
|
||||
assertEquals(ImportExportResultType.Success, result.resultType());
|
||||
outStream.close();
|
||||
|
||||
// Modify existing cards
|
||||
for (int index = 1; index <= NUM_CARDS; index++) {
|
||||
int id = NUM_CARDS - index + 1;
|
||||
DBHelper.updateLoyaltyCardBalance(mDatabase, id, new BigDecimal(String.valueOf(index * 2)));
|
||||
}
|
||||
|
||||
ByteArrayInputStream inData = new ByteArrayInputStream(outData.toByteArray());
|
||||
|
||||
// Import the CSV data on top of the existing database
|
||||
result = MultiFormatImporter.importData(activity.getApplicationContext(), mDatabase, inData, DataFormat.Catima, null);
|
||||
assertEquals(ImportExportResultType.Success, result.resultType());
|
||||
|
||||
assertEquals(NUM_CARDS * 2, DBHelper.getLoyaltyCardCount(mDatabase));
|
||||
|
||||
checkLoyaltyCardsAndDuplicates(NUM_CARDS);
|
||||
|
||||
// Clear the database for the next format under test
|
||||
TestHelpers.getEmptyDb(activity);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void corruptedImportNothingSaved() {
|
||||
final int NUM_CARDS = 10;
|
||||
|
||||
Reference in New Issue
Block a user