mirror of
https://github.com/CatimaLoyalty/Android.git
synced 2025-12-26 00:27:55 -05:00
Compare commits
75 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a408f8d727 | ||
|
|
2d7bb02d1a | ||
|
|
38c8e38ed6 | ||
|
|
28b95b8f75 | ||
|
|
c1ebbdb997 | ||
|
|
fadba7a11c | ||
|
|
fd61434565 | ||
|
|
398dff4b3c | ||
|
|
5c95b750b2 | ||
|
|
9851c0a2fa | ||
|
|
c121f846c5 | ||
|
|
c20ba027cf | ||
|
|
e5dd26b8ee | ||
|
|
3b449464ac | ||
|
|
3f4d4e38cd | ||
|
|
30ccd03686 | ||
|
|
5493947c28 | ||
|
|
4172903b42 | ||
|
|
00b1368176 | ||
|
|
09fee5628f | ||
|
|
7a7a2f8361 | ||
|
|
a9ced56023 | ||
|
|
a4af171598 | ||
|
|
164c82a779 | ||
|
|
608c3ab863 | ||
|
|
379a71c7ad | ||
|
|
ee6a6dffcf | ||
|
|
1d6a393914 | ||
|
|
5c4a905ac0 | ||
|
|
91ce71ea68 | ||
|
|
f07ac3e026 | ||
|
|
649f2c47b4 | ||
|
|
62dcc373ed | ||
|
|
e9b04adec6 | ||
|
|
636be16bdd | ||
|
|
5ad28f37b8 | ||
|
|
5ff6059e86 | ||
|
|
b9df712394 | ||
|
|
790fd7e48f | ||
|
|
bb43266a01 | ||
|
|
450cfce84a | ||
|
|
6cef56b38b | ||
|
|
fbb7cf7e9c | ||
|
|
9ab2a6a5b2 | ||
|
|
682fc8303c | ||
|
|
aa1274566b | ||
|
|
d8cd581cb0 | ||
|
|
2cd00f9103 | ||
|
|
ae07f94b25 | ||
|
|
22d671263a | ||
|
|
227f30361f | ||
|
|
b964652b83 | ||
|
|
f926ffa1d0 | ||
|
|
d03b8b5635 | ||
|
|
4e5fea7a52 | ||
|
|
95cb6c0a08 | ||
|
|
d350d0b2c7 | ||
|
|
ac0f6f6f3e | ||
|
|
ab030ba002 | ||
|
|
3ae665b70f | ||
|
|
9cf9959b6b | ||
|
|
d11e2c166b | ||
|
|
f783be7a4f | ||
|
|
9ee96b88e8 | ||
|
|
ba896fc1db | ||
|
|
1425d4af58 | ||
|
|
48510494eb | ||
|
|
d5d53b241a | ||
|
|
901c2d8154 | ||
|
|
84d7e15b5c | ||
|
|
b8fa4d7060 | ||
|
|
da9e3bb6b2 | ||
|
|
3a5973a04d | ||
|
|
5f99f2b17e | ||
|
|
bf05103955 |
2
.github/workflows/autoclose-needs-info.yml
vendored
2
.github/workflows/autoclose-needs-info.yml
vendored
@@ -19,6 +19,4 @@ jobs:
|
||||
close-issue-message: 'This issue is missing necessary information and cannot be worked on in its current state. It has therefore been closed to keep the issue tracker clean. If you have more information, feel free to reopen it.'
|
||||
close-pr-message: 'This PR is missing necessary information and cannot be merged in its current state. It has therefore been closed to keep the issue tracker clean. If you have more information, feel free to reopen it.'
|
||||
only-labels: 'state: needs info'
|
||||
stale-issue-label: 'state: needs info'
|
||||
stale-pr-label: 'state: needs info'
|
||||
enable-statistics: true
|
||||
|
||||
44
.scripts/dump_stocard_stores.py
Executable file
44
.scripts/dump_stocard_stores.py
Executable file
@@ -0,0 +1,44 @@
|
||||
#!/usr/bin/python3
|
||||
|
||||
import csv
|
||||
import json
|
||||
import msgpack
|
||||
|
||||
MSGPACK = "bootstrapdata.msgpack"
|
||||
OUTFILE = "stocard_stores.csv"
|
||||
|
||||
|
||||
def load(fh):
|
||||
data = []
|
||||
for r in msgpack.Unpacker(fh, raw=False):
|
||||
if r["collection"] == "/loyalty-card-providers/":
|
||||
d = json.loads(r["data"])
|
||||
data.append([r["resource_id"], d["name"], d["default_barcode_format"]])
|
||||
return data
|
||||
|
||||
|
||||
def save(data, output_file=OUTFILE):
|
||||
with open(output_file, "w") as fh:
|
||||
writer = csv.writer(fh, lineterminator="\n")
|
||||
writer.writerow(["_id", "name", "barcodeFormat"])
|
||||
for row in data:
|
||||
writer.writerow(row)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import argparse
|
||||
parser = argparse.ArgumentParser(
|
||||
epilog=f"INPUT_FILE must be a .msgpack or .apk and defaults to {MSGPACK}; "
|
||||
f"OUTPUT_FILE defaults to {OUTFILE}")
|
||||
parser.add_argument("input_file", metavar="INPUT_FILE", nargs="?", default=MSGPACK)
|
||||
parser.add_argument("output_file", metavar="OUTPUT_FILE", nargs="?", default=OUTFILE)
|
||||
args = parser.parse_args()
|
||||
if args.input_file.lower().endswith(".apk"):
|
||||
import zipfile
|
||||
with zipfile.ZipFile(args.input_file) as zf:
|
||||
with zf.open(f"assets/{MSGPACK}") as fh:
|
||||
data = load(fh)
|
||||
else:
|
||||
with open(args.input_file, "rb") as fh:
|
||||
data = load(fh)
|
||||
save(data, args.output_file)
|
||||
@@ -1,5 +1,11 @@
|
||||
# Changelog
|
||||
|
||||
## v2.25.2 - 129 (2023-07-27)
|
||||
|
||||
- Improved Catima importer (fixes cards missing when importing)
|
||||
- Fix crash when rotating screen while setting valid from/expiry date
|
||||
- Minor UI tweaks
|
||||
|
||||
## v2.25.1 - 128 (2023-07-17)
|
||||
|
||||
- Fix rare crash
|
||||
|
||||
34
Gemfile.lock
34
Gemfile.lock
@@ -8,20 +8,20 @@ GEM
|
||||
artifactory (3.0.15)
|
||||
atomos (0.1.3)
|
||||
aws-eventstream (1.2.0)
|
||||
aws-partitions (1.771.0)
|
||||
aws-sdk-core (3.173.1)
|
||||
aws-partitions (1.793.0)
|
||||
aws-sdk-core (3.180.0)
|
||||
aws-eventstream (~> 1, >= 1.0.2)
|
||||
aws-partitions (~> 1, >= 1.651.0)
|
||||
aws-sigv4 (~> 1.5)
|
||||
jmespath (~> 1, >= 1.6.1)
|
||||
aws-sdk-kms (1.64.0)
|
||||
aws-sdk-core (~> 3, >= 3.165.0)
|
||||
aws-sdk-kms (1.71.0)
|
||||
aws-sdk-core (~> 3, >= 3.177.0)
|
||||
aws-sigv4 (~> 1.1)
|
||||
aws-sdk-s3 (1.122.0)
|
||||
aws-sdk-core (~> 3, >= 3.165.0)
|
||||
aws-sdk-s3 (1.132.0)
|
||||
aws-sdk-core (~> 3, >= 3.179.0)
|
||||
aws-sdk-kms (~> 1)
|
||||
aws-sigv4 (~> 1.4)
|
||||
aws-sigv4 (1.5.2)
|
||||
aws-sigv4 (~> 1.6)
|
||||
aws-sigv4 (1.6.0)
|
||||
aws-eventstream (~> 1, >= 1.0.2)
|
||||
babosa (1.0.4)
|
||||
claide (1.1.0)
|
||||
@@ -30,13 +30,13 @@ GEM
|
||||
commander (4.6.0)
|
||||
highline (~> 2.0.0)
|
||||
declarative (0.0.20)
|
||||
digest-crc (0.6.4)
|
||||
digest-crc (0.6.5)
|
||||
rake (>= 12.0.0, < 14.0.0)
|
||||
domain_name (0.5.20190701)
|
||||
unf (>= 0.0.5, < 1.0.0)
|
||||
dotenv (2.8.1)
|
||||
emoji_regex (3.2.3)
|
||||
excon (0.99.0)
|
||||
excon (0.100.0)
|
||||
faraday (1.10.3)
|
||||
faraday-em_http (~> 1.0)
|
||||
faraday-em_synchrony (~> 1.0)
|
||||
@@ -66,7 +66,7 @@ GEM
|
||||
faraday_middleware (1.2.0)
|
||||
faraday (~> 1.0)
|
||||
fastimage (2.2.7)
|
||||
fastlane (2.213.0)
|
||||
fastlane (2.214.0)
|
||||
CFPropertyList (>= 2.3, < 4.0.0)
|
||||
addressable (>= 2.8, < 3.0.0)
|
||||
artifactory (~> 3.0)
|
||||
@@ -106,9 +106,9 @@ GEM
|
||||
xcpretty (~> 0.3.0)
|
||||
xcpretty-travis-formatter (>= 0.0.3)
|
||||
gh_inspector (1.1.3)
|
||||
google-apis-androidpublisher_v3 (0.42.0)
|
||||
google-apis-androidpublisher_v3 (0.46.0)
|
||||
google-apis-core (>= 0.11.0, < 2.a)
|
||||
google-apis-core (0.11.0)
|
||||
google-apis-core (0.11.1)
|
||||
addressable (~> 2.5, >= 2.5.1)
|
||||
googleauth (>= 0.16.2, < 2.a)
|
||||
httpclient (>= 2.8.1, < 3.a)
|
||||
@@ -137,7 +137,7 @@ GEM
|
||||
google-cloud-core (~> 1.6)
|
||||
googleauth (>= 0.16.2, < 2.a)
|
||||
mini_mime (~> 1.0)
|
||||
googleauth (1.5.2)
|
||||
googleauth (1.7.0)
|
||||
faraday (>= 0.17.3, < 3.a)
|
||||
jwt (>= 1.4, < 3.0)
|
||||
memoist (~> 0.16)
|
||||
@@ -150,7 +150,7 @@ GEM
|
||||
httpclient (2.8.3)
|
||||
jmespath (1.6.2)
|
||||
json (2.6.3)
|
||||
jwt (2.7.0)
|
||||
jwt (2.7.1)
|
||||
memoist (0.16.2)
|
||||
mini_magick (4.12.0)
|
||||
mini_mime (1.1.2)
|
||||
@@ -161,14 +161,14 @@ GEM
|
||||
optparse (0.1.1)
|
||||
os (1.1.4)
|
||||
plist (3.7.0)
|
||||
public_suffix (5.0.1)
|
||||
public_suffix (5.0.3)
|
||||
rake (13.0.6)
|
||||
representable (3.2.0)
|
||||
declarative (< 0.1.0)
|
||||
trailblazer-option (>= 0.1.1, < 0.2.0)
|
||||
uber (< 0.2.0)
|
||||
retriable (3.1.2)
|
||||
rexml (3.2.5)
|
||||
rexml (3.2.6)
|
||||
rouge (2.0.7)
|
||||
ruby2_keywords (0.0.5)
|
||||
rubyzip (2.3.2)
|
||||
|
||||
@@ -19,8 +19,8 @@ android {
|
||||
applicationId "me.hackerchick.catima"
|
||||
minSdk 21
|
||||
targetSdk 33
|
||||
versionCode 128
|
||||
versionName "2.25.1"
|
||||
versionCode 129
|
||||
versionName "2.25.2"
|
||||
|
||||
vectorDrawables.useSupportLibrary true
|
||||
multiDexEnabled true
|
||||
|
||||
@@ -94,10 +94,12 @@ public class AboutContent {
|
||||
|
||||
public String getContributorInfo() {
|
||||
StringBuilder contributorInfo = new StringBuilder();
|
||||
contributorInfo.append(HtmlCompat.fromHtml(String.format(context.getString(R.string.app_contributors), getContributors()), HtmlCompat.FROM_HTML_MODE_COMPACT));
|
||||
contributorInfo.append(getCopyright());
|
||||
contributorInfo.append("\n\n");
|
||||
contributorInfo.append(context.getString(R.string.app_copyright_old));
|
||||
contributorInfo.append("\n\n");
|
||||
contributorInfo.append(HtmlCompat.fromHtml(String.format(context.getString(R.string.app_contributors), getContributors()), HtmlCompat.FROM_HTML_MODE_COMPACT));
|
||||
contributorInfo.append("\n\n");
|
||||
contributorInfo.append(HtmlCompat.fromHtml(String.format(context.getString(R.string.app_libraries), getThirdPartyLibraries()), HtmlCompat.FROM_HTML_MODE_COMPACT));
|
||||
contributorInfo.append("\n\n");
|
||||
contributorInfo.append(HtmlCompat.fromHtml(String.format(context.getString(R.string.app_resources), getUsedThirdPartyAssets()), HtmlCompat.FROM_HTML_MODE_COMPACT));
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -36,6 +36,20 @@ import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.activity.result.ActivityResultLauncher;
|
||||
import androidx.activity.result.contract.ActivityResultContracts;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.widget.AppCompatTextView;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.core.content.FileProvider;
|
||||
import androidx.exifinterface.media.ExifInterface;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
|
||||
import com.google.android.material.chip.Chip;
|
||||
import com.google.android.material.chip.ChipGroup;
|
||||
import com.google.android.material.color.MaterialColors;
|
||||
@@ -67,20 +81,9 @@ import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.NoSuchElementException;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.Callable;
|
||||
|
||||
import androidx.activity.result.ActivityResultLauncher;
|
||||
import androidx.activity.result.contract.ActivityResultContracts;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.widget.AppCompatTextView;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.core.content.FileProvider;
|
||||
import androidx.exifinterface.media.ExifInterface;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
import protect.card_locker.async.TaskHandler;
|
||||
import protect.card_locker.databinding.LayoutChipChoiceBinding;
|
||||
import protect.card_locker.databinding.LoyaltyCardEditActivityBinding;
|
||||
@@ -376,6 +379,19 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity implements
|
||||
|
||||
addDateFieldTextChangedListener(expiryField, R.string.never, R.string.chooseExpiryDate, LoyaltyCardField.expiry);
|
||||
|
||||
DatePickerFragment.registerDatePickListener(this, (textFieldToEdit, newDate) -> {
|
||||
switch (textFieldToEdit) {
|
||||
case validFrom:
|
||||
formatDateField(this, validFromField, newDate);
|
||||
break;
|
||||
case expiry:
|
||||
formatDateField(this, expiryField, newDate);
|
||||
break;
|
||||
default:
|
||||
throw new AssertionError("Unexpected field: " + textFieldToEdit);
|
||||
}
|
||||
});
|
||||
|
||||
balanceField.setOnFocusChangeListener((v, hasFocus) -> {
|
||||
if (!hasFocus) {
|
||||
balanceField.setText(Utils.formatBalanceWithoutCurrencySymbol(tempLoyaltyCard.balance, tempLoyaltyCard.balanceType));
|
||||
@@ -967,9 +983,9 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity implements
|
||||
if (!lastValue.toString().equals(getString(chooseDateOptionStringId))) {
|
||||
dateField.setText(lastValue);
|
||||
}
|
||||
DialogFragment datePickerFragment = new DatePickerFragment(
|
||||
LoyaltyCardEditActivity.this,
|
||||
dateField,
|
||||
DialogFragment datePickerFragment = DatePickerFragment.newInstance(
|
||||
loyaltyCardField,
|
||||
(Date) dateField.getTag(),
|
||||
// if the expiry date is being set, set date picker's minDate to the 'valid from' date
|
||||
loyaltyCardField == LoyaltyCardField.expiry ? (Date) validFromField.getTag() : null,
|
||||
// if the 'valid from' date is being set, set date picker's maxDate to the expiry date
|
||||
@@ -1336,27 +1352,54 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity implements
|
||||
public static class DatePickerFragment extends DialogFragment
|
||||
implements DatePickerDialog.OnDateSetListener {
|
||||
|
||||
final Context context;
|
||||
final EditText textFieldEdit;
|
||||
@Nullable
|
||||
final Date minDate;
|
||||
@Nullable
|
||||
final Date maxDate;
|
||||
public interface OnDatePickListener {
|
||||
void onDatePicked(@NonNull LoyaltyCardField textFieldToEdit, @NonNull Date newDate);
|
||||
}
|
||||
|
||||
DatePickerFragment(Context context, EditText textFieldEdit, @Nullable Date minDate, @Nullable Date maxDate) {
|
||||
this.context = context;
|
||||
this.textFieldEdit = textFieldEdit;
|
||||
this.minDate = minDate;
|
||||
this.maxDate = maxDate;
|
||||
private static final String TEXT_FIELD_TO_EDIT_ARGUMENT_KEY = "text_field_to_edit";
|
||||
private static final String CURRENT_DATE_ARGUMENT_KEY = "current_date";
|
||||
private static final String MIN_DATE_ARGUMENT_KEY = "min_date";
|
||||
private static final String MAX_DATE_ARGUMENT_KEY = "max_date";
|
||||
private static final String PICK_DATE_REQUEST_KEY = "pick_date_request";
|
||||
private static final String NEWLY_PICKED_DATE_ARGUMENT_KEY = "newly_picked_date";
|
||||
|
||||
LoyaltyCardField textFieldEdit;
|
||||
@Nullable
|
||||
Date minDate;
|
||||
@Nullable
|
||||
Date maxDate;
|
||||
|
||||
public static DatePickerFragment newInstance(@NonNull LoyaltyCardField textField, @Nullable Date currentDate, @Nullable Date minDate, @Nullable Date maxDate) {
|
||||
Bundle args = new Bundle();
|
||||
args.putSerializable(TEXT_FIELD_TO_EDIT_ARGUMENT_KEY, textField);
|
||||
args.putSerializable(CURRENT_DATE_ARGUMENT_KEY, currentDate);
|
||||
args.putSerializable(MIN_DATE_ARGUMENT_KEY, minDate);
|
||||
args.putSerializable(MAX_DATE_ARGUMENT_KEY, maxDate);
|
||||
DatePickerFragment fragment = new DatePickerFragment();
|
||||
fragment.setArguments(args);
|
||||
return fragment;
|
||||
}
|
||||
|
||||
public static void registerDatePickListener(@NonNull AppCompatActivity activity, @NonNull OnDatePickListener listener) {
|
||||
activity.getSupportFragmentManager().setFragmentResultListener(
|
||||
PICK_DATE_REQUEST_KEY,
|
||||
activity,
|
||||
(requestKey, result) -> listener.onDatePicked(
|
||||
(LoyaltyCardField) Objects.requireNonNull(result.getSerializable(TEXT_FIELD_TO_EDIT_ARGUMENT_KEY)),
|
||||
(Date) Objects.requireNonNull(result.getSerializable(NEWLY_PICKED_DATE_ARGUMENT_KEY))));
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
||||
Bundle args = requireArguments();
|
||||
textFieldEdit = (LoyaltyCardField) args.getSerializable(TEXT_FIELD_TO_EDIT_ARGUMENT_KEY);
|
||||
minDate = (Date) args.getSerializable(MIN_DATE_ARGUMENT_KEY);
|
||||
maxDate = (Date) args.getSerializable(MAX_DATE_ARGUMENT_KEY);
|
||||
// Use the current date as the default date in the picker
|
||||
final Calendar c = Calendar.getInstance();
|
||||
|
||||
Date date = (Date) textFieldEdit.getTag();
|
||||
Date date = (Date) args.getSerializable(CURRENT_DATE_ARGUMENT_KEY);
|
||||
if (date != null) {
|
||||
c.setTime(date);
|
||||
}
|
||||
@@ -1398,7 +1441,10 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity implements
|
||||
|
||||
Date date = new Date(unixTime);
|
||||
|
||||
formatDateField(context, textFieldEdit, date);
|
||||
Bundle result = new Bundle();
|
||||
result.putSerializable(TEXT_FIELD_TO_EDIT_ARGUMENT_KEY, textFieldEdit);
|
||||
result.putSerializable(NEWLY_PICKED_DATE_ARGUMENT_KEY, date);
|
||||
getParentFragmentManager().setFragmentResult(PICK_DATE_REQUEST_KEY, result);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,16 +11,21 @@ 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;
|
||||
import java.math.BigDecimal;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.text.ParseException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import protect.card_locker.CatimaBarcode;
|
||||
import protect.card_locker.DBHelper;
|
||||
import protect.card_locker.FormatException;
|
||||
import protect.card_locker.LoyaltyCard;
|
||||
import protect.card_locker.Utils;
|
||||
|
||||
/**
|
||||
@@ -31,7 +36,16 @@ 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 static class ImportedData {
|
||||
public final List<LoyaltyCard> cards;
|
||||
|
||||
ImportedData(final List<LoyaltyCard> cards) {
|
||||
this.cards = cards;
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
@@ -54,10 +68,14 @@ public class FidmeImporter implements Importer {
|
||||
}
|
||||
|
||||
final CSVParser fidmeParser = new CSVParser(new StringReader(loyaltyCards.toString()), CSVFormat.RFC4180.builder().setDelimiter(';').setHeader().build());
|
||||
ImportedData importedData = new ImportedData(new ArrayList<>());
|
||||
|
||||
try {
|
||||
for (CSVRecord record : fidmeParser) {
|
||||
importLoyaltyCard(context, database, record);
|
||||
LoyaltyCard card = importLoyaltyCard(context, record);
|
||||
if (card != null) {
|
||||
importedData.cards.add(card);
|
||||
}
|
||||
|
||||
if (Thread.currentThread().isInterrupted()) {
|
||||
throw new InterruptedException();
|
||||
@@ -70,14 +88,16 @@ public class FidmeImporter implements Importer {
|
||||
}
|
||||
|
||||
zipInputStream.close();
|
||||
input.close();
|
||||
|
||||
saveAndDeduplicate(database, importedData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Import a single loyalty card into the database using the given
|
||||
* session.
|
||||
*/
|
||||
private void importLoyaltyCard(Context context, SQLiteDatabase database, CSVRecord record)
|
||||
throws FormatException {
|
||||
private LoyaltyCard importLoyaltyCard(Context context, CSVRecord record) throws FormatException {
|
||||
// A loyalty card export from Fidme contains the following fields:
|
||||
// Retailer (store name)
|
||||
// Program (program name)
|
||||
@@ -113,7 +133,7 @@ public class FidmeImporter implements Importer {
|
||||
// Fidme deletes the card id if a card is expired
|
||||
// Because Catima considers the card id a required field, we ignore these expired cards
|
||||
// https://github.com/CatimaLoyalty/Android/issues/1005
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
|
||||
// Sadly, Fidme exports don't contain the card type
|
||||
@@ -128,6 +148,17 @@ public class FidmeImporter implements Importer {
|
||||
|
||||
// TODO: Front and back image
|
||||
|
||||
DBHelper.insertLoyaltyCard(database, store, note, null, null, BigDecimal.valueOf(0), null, cardId, null, barcodeType, headerColor, starStatus, null,archiveStatus);
|
||||
// use -1 for the ID, it will be ignored when inserting the card into the DB
|
||||
return new LoyaltyCard(-1, store, note, null, null, BigDecimal.valueOf(0), null, cardId, null, barcodeType, headerColor, starStatus, Utils.getUnixTime(), DBHelper.DEFAULT_ZOOM_LEVEL, archiveStatus);
|
||||
}
|
||||
|
||||
public void saveAndDeduplicate(SQLiteDatabase database, final ImportedData data) {
|
||||
// This format does not have IDs that can cause conflicts
|
||||
// Proper deduplication for all formats will be implemented later
|
||||
for (LoyaltyCard card : data.cards) {
|
||||
// Do not use card.id which is set to -1
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -13,21 +13,29 @@ import net.lingala.zip4j.model.LocalFileHeader;
|
||||
import org.apache.commons.csv.CSVFormat;
|
||||
import org.apache.commons.csv.CSVParser;
|
||||
import org.apache.commons.csv.CSVRecord;
|
||||
import org.json.JSONArray;
|
||||
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;
|
||||
import java.math.BigDecimal;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.text.ParseException;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import protect.card_locker.CatimaBarcode;
|
||||
import protect.card_locker.DBHelper;
|
||||
import protect.card_locker.FormatException;
|
||||
import protect.card_locker.ImageLocationType;
|
||||
import protect.card_locker.LoyaltyCard;
|
||||
import protect.card_locker.R;
|
||||
import protect.card_locker.Utils;
|
||||
import protect.card_locker.ZipUtils;
|
||||
@@ -40,11 +48,30 @@ import protect.card_locker.ZipUtils;
|
||||
* A header is expected for the each table showing the names of the columns.
|
||||
*/
|
||||
public class StocardImporter implements Importer {
|
||||
public static class ZIPData {
|
||||
public final HashMap<String, HashMap<String, Object>> loyaltyCardHashMap;
|
||||
public final HashMap<String, HashMap<String, Object>> providers;
|
||||
|
||||
ZIPData(final HashMap<String, HashMap<String, Object>> loyaltyCardHashMap, final HashMap<String, HashMap<String, Object>> providers) {
|
||||
this.loyaltyCardHashMap = loyaltyCardHashMap;
|
||||
this.providers = providers;
|
||||
}
|
||||
}
|
||||
|
||||
public static class ImportedData {
|
||||
public final List<LoyaltyCard> cards;
|
||||
public final Map<Integer, Map<ImageLocationType, Bitmap>> images;
|
||||
|
||||
ImportedData(final List<LoyaltyCard> cards, final Map<Integer, Map<ImageLocationType, Bitmap>> images) {
|
||||
this.cards = cards;
|
||||
this.images = images;
|
||||
}
|
||||
}
|
||||
|
||||
private static final String TAG = "Catima";
|
||||
|
||||
public void importData(Context context, SQLiteDatabase database, InputStream input, char[] password) throws IOException, FormatException, JSONException, ParseException {
|
||||
HashMap<String, HashMap<String, Object>> loyaltyCardHashMap = new HashMap<>();
|
||||
HashMap<String, HashMap<String, Object>> providers = new HashMap<>();
|
||||
public void importData(Context context, SQLiteDatabase database, File inputFile, char[] password) throws IOException, FormatException, JSONException, ParseException {
|
||||
ZIPData zipData = new ZIPData(new HashMap<>(), new HashMap<>());
|
||||
|
||||
final CSVParser parser = new CSVParser(new InputStreamReader(context.getResources().openRawResource(R.raw.stocard_stores), StandardCharsets.UTF_8), CSVFormat.RFC4180.builder().setHeader().build());
|
||||
|
||||
@@ -54,7 +81,7 @@ public class StocardImporter implements Importer {
|
||||
recordData.put("name", record.get("name"));
|
||||
recordData.put("barcodeFormat", record.get("barcodeFormat"));
|
||||
|
||||
providers.put(record.get("_id"), recordData);
|
||||
zipData.providers.put(record.get("_id"), recordData);
|
||||
}
|
||||
|
||||
parser.close();
|
||||
@@ -62,7 +89,23 @@ public class StocardImporter implements Importer {
|
||||
throw new FormatException("Issue parsing CSV data", e);
|
||||
}
|
||||
|
||||
InputStream input = new FileInputStream(inputFile);
|
||||
ZipInputStream zipInputStream = new ZipInputStream(input, password);
|
||||
zipData = importZIP(zipInputStream, zipData);
|
||||
zipInputStream.close();
|
||||
input.close();
|
||||
|
||||
if (zipData.loyaltyCardHashMap.keySet().size() == 0) {
|
||||
throw new FormatException("Couldn't find any loyalty cards in this Stocard export.");
|
||||
}
|
||||
|
||||
ImportedData importedData = importLoyaltyCardHashMap(context, zipData);
|
||||
saveAndDeduplicate(context, database, importedData);
|
||||
}
|
||||
|
||||
public ZIPData importZIP(ZipInputStream zipInputStream, final ZIPData zipData) throws IOException, JSONException {
|
||||
HashMap<String, HashMap<String, Object>> loyaltyCardHashMap = zipData.loyaltyCardHashMap;
|
||||
HashMap<String, HashMap<String, Object>> providers = zipData.providers;
|
||||
|
||||
String[] providersFileName = null;
|
||||
String[] customProvidersBaseName = null;
|
||||
@@ -142,6 +185,15 @@ public class StocardImporter implements Importer {
|
||||
jsonObject.getString("input_id")
|
||||
);
|
||||
|
||||
if (jsonObject.has("input_provider_name")) {
|
||||
loyaltyCardHashMap = appendToHashMap(
|
||||
loyaltyCardHashMap,
|
||||
cardName,
|
||||
"store",
|
||||
jsonObject.getString("input_provider_name")
|
||||
);
|
||||
}
|
||||
|
||||
// Provider ID can be either custom or not, extract whatever version is relevant
|
||||
String customProviderPrefix = "/users/" + nameParts[1] + "/loyalty-card-custom-providers/";
|
||||
String providerId = jsonObject
|
||||
@@ -176,14 +228,28 @@ public class StocardImporter implements Importer {
|
||||
ZipUtils.readJSON(zipInputStream)
|
||||
.getString("content")
|
||||
);
|
||||
} else if (fileName.endsWith("/images/front.png")) {
|
||||
} else if (fileName.endsWith("usage-statistics/content.json")) {
|
||||
JSONArray usages = ZipUtils.readJSON(zipInputStream).getJSONArray("usages");
|
||||
if (usages.length() > 0) {
|
||||
JSONObject lastUsedObject = usages.getJSONObject(usages.length() - 1);
|
||||
String lastUsedString = lastUsedObject.getJSONObject("time").getString("value");
|
||||
long timeStamp = Instant.parse(lastUsedString).getEpochSecond();
|
||||
|
||||
loyaltyCardHashMap = appendToHashMap(
|
||||
loyaltyCardHashMap,
|
||||
cardName,
|
||||
"lastUsed",
|
||||
timeStamp
|
||||
);
|
||||
}
|
||||
} else if (fileName.endsWith("/images/front.png") || fileName.endsWith("/images/front/front.jpg")) {
|
||||
loyaltyCardHashMap = appendToHashMap(
|
||||
loyaltyCardHashMap,
|
||||
cardName,
|
||||
"frontImage",
|
||||
ZipUtils.readImage(zipInputStream)
|
||||
);
|
||||
} else if (fileName.endsWith("/images/back.png")) {
|
||||
} else if (fileName.endsWith("/images/back.png") || fileName.endsWith("/images/back/back.jpg")) {
|
||||
loyaltyCardHashMap = appendToHashMap(
|
||||
loyaltyCardHashMap,
|
||||
cardName,
|
||||
@@ -194,11 +260,14 @@ public class StocardImporter implements Importer {
|
||||
}
|
||||
}
|
||||
|
||||
if (loyaltyCardHashMap.keySet().size() == 0) {
|
||||
throw new FormatException("Couldn't find any loyalty cards in this Stocard export.");
|
||||
}
|
||||
return new ZIPData(loyaltyCardHashMap, providers);
|
||||
}
|
||||
|
||||
for (HashMap<String, Object> loyaltyCardData : loyaltyCardHashMap.values()) {
|
||||
public ImportedData importLoyaltyCardHashMap(Context context, final ZIPData zipData) {
|
||||
ImportedData importedData = new ImportedData(new ArrayList<>(), new HashMap<>());
|
||||
int tempID = 0;
|
||||
|
||||
for (Map<String, Object> loyaltyCardData : zipData.loyaltyCardHashMap.values()) {
|
||||
String providerId = (String) loyaltyCardData.get("_providerId");
|
||||
|
||||
if (providerId == null) {
|
||||
@@ -206,9 +275,16 @@ public class StocardImporter implements Importer {
|
||||
continue;
|
||||
}
|
||||
|
||||
HashMap<String, Object> providerData = providers.get(providerId);
|
||||
HashMap<String, Object> providerData = zipData.providers.get(providerId);
|
||||
|
||||
// Read store from card, if not available (old export), fall back to providerData
|
||||
String store;
|
||||
if (loyaltyCardData.containsKey("store")) {
|
||||
store = (String) loyaltyCardData.get("store");
|
||||
} else {
|
||||
store = providerData != null ? providerData.get("name").toString() : providerId;
|
||||
}
|
||||
|
||||
String store = providerData != null ? providerData.get("name").toString() : providerId;
|
||||
String note = (String) Utils.mapGetOrDefault(loyaltyCardData, "note", "");
|
||||
String cardId = (String) loyaltyCardData.get("cardId");
|
||||
String barcodeTypeString = (String) Utils.mapGetOrDefault(loyaltyCardData, "barcodeType", providerData != null ? providerData.get("barcodeFormat") : null);
|
||||
@@ -230,21 +306,46 @@ public class StocardImporter implements Importer {
|
||||
headerColor = Utils.getHeaderColorFromImage(cardIcon, headerColor);
|
||||
}
|
||||
|
||||
long loyaltyCardInternalId = DBHelper.insertLoyaltyCard(database, store, note, null, null, BigDecimal.valueOf(0), null, cardId, null, barcodeType, headerColor, 0, null,0);
|
||||
long lastUsed;
|
||||
if (loyaltyCardData.containsKey("lastUsed")) {
|
||||
lastUsed = (long) loyaltyCardData.get("lastUsed");
|
||||
} else {
|
||||
lastUsed = Utils.getUnixTime();
|
||||
}
|
||||
|
||||
LoyaltyCard card = new LoyaltyCard(tempID, store, note, null, null, BigDecimal.valueOf(0), null, cardId, null, barcodeType, headerColor, 0, lastUsed, DBHelper.DEFAULT_ZOOM_LEVEL, 0);
|
||||
importedData.cards.add(card);
|
||||
|
||||
Map<ImageLocationType, Bitmap> images = new HashMap<>();
|
||||
|
||||
if (cardIcon != null) {
|
||||
Utils.saveCardImage(context, cardIcon, (int) loyaltyCardInternalId, ImageLocationType.icon);
|
||||
images.put(ImageLocationType.icon, cardIcon);
|
||||
}
|
||||
|
||||
if (loyaltyCardData.containsKey("frontImage")) {
|
||||
Utils.saveCardImage(context, (Bitmap) loyaltyCardData.get("frontImage"), (int) loyaltyCardInternalId, ImageLocationType.front);
|
||||
images.put(ImageLocationType.front, (Bitmap) loyaltyCardData.get("frontImage"));
|
||||
}
|
||||
if (loyaltyCardData.containsKey("backImage")) {
|
||||
Utils.saveCardImage(context, (Bitmap) loyaltyCardData.get("backImage"), (int) loyaltyCardInternalId, ImageLocationType.back);
|
||||
images.put(ImageLocationType.back, (Bitmap) loyaltyCardData.get("backImage"));
|
||||
}
|
||||
|
||||
importedData.images.put(tempID, images);
|
||||
tempID++;
|
||||
}
|
||||
|
||||
zipInputStream.close();
|
||||
return importedData;
|
||||
}
|
||||
|
||||
public void saveAndDeduplicate(Context context, SQLiteDatabase database, final ImportedData data) throws IOException {
|
||||
// This format does not have IDs that can cause conflicts
|
||||
// Proper deduplication for all formats will be implemented later
|
||||
for (LoyaltyCard card : data.cards) {
|
||||
// card.id is temporary and only used to index the images Map
|
||||
long id = 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);
|
||||
for (Map.Entry<ImageLocationType, Bitmap> entry : data.images.get(card.id).entrySet()) {
|
||||
Utils.saveCardImage(context, entry.getValue(), (int) id, entry.getKey());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean startsWith(String[] full, String[] start, int minExtraLength) {
|
||||
@@ -272,4 +373,4 @@ public class StocardImporter implements Importer {
|
||||
|
||||
return loyaltyCardHashMap;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -19,13 +21,16 @@ import java.math.BigDecimal;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.text.ParseException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Currency;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.TimeZone;
|
||||
|
||||
import protect.card_locker.CatimaBarcode;
|
||||
import protect.card_locker.DBHelper;
|
||||
import protect.card_locker.FormatException;
|
||||
import protect.card_locker.LoyaltyCard;
|
||||
import protect.card_locker.Utils;
|
||||
|
||||
/**
|
||||
@@ -36,7 +41,16 @@ 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 static class ImportedData {
|
||||
public final List<LoyaltyCard> cards;
|
||||
|
||||
ImportedData(final List<LoyaltyCard> cards) {
|
||||
this.cards = cards;
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
@@ -46,6 +60,16 @@ public class VoucherVaultImporter implements Importer {
|
||||
}
|
||||
JSONArray jsonArray = new JSONArray(sb.toString());
|
||||
|
||||
bufferedReader.close();
|
||||
input.close();
|
||||
|
||||
ImportedData importedData = importJSON(jsonArray);
|
||||
saveAndDeduplicate(database, importedData);
|
||||
}
|
||||
|
||||
public ImportedData importJSON(JSONArray jsonArray) throws FormatException, JSONException, ParseException {
|
||||
ImportedData importedData = new ImportedData(new ArrayList<>());
|
||||
|
||||
// See https://github.com/tim-smart/vouchervault/issues/4#issuecomment-788226503 for more info
|
||||
for (int i = 0; i < jsonArray.length(); i++) {
|
||||
JSONObject jsonCard = jsonArray.getJSONObject(i);
|
||||
@@ -126,9 +150,20 @@ public class VoucherVaultImporter implements Importer {
|
||||
throw new FormatException("Unknown colour type found: " + colorFromJSON);
|
||||
}
|
||||
|
||||
DBHelper.insertLoyaltyCard(database, store, "", null, expiry, balance, balanceType, cardId, null, barcodeType, headerColor, 0, Utils.getUnixTime(),0);
|
||||
// use -1 for the ID, it will be ignored when inserting the card into the DB
|
||||
importedData.cards.add(new LoyaltyCard(-1, store, "", null, expiry, balance, balanceType, cardId, null, barcodeType, headerColor, 0, Utils.getUnixTime(), DBHelper.DEFAULT_ZOOM_LEVEL, 0));
|
||||
}
|
||||
|
||||
bufferedReader.close();
|
||||
return importedData;
|
||||
}
|
||||
|
||||
public void saveAndDeduplicate(SQLiteDatabase database, final ImportedData data) {
|
||||
// This format does not have IDs that can cause conflicts
|
||||
// Proper deduplication for all formats will be implemented later
|
||||
for (LoyaltyCard card : data.cards) {
|
||||
// Do not use card.id which is set to -1
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,10 +2,11 @@
|
||||
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
style="?attr/materialCardViewElevatedStyle"
|
||||
android:id="@+id/row"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="8dp">
|
||||
android:layout_margin="5dp">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
@@ -137,7 +138,6 @@
|
||||
android:layout_marginEnd="8dp"
|
||||
android:layout_marginBottom="4dp"
|
||||
android:textAppearance="?attr/textAppearanceHeadlineSmall"
|
||||
android:textColor="?android:attr/textColorSecondary"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible"
|
||||
app:layout_constraintTop_toBottomOf="@+id/icon_layout"
|
||||
@@ -154,7 +154,6 @@
|
||||
android:layout_marginEnd="8dp"
|
||||
android:layout_marginBottom="4dp"
|
||||
android:textAppearance="?attr/textAppearanceBody2"
|
||||
android:textColor="?android:attr/textColorSecondary"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible"
|
||||
app:layout_constraintTop_toBottomOf="@+id/store"
|
||||
@@ -187,7 +186,6 @@
|
||||
android:layout_marginEnd="8dp"
|
||||
android:layout_marginBottom="4dp"
|
||||
android:textAppearance="?attr/textAppearanceBody2"
|
||||
android:textColor="?android:attr/textColorSecondary"
|
||||
app:drawableLeftCompat="@drawable/ic_baseline_payments_24"
|
||||
android:drawablePadding="4dp"
|
||||
android:visibility="gone"
|
||||
@@ -207,7 +205,6 @@
|
||||
android:layout_marginEnd="8dp"
|
||||
android:layout_marginBottom="4dp"
|
||||
android:textAppearance="?attr/textAppearanceBody2"
|
||||
android:textColor="?android:attr/textColorSecondary"
|
||||
app:drawableLeftCompat="@drawable/ic_valid_from_24dp"
|
||||
android:drawablePadding="4dp"
|
||||
android:visibility="gone"
|
||||
@@ -227,7 +224,6 @@
|
||||
android:layout_marginEnd="8dp"
|
||||
android:layout_marginBottom="4dp"
|
||||
android:textAppearance="?attr/textAppearanceBody2"
|
||||
android:textColor="?android:attr/textColorSecondary"
|
||||
app:drawableLeftCompat="@drawable/ic_baseline_access_time_24"
|
||||
android:drawablePadding="4dp"
|
||||
android:visibility="gone"
|
||||
|
||||
@@ -10,30 +10,30 @@ mondstern
|
||||
SlavekB
|
||||
StoyanDimitrov
|
||||
IllusiveMan196
|
||||
FC Stegerman
|
||||
Altonss
|
||||
Michael Moroni
|
||||
Gediminas Murauskas
|
||||
Petr Novák
|
||||
Joel A
|
||||
laralem
|
||||
FC Stegerman
|
||||
Taco
|
||||
pfaffenrodt
|
||||
gallegonovato
|
||||
Nyatsuki
|
||||
HudobniVolk
|
||||
Samantaz Fox
|
||||
Eric
|
||||
arno-github
|
||||
Ankit Tiwari
|
||||
Sergio Paredes
|
||||
Clxff H3r4ld0
|
||||
Eric
|
||||
Aayush Gupta
|
||||
huuhaa
|
||||
Balázs Meskó
|
||||
huuhaa
|
||||
Projjal Moitra
|
||||
Quentin PAGÈS
|
||||
Giovanni Donisi
|
||||
Projjal Moitra
|
||||
Alexander Ivanov
|
||||
arshbeerSingh
|
||||
Denis Shilin
|
||||
@@ -45,6 +45,7 @@ Arnis Jaundžeikars
|
||||
Dan
|
||||
sr093906
|
||||
mdvhimself
|
||||
Jiri Grönroos
|
||||
Katarzyna
|
||||
echo r"0xX4H" | rev
|
||||
Magnitudee
|
||||
@@ -57,7 +58,6 @@ enolp
|
||||
Evgeniy Khramov
|
||||
Jane Kong
|
||||
Jean Mareilles
|
||||
Jiri Grönroos
|
||||
José Rebelo
|
||||
K. Herbert
|
||||
Lisa A.
|
||||
@@ -81,6 +81,7 @@ BMN
|
||||
balaraz
|
||||
BootVirtual
|
||||
Bottan Hermawan
|
||||
zChiip
|
||||
Clonewayx
|
||||
D. Domig
|
||||
Diego
|
||||
@@ -185,6 +186,7 @@ Mateo Gomez
|
||||
Mattia
|
||||
Md. Al-Amin
|
||||
Michael Gangolf
|
||||
Milan Šalka
|
||||
3DN1M
|
||||
Minecraft boom
|
||||
Mobashir Raihan
|
||||
@@ -200,6 +202,7 @@ vandman
|
||||
Piotr Strebski
|
||||
Piotr Zet
|
||||
Poorva Patidar
|
||||
Quang Trung
|
||||
Quang Nguyen
|
||||
Ratnesh
|
||||
Reza
|
||||
@@ -217,6 +220,7 @@ Subhradeep Bera
|
||||
Swayam Khare
|
||||
SziaTomi
|
||||
Mehedi Hasan
|
||||
Tim Trek
|
||||
Titas Pažereckas
|
||||
atakujonc
|
||||
tkraljevic
|
||||
@@ -224,6 +228,7 @@ Tony C
|
||||
Vancha March
|
||||
tyap-lyap-ivprod
|
||||
Waldemar Stoczkowski
|
||||
Wiktor Kwapisiewicz
|
||||
Yevgeny M
|
||||
Yusril A
|
||||
Ziad OUALHADJ
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
# stocard_stores.csv
|
||||
|
||||
stocard_stores.csv was created by extracting /data/data/de.stocard/de.stocard.stocard/databases/stores on a rooted devices and running the following command over it:
|
||||
|
||||
```
|
||||
sqlite3 -header -csv sync_db "select id,content from synced_resources where collection = '/loyalty-card-providers/'" > stocard_providers.csv
|
||||
while IFS= read -r line; do
|
||||
if [ "$line" = "id,content" ]; then
|
||||
echo "_id,name,barcodeFormat" > stocard_stores.csv
|
||||
else
|
||||
id="$(echo "$line" | cut -d ',' -f1)"
|
||||
name="$(echo "$line" | cut -d ',' -f2- | sed 's/""/"/g' | sed 's/^"//g' | sed 's/"$//g' | jq -r .name)"
|
||||
barcodeFormat="$(echo "$line" | cut -d ',' -f2- | sed 's/""/"/g' | sed 's/^"//g' | sed 's/"$//g' | jq -r .default_barcode_format)"
|
||||
echo "$id,\"$name\",$barcodeFormat" >> stocard_stores.csv
|
||||
fi
|
||||
done < stocard_providers.csv
|
||||
```
|
||||
|
||||
Only used for data portability reasons (ensuring importing works). Do NOT copy this anywhere else or use it for any purpose other than ensuring we can import a GDPR-provided export. We want to make sure this stays under fair use.
|
||||
File diff suppressed because it is too large
Load Diff
@@ -270,7 +270,7 @@
|
||||
<string name="donate">Spenden</string>
|
||||
<string name="show_note">Notiz anzeigen</string>
|
||||
<string name="show_balance">Betrag anzeigen</string>
|
||||
<string name="show_validity">Gültigkeitsdauer anziegen</string>
|
||||
<string name="show_validity">Gültigkeitsdauer anzeigen</string>
|
||||
<string name="show_name_below_image_thumbnail">Namen unter Bildvorschau anzeigen</string>
|
||||
<string name="settings_allow_content_provider_read_title">Anderen Anwendungen den Zugriff auf meine Daten gestatten</string>
|
||||
<string name="permissionReadCardsLabel">Catima-Karten lesen</string>
|
||||
|
||||
@@ -188,7 +188,7 @@
|
||||
<item quantity="other"><xliff:g>%s</xliff:g> pistettä</item>
|
||||
</plurals>
|
||||
<string name="settings_oled_dark">Musta tausta tummalle teemalle</string>
|
||||
<string name="setIcon">Aseta kuvake</string>
|
||||
<string name="setIcon">Aseta pikkukuva</string>
|
||||
<string name="sort_by_name">Nimi</string>
|
||||
<string name="sort_by_most_recently_used">Viimeksi käytetyt</string>
|
||||
<string name="sort_by_expiry">Viimeinen voimassaoloaika</string>
|
||||
@@ -259,4 +259,28 @@
|
||||
<string name="anyDate">Mikä tahansa päivämäärä</string>
|
||||
<string name="chooseValidFromDate">Valitse kelvollinen päivämäärä</string>
|
||||
<string name="validFromSentence">Kelvollinen alkaen: <xliff:g>%s</xliff:g></string>
|
||||
</resources>
|
||||
<string name="donate">Lahjoita</string>
|
||||
<string name="permissionReadCardsLabel">Lue Catima-kortteja</string>
|
||||
<string name="permissionReadCardsDescription">Lue korttisi ja kaikki niiden tiedot, mukaan lukien huomautukset ja kuvat</string>
|
||||
<string name="settings_allow_content_provider_read_summary">Sovellusten tulee silti pyytää lupaa saadakseen pääsyn</string>
|
||||
<string name="settings_category_title_privacy">Yksityisyys</string>
|
||||
<string name="height">Korkeus:</string>
|
||||
<string name="switchToFrontImage">Vaihda etukuvaan</string>
|
||||
<string name="switchToBarcode">Vaihda viivakoodiin</string>
|
||||
<string name="openFrontImageInGalleryApp">Avaa etukuva galleriasovelluksessa</string>
|
||||
<string name="settings_display_barcode_max_brightness_summary">Välttämätön, jotta jotkin skannerit toimivat</string>
|
||||
<string name="settings_keep_screen_on_summary">Poistaa näytön aikakatkaisun korttia katsellessa</string>
|
||||
<string name="settings_disable_lockscreen_while_viewing_card_summary">Poistaa näyttölukituksen käytöstä korttia katsellessa</string>
|
||||
<string name="settings_allow_content_provider_read_title">Salli muiden sovellusten käyttää omaa dataa</string>
|
||||
<string name="settings_oled_dark_summary">Vähentää akun käyttöä OLED-näytöillä</string>
|
||||
<string name="switchToBackImage">Vaihda takakuvaan</string>
|
||||
<string name="openBackImageInGalleryApp">Avaa takakuva galleriasovelluksessa</string>
|
||||
<string name="setBarcodeHeight">Aseta viivakoodin korkeus</string>
|
||||
<string name="icon_header_click_text">Pitkä painallus pikkukuvan muokkaamiseksi</string>
|
||||
<string name="show_name_below_image_thumbnail">Näytä nimi pikkukuvan alapuolella</string>
|
||||
<string name="show_note">Näytä huomautus</string>
|
||||
<string name="show_balance">Näytä saldo</string>
|
||||
<string name="show_validity">Näytä kelpoisuus</string>
|
||||
<string name="settings_category_title_cards">Kortit</string>
|
||||
<string name="settings_category_title_general">Yleiset</string>
|
||||
</resources>
|
||||
2
app/src/main/res/values-lzh/strings.xml
Normal file
2
app/src/main/res/values-lzh/strings.xml
Normal file
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources></resources>
|
||||
@@ -282,7 +282,19 @@
|
||||
<string name="donate">Darowizna</string>
|
||||
<string name="openBackImageInGalleryApp">Otwórz obraz z powrotem w aplikacji galerii</string>
|
||||
<string name="icon_header_click_text">Przytrzymaj, aby edytować miniaturę</string>
|
||||
<string name="show_name_below_image_thumbnail">Pokaż imię pod miniaturką zdjęcia</string>
|
||||
<string name="show_name_below_image_thumbnail">Pokaż nazwę pod miniaturką zdjęcia</string>
|
||||
<string name="show_balance">Pokaż balans</string>
|
||||
<string name="show_validity">Pokaż ważność</string>
|
||||
</resources>
|
||||
<string name="show_note">Pokaż notatkę</string>
|
||||
<string name="permissionReadCardsLabel">Odczytaj Karty Catima</string>
|
||||
<string name="permissionReadCardsDescription">Odczytaj swoje karty i ich szczegóły, włącznie z notatkami i obrazkami</string>
|
||||
<string name="settings_allow_content_provider_read_title">Pozwól innym aplikacjom na dostęp do moich danych</string>
|
||||
<string name="settings_display_barcode_max_brightness_summary">Potrzebne aby niektóre skanery działały</string>
|
||||
<string name="settings_keep_screen_on_summary">Wyłącza wygaszanie ekranu kiedy wyświetlana jest karta</string>
|
||||
<string name="settings_disable_lockscreen_while_viewing_card_summary">Wyłącza blokadę ekranu kiedy wyświetlana jest karta</string>
|
||||
<string name="settings_allow_content_provider_read_summary">Aplikacje będą wymagały pozwolenia aby otrzymać dostęp do danych</string>
|
||||
<string name="settings_oled_dark_summary">Zmniejsza zużycie baterii na wyświetlaczach OLED</string>
|
||||
<string name="settings_category_title_cards">Karty</string>
|
||||
<string name="settings_category_title_general">Ogólne</string>
|
||||
<string name="settings_category_title_privacy">Prywatność</string>
|
||||
</resources>
|
||||
@@ -150,7 +150,7 @@
|
||||
<string name="yes">Áno</string>
|
||||
<string name="importStocard">Import z aplikácie Stocard</string>
|
||||
<string name="selectColor">Vybrať farbu</string>
|
||||
<string name="setIcon">Nastaviť ikonu</string>
|
||||
<string name="setIcon">Nastavenie miniatúry</string>
|
||||
<string name="settings_catima_theme">Catima</string>
|
||||
<string name="settings_pink_theme">Ružová</string>
|
||||
<string name="settings_magenta_theme">Purpurová</string>
|
||||
@@ -266,4 +266,28 @@
|
||||
<string name="noUnarchivedCardsMessage">Nie sú žiadne karty vrátené z archívu</string>
|
||||
<string name="newBalanceSentence">Nový zostatok: <xliff:g>%s</xliff:g></string>
|
||||
<string name="failedLaunchingPhotoPicker">Nepodarilo sa nájsť podporovanú aplikáciu galérie</string>
|
||||
</resources>
|
||||
<string name="show_note">Zobraziť poznámku</string>
|
||||
<string name="icon_header_click_text">Dlhým stlačením upravíte miniatúru</string>
|
||||
<string name="settings_category_title_general">Všeobecné</string>
|
||||
<string name="settings_category_title_privacy">Súkromie</string>
|
||||
<string name="settings_keep_screen_on_summary">Zakázanie časového limitu obrazovky počas prezerania karty</string>
|
||||
<string name="settings_display_barcode_max_brightness_summary">Potrebné pre fungovanie niektorých skenerov</string>
|
||||
<string name="settings_allow_content_provider_read_summary">Aplikácie budú musieť stále žiadať o povolenie, aby im bol udelený prístup</string>
|
||||
<string name="openBackImageInGalleryApp">Otvorenie spätného obrázka v aplikácii galéria</string>
|
||||
<string name="openFrontImageInGalleryApp">Otvorenie predného obrázka v aplikácii galéria</string>
|
||||
<string name="setBarcodeHeight">Nastavenie výšky čiarového kódu</string>
|
||||
<string name="show_balance">Ukážte rovnováhu</string>
|
||||
<string name="show_name_below_image_thumbnail">Zobraziť názov pod miniatúrou obrázka</string>
|
||||
<string name="show_validity">Zobraziť platnosť</string>
|
||||
<string name="permissionReadCardsLabel">Načítať Catima karty</string>
|
||||
<string name="permissionReadCardsDescription">Čítanie kariet a všetkých ich detailov vrátane poznámok a obrázkov</string>
|
||||
<string name="switchToBackImage">Prepnutie na zadný obrázok</string>
|
||||
<string name="height">Výška:</string>
|
||||
<string name="switchToFrontImage">Prepnutie na predný obrázok</string>
|
||||
<string name="settings_disable_lockscreen_while_viewing_card_summary">Zakázanie uzamknutia obrazovky počas prezerania karty</string>
|
||||
<string name="settings_allow_content_provider_read_title">Povolenie prístupu k mojim údajom iným aplikáciám</string>
|
||||
<string name="settings_oled_dark_summary">Znižuje spotrebu batérie na displejoch OLED</string>
|
||||
<string name="switchToBarcode">Prepnutie na čiarový kód</string>
|
||||
<string name="settings_category_title_cards">Karty</string>
|
||||
<string name="donate">Darujte</string>
|
||||
</resources>
|
||||
@@ -286,4 +286,15 @@
|
||||
<string name="show_note">Показати примітку</string>
|
||||
<string name="show_validity">Показати термін дії</string>
|
||||
<string name="show_balance">Показати баланс</string>
|
||||
</resources>
|
||||
<string name="permissionReadCardsLabel">Читати карти Catima</string>
|
||||
<string name="settings_allow_content_provider_read_summary">Додатки все ще муситимуть запитувати дозвіл на отримання доступу</string>
|
||||
<string name="permissionReadCardsDescription">Читати всі ваші карти та їх деталі, в тому числі нотатки та зображення</string>
|
||||
<string name="settings_display_barcode_max_brightness_summary">Необхідно для роботи деяких сканерів</string>
|
||||
<string name="settings_keep_screen_on_summary">Вимикає тайм-аут екрана під час перегляду картки</string>
|
||||
<string name="settings_disable_lockscreen_while_viewing_card_summary">Вимикає блокування екрана під час перегляду картки</string>
|
||||
<string name="settings_allow_content_provider_read_title">Дозволити іншим програмам доступ до моїх даних</string>
|
||||
<string name="settings_oled_dark_summary">Зменшує використання батареї на екранах з OLED</string>
|
||||
<string name="settings_category_title_cards">Картки</string>
|
||||
<string name="settings_category_title_general">Загальні</string>
|
||||
<string name="settings_category_title_privacy">Конфіденційність</string>
|
||||
</resources>
|
||||
@@ -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;
|
||||
@@ -755,8 +818,9 @@ public class ImportExportTest {
|
||||
@Test
|
||||
public void exportImportV2Zip() throws FileNotFoundException {
|
||||
// Prepare images
|
||||
Bitmap bitmap1 = new LetterBitmap(activity.getApplicationContext(), "1", "1", 12, 64, 64, Color.BLACK, Color.YELLOW).getLetterTile();
|
||||
Bitmap bitmap2 = new LetterBitmap(activity.getApplicationContext(), "2", "2", 12, 64, 64, Color.GREEN, Color.WHITE).getLetterTile();
|
||||
// NB: we can't use LetterBitmap as robolectric doesn't support Canvas enough
|
||||
Bitmap bitmap1 = BitmapFactory.decodeStream(getClass().getResourceAsStream("stocard-front.jpg"));
|
||||
Bitmap bitmap2 = BitmapFactory.decodeStream(getClass().getResourceAsStream("stocard-back.jpg"));
|
||||
|
||||
// Set up cards and groups
|
||||
HashMap<Integer, LoyaltyCard> loyaltyCardHashMap = new HashMap<>();
|
||||
@@ -1061,10 +1125,6 @@ public class ImportExportTest {
|
||||
|
||||
@Test
|
||||
public void importStocard() {
|
||||
// FIXME: The provided stocard.zip is a very old export (8 July 2021) manually edited to
|
||||
// look more like the Stocard files provided by users for #1242. It is not an up-to-date
|
||||
// export and the test is possibly unreliable. This should be replaced by an up-to-date
|
||||
// export.
|
||||
InputStream inputStream = getClass().getResourceAsStream("stocard.zip");
|
||||
|
||||
// Import the Stocard data
|
||||
@@ -1074,7 +1134,7 @@ public class ImportExportTest {
|
||||
|
||||
inputStream = getClass().getResourceAsStream("stocard.zip");
|
||||
|
||||
result = MultiFormatImporter.importData(activity.getApplicationContext(), mDatabase, inputStream, DataFormat.Stocard, "da811b40a4dac56f0cbb2d99b21bbb9a".toCharArray());
|
||||
result = MultiFormatImporter.importData(activity.getApplicationContext(), mDatabase, inputStream, DataFormat.Stocard, "sE0p0RiFDteqhlD4adwWpwjvmI0r0CFOTfyzRae4vEsgNe3NKL".toCharArray());
|
||||
assertEquals(ImportExportResultType.Success, result.resultType());
|
||||
assertEquals(3, DBHelper.getLoyaltyCardCount(mDatabase));
|
||||
|
||||
@@ -1090,6 +1150,7 @@ public class ImportExportTest {
|
||||
assertEquals(null, card.barcodeId);
|
||||
assertEquals(BarcodeFormat.EAN_13, card.barcodeType.format());
|
||||
assertEquals(0, card.starStatus);
|
||||
assertEquals(1625600883, card.lastUsed);
|
||||
|
||||
assertNull(Utils.retrieveCardImage(activity.getApplicationContext(), 1, ImageLocationType.front));
|
||||
assertNull(Utils.retrieveCardImage(activity.getApplicationContext(), 1, ImageLocationType.back));
|
||||
@@ -1107,6 +1168,7 @@ public class ImportExportTest {
|
||||
assertEquals(null, card.barcodeId);
|
||||
assertEquals(BarcodeFormat.EAN_13, card.barcodeType.format());
|
||||
assertEquals(0, card.starStatus);
|
||||
assertEquals(1625690099, card.lastUsed);
|
||||
|
||||
assertTrue(BitmapFactory.decodeStream(getClass().getResourceAsStream("stocard-front.jpg")).sameAs(Utils.retrieveCardImage(activity.getApplicationContext(), 2, ImageLocationType.front)));
|
||||
assertTrue(BitmapFactory.decodeStream(getClass().getResourceAsStream("stocard-back.jpg")).sameAs(Utils.retrieveCardImage(activity.getApplicationContext(), 2, ImageLocationType.back)));
|
||||
@@ -1124,6 +1186,7 @@ public class ImportExportTest {
|
||||
assertEquals(null, card.barcodeId);
|
||||
assertEquals(BarcodeFormat.RSS_EXPANDED, card.barcodeType.format());
|
||||
assertEquals(0, card.starStatus);
|
||||
assertEquals(1625600120, card.lastUsed);
|
||||
|
||||
assertNull(Utils.retrieveCardImage(activity.getApplicationContext(), 3, ImageLocationType.front));
|
||||
assertNull(Utils.retrieveCardImage(activity.getApplicationContext(), 3, ImageLocationType.back));
|
||||
|
||||
Binary file not shown.
5
docs/STOCARD.md
Normal file
5
docs/STOCARD.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# Stocard Importer
|
||||
|
||||
The `app/src/main/res/raw/stocard_stores.csv` CSV file used by the Stocard importer was created using the `.scripts/dump_stocard_stores.py` script.
|
||||
|
||||
Only used for data portability reasons (ensuring importing works). Do NOT copy this anywhere else or use it for any purpose other than ensuring we can import a GDPR-provided export. We want to make sure this stays under fair use.
|
||||
2
fastlane/metadata/android/cs-CZ/changelogs/129.txt
Normal file
2
fastlane/metadata/android/cs-CZ/changelogs/129.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
- Vylepšení importu Catima (oprava chybějících karet při importu)
|
||||
- Oprava havárie při otáčení obrazovky během nastavení data platnosti od/vypršení
|
||||
3
fastlane/metadata/android/en-US/changelogs/129.txt
Normal file
3
fastlane/metadata/android/en-US/changelogs/129.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
- Improved Catima importer (fixes cards missing when importing)
|
||||
- Fix crash when rotating screen while setting valid from/expiry date
|
||||
- Minor UI tweaks
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 84 KiB After Width: | Height: | Size: 87 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 81 KiB After Width: | Height: | Size: 81 KiB |
1
fastlane/metadata/android/es-ES/changelogs/109.txt
Normal file
1
fastlane/metadata/android/es-ES/changelogs/109.txt
Normal file
@@ -0,0 +1 @@
|
||||
- Corrección de color de texto incorrecto en el botón "No hay código de barras"
|
||||
5
fastlane/metadata/android/es-ES/changelogs/11.txt
Normal file
5
fastlane/metadata/android/es-ES/changelogs/11.txt
Normal file
@@ -0,0 +1,5 @@
|
||||
- Cuando se edita un ID de tarjeta, pre-rellenar el ID existente al empezar. (pull #94 (https://github.com/brarcher/loyalty-card-locker/pull/94))
|
||||
- Limitar el ancho de los códigos de barras generados para reducir el uso de memoria y los errores de memoria agotada. (pull #103 (https://github.com/brarcher/loyalty-card-locker/pull/103))
|
||||
- Al editar una tarjeta, cambiar que el botón "Introducir Tarjeta" diga "Editar Tarjeta" si ya existe un ID de tarjeta. (pull #104 (https://github.com/brarcher/loyalty-card-locker/pull/104))
|
||||
-Cambiar la combinación de colores para ser más tenue y compatible con el icono de la aplicación, y cambiar la distribución al ver una tarjeta por una más limpia. (pull #107 (https://github.com/brarcher/loyalty-card-locker/pull/107))
|
||||
-Añadir un asistente de inicio que se ejecute en el primer uso de la aplicación. (pull #108 (https://github.com/brarcher/loyalty-card-locker/pull/108))
|
||||
7
fastlane/metadata/android/es-ES/changelogs/111.txt
Normal file
7
fastlane/metadata/android/es-ES/changelogs/111.txt
Normal file
@@ -0,0 +1,7 @@
|
||||
- Soporte para idioma árabe
|
||||
- Mostrar el número de tarjetas archivadas en la vista general de grupo
|
||||
- Corrección de errores en el análisis de saldos (las cartas no se podían guardar en árabe y otros idiomas con números no occidentales)
|
||||
- Corrección cuando un tema personalizado no se aplica correctamente a las pantallas principales
|
||||
- Mejoras en la pantalla de selección de tarjetas
|
||||
- Corrección del fallo al salir de la vista de tarjeta en diseños RTL para tarjetas con caducidad o saldo
|
||||
- Corrección del fallo de la flecha para volver en la vista de tarjeta que apunta en la dirección incorrecta en diseños RTL
|
||||
1
fastlane/metadata/android/es-ES/changelogs/112.txt
Normal file
1
fastlane/metadata/android/es-ES/changelogs/112.txt
Normal file
@@ -0,0 +1 @@
|
||||
- Hacer más visible la posibilidad de crear una cabecera personalizada
|
||||
3
fastlane/metadata/android/es-ES/changelogs/113.txt
Normal file
3
fastlane/metadata/android/es-ES/changelogs/113.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
- Añadir los botones de anterior y siguiente a la vista de la tarjeta de fidelización
|
||||
- Corrección de color principal en el botón de editar
|
||||
- Reemplazar el icono de guardado de disquete por una marca de confirmación
|
||||
3
fastlane/metadata/android/es-ES/changelogs/114.txt
Normal file
3
fastlane/metadata/android/es-ES/changelogs/114.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
- Añadir icono monocromático para Android 13
|
||||
- Mejora de la pantalla en la primera ejecución
|
||||
- Correcciones en la importación de Fidme
|
||||
4
fastlane/metadata/android/es-ES/changelogs/115.txt
Normal file
4
fastlane/metadata/android/es-ES/changelogs/115.txt
Normal file
@@ -0,0 +1,4 @@
|
||||
- Abrir imagen en la galería con una pulsación larga
|
||||
- Aplicar estilo Material a los modales
|
||||
- Soporte para crear una tarjeta compartiendo una imagen en Catima
|
||||
- Añadir el botón de gastos rápidos a la pantalla de la tarjeta
|
||||
2
fastlane/metadata/android/es-ES/changelogs/116.txt
Normal file
2
fastlane/metadata/android/es-ES/changelogs/116.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
- Corrección de mensaje que no permitía el separador , en los gastos rápidos
|
||||
- Soporte para cargar una imagen desde el gestor de archivos
|
||||
2
fastlane/metadata/android/es-ES/changelogs/117.txt
Normal file
2
fastlane/metadata/android/es-ES/changelogs/117.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
- Eliminados permisos innecesarios
|
||||
- Objetivo Android 13
|
||||
2
fastlane/metadata/android/es-ES/changelogs/118.txt
Normal file
2
fastlane/metadata/android/es-ES/changelogs/118.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
- Soporte para establecer el inicio de la validez de la tarjeta
|
||||
- Corrección de importación Stocard (cambio en el formato de exportación de Stocard)
|
||||
1
fastlane/metadata/android/es-ES/changelogs/119.txt
Normal file
1
fastlane/metadata/android/es-ES/changelogs/119.txt
Normal file
@@ -0,0 +1 @@
|
||||
- Uso de colores de Material You en más dispositivos (actualización de la biblioteca de Google)
|
||||
1
fastlane/metadata/android/es-ES/changelogs/12.txt
Normal file
1
fastlane/metadata/android/es-ES/changelogs/12.txt
Normal file
@@ -0,0 +1 @@
|
||||
- Prevenir un fallo al rotar la pantalla en la primera ejecución del asistente.
|
||||
3
fastlane/metadata/android/es-ES/changelogs/120.txt
Normal file
3
fastlane/metadata/android/es-ES/changelogs/120.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
- Rediseño completo de la pantalla principal y de vista de tarjeta de fidelidad
|
||||
- Diseño Material You para la pantalla de ajustes
|
||||
- Corrección de error al usar "Toma una foto" con la cámara de la aplicación deshabilitada
|
||||
1
fastlane/metadata/android/es-ES/changelogs/121.txt
Normal file
1
fastlane/metadata/android/es-ES/changelogs/121.txt
Normal file
@@ -0,0 +1 @@
|
||||
- Actualizar bibliotecas usadas
|
||||
2
fastlane/metadata/android/es-ES/changelogs/123.txt
Normal file
2
fastlane/metadata/android/es-ES/changelogs/123.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
- Pequeñas mejoras de UI
|
||||
- Corrección del nuevo diseño no disponible en dispositivos con pantallas cuadradas
|
||||
1
fastlane/metadata/android/es-ES/changelogs/124.txt
Normal file
1
fastlane/metadata/android/es-ES/changelogs/124.txt
Normal file
@@ -0,0 +1 @@
|
||||
- Soporte para seleccionar exactamente que detalles ver en la vista general de tarjeta
|
||||
1
fastlane/metadata/android/es-ES/changelogs/126.txt
Normal file
1
fastlane/metadata/android/es-ES/changelogs/126.txt
Normal file
@@ -0,0 +1 @@
|
||||
- Correcciones varias de RTL
|
||||
4
fastlane/metadata/android/es-ES/changelogs/127.txt
Normal file
4
fastlane/metadata/android/es-ES/changelogs/127.txt
Normal file
@@ -0,0 +1,4 @@
|
||||
- Mejoras en el renderizado de los códigos de barras
|
||||
- Interoperabilidad básica con aplicaciones externas (Android 6.0+)
|
||||
- Pantalla de ajustes reorganizada
|
||||
- Corrección al importar desde algunos navegadores que añaden una / al final de la URL al compartir
|
||||
1
fastlane/metadata/android/es-ES/changelogs/128.txt
Normal file
1
fastlane/metadata/android/es-ES/changelogs/128.txt
Normal file
@@ -0,0 +1 @@
|
||||
- Correción de error inusual
|
||||
2
fastlane/metadata/android/es-ES/changelogs/129.txt
Normal file
2
fastlane/metadata/android/es-ES/changelogs/129.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
- Mejorada la importación de Catima (corrige la tarjetas que faltan al importar)
|
||||
- Corrección del error al rotar la pantalla mientras se establece las fechas de validez desde/hasta
|
||||
3
fastlane/metadata/android/es-ES/changelogs/2.txt
Normal file
3
fastlane/metadata/android/es-ES/changelogs/2.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
- Traducciones en italiano
|
||||
- Soporte para todos los tipos de códigos de barras 1D. (Originalmente sólo tenian soporte los productos con códigos de barras 1D)
|
||||
- Añadir permiso obligatorio para la cámara, que inicialmente faltaba.
|
||||
1
fastlane/metadata/android/es-ES/changelogs/20.txt
Normal file
1
fastlane/metadata/android/es-ES/changelogs/20.txt
Normal file
@@ -0,0 +1 @@
|
||||
- Solución alternativa al fallo durante la instalación en algunas versiones de Android (como Android 5 o menor), (pull #184 (https://github.com/brarcher/loyalty-card-locker/pull/184))
|
||||
2
fastlane/metadata/android/es-ES/changelogs/21.txt
Normal file
2
fastlane/metadata/android/es-ES/changelogs/21.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
- Mejorada la distribución en la lista de tarjetas. (pull #188 (https://github.com/brarcher/loyalty-card-locker/pull/188))
|
||||
- Mejorada la distribución al ver una tarjeta. (pull #190 (https://github.com/brarcher/loyalty-card-locker/pull/190))
|
||||
1
fastlane/metadata/android/es-ES/changelogs/22.txt
Normal file
1
fastlane/metadata/android/es-ES/changelogs/22.txt
Normal file
@@ -0,0 +1 @@
|
||||
- Cambios al mostrar una nota en la vista de tarjeta, permitir que el ID de tarjeta ocupe varias líneas, y mostrar el nomber de la tienda. (pull #197 (https://github.com/brarcher/loyalty-card-locker/pull/197))
|
||||
1
fastlane/metadata/android/es-ES/changelogs/33.txt
Normal file
1
fastlane/metadata/android/es-ES/changelogs/33.txt
Normal file
@@ -0,0 +1 @@
|
||||
- Actualizar y añadir traducciones
|
||||
1
fastlane/metadata/android/es-ES/changelogs/34.txt
Normal file
1
fastlane/metadata/android/es-ES/changelogs/34.txt
Normal file
@@ -0,0 +1 @@
|
||||
- Actualizar traducciones en ruso
|
||||
2
fastlane/metadata/android/es-ES/changelogs/35.txt
Normal file
2
fastlane/metadata/android/es-ES/changelogs/35.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
- Habilitar las copias de seguridad de la aplicación
|
||||
- Actualizar las traducciones en francés y esloveno
|
||||
1
fastlane/metadata/android/hu-HU/changelogs/124.txt
Normal file
1
fastlane/metadata/android/hu-HU/changelogs/124.txt
Normal file
@@ -0,0 +1 @@
|
||||
- A kártyaáttekintőben megjelenítendő pontos részletek kiválasztása
|
||||
1
fastlane/metadata/android/hu-HU/changelogs/128.txt
Normal file
1
fastlane/metadata/android/hu-HU/changelogs/128.txt
Normal file
@@ -0,0 +1 @@
|
||||
- Ritka összeomlás javítása
|
||||
1
fastlane/metadata/android/hu-HU/changelogs/129.txt
Normal file
1
fastlane/metadata/android/hu-HU/changelogs/129.txt
Normal file
@@ -0,0 +1 @@
|
||||
- Javított Catima importáló (javítja az importálás után hiányzó kártyákat)
|
||||
@@ -1,3 +1,4 @@
|
||||
- Улучшения визуализации штрих‐кодов
|
||||
- Базовая совместимость с внешними приложениями (Android 6.0+)
|
||||
- Реорганизация экрана настроек
|
||||
- Исправление импорта из некоторых браузеров, добавляющих в URL-адрес ресурса концевой символ /
|
||||
|
||||
1
fastlane/metadata/android/ru-RU/changelogs/128.txt
Normal file
1
fastlane/metadata/android/ru-RU/changelogs/128.txt
Normal file
@@ -0,0 +1 @@
|
||||
- Исправлен редкий сбой
|
||||
2
fastlane/metadata/android/ru-RU/changelogs/129.txt
Normal file
2
fastlane/metadata/android/ru-RU/changelogs/129.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
- Улучшен импор в Catima (исправлено пропадание карт при импорте)
|
||||
- Исправлен сбой при повороте экрана во время установки даты срока действия
|
||||
4
fastlane/metadata/android/uk/changelogs/127.txt
Normal file
4
fastlane/metadata/android/uk/changelogs/127.txt
Normal file
@@ -0,0 +1,4 @@
|
||||
- Покращення відображення штрих-кодів
|
||||
- Базова сумісність із зовнішніми додатками (Android 6.0+)
|
||||
- Налаштування стали зручнішими
|
||||
- Виправлено імпорт з деяких браузерів, які додають до URL-адреси символ "/" при поширенні посилання
|
||||
1
fastlane/metadata/android/uk/changelogs/128.txt
Normal file
1
fastlane/metadata/android/uk/changelogs/128.txt
Normal file
@@ -0,0 +1 @@
|
||||
- Виправлена нечаста помилка що призводила викидів з програми
|
||||
2
fastlane/metadata/android/uk/changelogs/129.txt
Normal file
2
fastlane/metadata/android/uk/changelogs/129.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
- Покращено імпортер Catima (виправлено пропущені картки під час імпорту)
|
||||
- Виправлено збій при обертанні екрану під час встановлення дати терміну дії "від" та "до"
|
||||
2
fastlane/metadata/android/zh-CN/changelogs/129.txt
Normal file
2
fastlane/metadata/android/zh-CN/changelogs/129.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
- 改进了 Catima 数据导入程序 (修复导入时的卡片丢失)
|
||||
- 修复设置卡片有效期时屏幕旋转导致的崩溃
|
||||
Reference in New Issue
Block a user