Compare commits

...

109 Commits

Author SHA1 Message Date
Sylvia van Os
a408f8d727 Release Catima 2.25.2 2023-07-27 19:14:44 +02:00
Sylvia van Os
2d7bb02d1a Update fastlane 2023-07-27 19:07:10 +02:00
Sylvia van Os
38c8e38ed6 Merge pull request #1446 from CatimaLoyalty/create-pull-request/patch-1690299279
Update Fastlane changelogs
2023-07-25 17:35:15 +02:00
TheLastProject
28b95b8f75 Update Fastlane changelogs 2023-07-25 15:34:39 +00:00
Sylvia van Os
c1ebbdb997 Update CHANGELOG.md 2023-07-25 17:34:22 +02:00
Sylvia van Os
fadba7a11c Merge pull request #1445 from obfusk/stocard-csv
add python script & update stocard csv
2023-07-24 20:54:41 +02:00
FC Stegerman
fd61434565 move stocard readme & script 2023-07-24 20:35:25 +02:00
Sylvia van Os
398dff4b3c Merge pull request #1444 from weblate/weblate-catima-catima
Translations update from Hosted Weblate
2023-07-24 19:35:57 +02:00
FC Stegerman
5c95b750b2 rename to dump_stocard_stores.py 2023-07-24 18:24:42 +02:00
FC Stegerman
9851c0a2fa update stocard_stores.csv 2023-07-24 18:22:03 +02:00
FC Stegerman
c121f846c5 add stocard_stores.py script 2023-07-24 18:17:57 +02:00
ChengCheng
c20ba027cf Added translation using Weblate (Chinese (Literary)) 2023-07-24 11:42:49 +02:00
Kamborio
e5dd26b8ee Translated using Weblate (Spanish)
Currently translated at 29.1% (37 of 127 strings)

Translation: Catima/Android (Fastlane)
Translate-URL: https://hosted.weblate.org/projects/catima/fastlane/es/
2023-07-24 11:41:46 +02:00
Danylo Lystopadov
3b449464ac Translated using Weblate (Ukrainian)
Currently translated at 100.0% (127 of 127 strings)

Translation: Catima/Android (Fastlane)
Translate-URL: https://hosted.weblate.org/projects/catima/fastlane/uk/
2023-07-24 11:41:46 +02:00
Danylo Lystopadov
3f4d4e38cd Translated using Weblate (Ukrainian)
Currently translated at 100.0% (298 of 298 strings)

Translation: Catima/Android
Translate-URL: https://hosted.weblate.org/projects/catima/catima/uk/
2023-07-24 11:41:46 +02:00
Sylvia van Os
30ccd03686 Merge pull request #1442 from obfusk/usages-len
StocardImporter: check usages is not empty just in case
2023-07-23 21:57:48 +02:00
FC Stegerman
5493947c28 StocardImporter: check usages is not empty just in case 2023-07-23 20:56:24 +02:00
Sylvia van Os
4172903b42 Merge pull request #1441 from CatimaLoyalty/stocard_20230721
Update Stocard importer
2023-07-23 16:48:53 +02:00
Sylvia van Os
00b1368176 Fix image import 2023-07-23 16:29:55 +02:00
Sylvia van Os
09fee5628f Read usage-statistics/content.json file for lastUsed data 2023-07-23 14:28:14 +02:00
Sylvia van Os
7a7a2f8361 Read input_provider_name in field if available 2023-07-23 14:25:56 +02:00
Sylvia van Os
a9ced56023 Include newer Stocard export 2023-07-23 14:01:59 +02:00
Sylvia van Os
a4af171598 Small credits dialog tweak 2023-07-23 13:06:16 +02:00
Sylvia van Os
164c82a779 Merge pull request #1440 from CatimaLoyalty/create-pull-request/patch-1690096427
Update contributors
2023-07-23 10:01:30 +02:00
TheLastProject
608c3ab863 Update contributors 2023-07-23 07:13:47 +00:00
Sylvia van Os
379a71c7ad Merge pull request #1439 from weblate/weblate-catima-catima
Translations update from Hosted Weblate
2023-07-22 17:21:15 +02:00
Jiri Grönroos
ee6a6dffcf Translated using Weblate (Finnish)
Currently translated at 100.0% (298 of 298 strings)

Translation: Catima/Android
Translate-URL: https://hosted.weblate.org/projects/catima/catima/fi/
2023-07-22 17:08:30 +02:00
solokot
1d6a393914 Translated using Weblate (Russian)
Currently translated at 100.0% (127 of 127 strings)

Translation: Catima/Android (Fastlane)
Translate-URL: https://hosted.weblate.org/projects/catima/fastlane/ru/
2023-07-22 17:08:30 +02:00
Tim Trek
5c4a905ac0 Translated using Weblate (German)
Currently translated at 100.0% (298 of 298 strings)

Translation: Catima/Android
Translate-URL: https://hosted.weblate.org/projects/catima/catima/de/
2023-07-22 17:08:30 +02:00
Sylvia van Os
91ce71ea68 Merge pull request #1438 from weblate/weblate-catima-catima
Translations update from Hosted Weblate
2023-07-22 10:42:18 +02:00
Eric
f07ac3e026 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (127 of 127 strings)

Translation: Catima/Android (Fastlane)
Translate-URL: https://hosted.weblate.org/projects/catima/fastlane/zh_Hans/
2023-07-22 03:11:31 +02:00
Slávek Banko
649f2c47b4 Translated using Weblate (Czech)
Currently translated at 100.0% (127 of 127 strings)

Translation: Catima/Android (Fastlane)
Translate-URL: https://hosted.weblate.org/projects/catima/fastlane/cs/
2023-07-22 03:11:31 +02:00
Milan Šalka
62dcc373ed Translated using Weblate (Slovak)
Currently translated at 100.0% (298 of 298 strings)

Translation: Catima/Android
Translate-URL: https://hosted.weblate.org/projects/catima/catima/sk/
2023-07-22 03:11:31 +02:00
Wiktor
e9b04adec6 Translated using Weblate (Polish)
Currently translated at 100.0% (298 of 298 strings)

Translation: Catima/Android
Translate-URL: https://hosted.weblate.org/projects/catima/catima/pl/
2023-07-22 03:11:31 +02:00
Sylvia van Os
636be16bdd Merge pull request #1436 from weblate/weblate-catima-catima
Translations update from Hosted Weblate
2023-07-20 17:26:50 +02:00
Sylvia van Os
5ad28f37b8 Merge pull request #1437 from CatimaLoyalty/feature/elevatedMainScreen
Small main screen layout tweaks
2023-07-20 17:26:17 +02:00
Sylvia van Os
5ff6059e86 Small main screen layout tweaks 2023-07-20 16:59:39 +02:00
Balázs Meskó
b9df712394 Translated using Weblate (Hungarian)
Currently translated at 33.0% (42 of 127 strings)

Translation: Catima/Android (Fastlane)
Translate-URL: https://hosted.weblate.org/projects/catima/fastlane/hu/
2023-07-20 13:07:59 +02:00
Eric
790fd7e48f Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (127 of 127 strings)

Translation: Catima/Android (Fastlane)
Translate-URL: https://hosted.weblate.org/projects/catima/fastlane/zh_Hans/
2023-07-20 13:07:59 +02:00
Slávek Banko
bb43266a01 Translated using Weblate (Czech)
Currently translated at 100.0% (127 of 127 strings)

Translation: Catima/Android (Fastlane)
Translate-URL: https://hosted.weblate.org/projects/catima/fastlane/cs/
2023-07-20 13:07:58 +02:00
Sylvia van Os
450cfce84a Merge pull request #1434 from obfusk/refactor-importer-part-2
refactor importer part 2
2023-07-19 21:17:22 +02:00
Sylvia van Os
6cef56b38b Merge pull request #1435 from CatimaLoyalty/create-pull-request/patch-1689792805
Update Fastlane changelogs
2023-07-19 20:53:53 +02:00
TheLastProject
fbb7cf7e9c Update Fastlane changelogs 2023-07-19 18:53:24 +00:00
Sylvia van Os
9ab2a6a5b2 Update CHANGELOG 2023-07-19 20:53:06 +02:00
Quang Trung
682fc8303c Fix crash on configuration changes in DatePickerFragment (#1431) 2023-07-19 20:52:25 +02:00
FC Stegerman
aa1274566b refactor FidmeImporter 2023-07-19 01:25:00 +02:00
FC Stegerman
d8cd581cb0 refactor StocardImporter 2023-07-19 01:10:39 +02:00
FC Stegerman
2cd00f9103 refactor VoucherVaultImporter 2023-07-19 00:24:09 +02:00
Sylvia van Os
ae07f94b25 Merge pull request #1433 from obfusk/test-bitmaps
use JPGs instead of LetterBitmap b/c of partial robolectric Canvas support
2023-07-18 23:53:15 +02:00
FC Stegerman
22d671263a JPGs instead of LetterBitmap (robolectric missing Canvas support) 2023-07-18 23:27:44 +02:00
Sylvia van Os
227f30361f Try to fix autoclose for real 2023-07-17 21:15:14 +02:00
Sylvia van Os
b964652b83 Merge pull request #1429 from CatimaLoyalty/create-pull-request/patch-1689620901
Update Fastlane changelogs
2023-07-17 21:09:32 +02:00
TheLastProject
f926ffa1d0 Update Fastlane changelogs 2023-07-17 19:08:21 +00:00
Sylvia van Os
d03b8b5635 Merge branch 'main' of github.com:CatimaLoyalty/Android 2023-07-17 21:08:02 +02:00
Sylvia van Os
4e5fea7a52 Update CHANGELOG 2023-07-17 21:07:54 +02:00
Sylvia van Os
95cb6c0a08 Merge pull request #1422 from obfusk/refactor-importer-part-1
refactor importer part 1
2023-07-17 21:06:00 +02:00
FC Stegerman
d350d0b2c7 CatimaImporter: add comment about card group import
Co-authored-by: Sylvia van Os <sylvia@hackerchick.me>
2023-07-17 20:42:36 +02:00
FC Stegerman
ac0f6f6f3e Utils.getRenamedCardImageFileName(): add javadoc
Co-authored-by: Sylvia van Os <sylvia@hackerchick.me>
2023-07-17 20:35:02 +02:00
FC Stegerman
ab030ba002 LocaltyCard.isDuplicate(): reformat & add comments
Co-authored-by: Sylvia van Os <sylvia@hackerchick.me>
2023-07-17 20:23:52 +02:00
Sylvia van Os
93103c8c6d Merge branch 'main' of github.com:CatimaLoyalty/Android 2023-07-17 18:36:28 +02:00
Sylvia van Os
576ec1e6de Release Catima 2.25.1 2023-07-17 18:35:50 +02:00
Sylvia van Os
b602ce5d78 Merge pull request #1426 from weblate/weblate-catima-catima
Translations update from Hosted Weblate
2023-07-17 18:29:08 +02:00
Cabrito
0ecd38ed1c Translated using Weblate (Portuguese)
Currently translated at 100.0% (126 of 126 strings)

Translation: Catima/Android (Fastlane)
Translate-URL: https://hosted.weblate.org/projects/catima/fastlane/pt/
2023-07-17 18:03:59 +02:00
Cabrito
d48e02463c Translated using Weblate (Portuguese)
Currently translated at 100.0% (298 of 298 strings)

Translation: Catima/Android
Translate-URL: https://hosted.weblate.org/projects/catima/catima/pt/
2023-07-17 18:03:59 +02:00
Sylvia van Os
b9d9c8d2e3 Use chevron-style up/down icons in groups layout 2023-07-16 21:34:03 +02:00
FC Stegerman
3ae665b70f DBHelper: add note to DEFAULT_ZOOM_LEVEL 2023-07-16 20:16:01 +02:00
FC Stegerman
9cf9959b6b add importExistingCardsAfterModification test 2023-07-16 19:53:41 +02:00
FC Stegerman
d11e2c166b Utils.copyToTempFile(): use try for resource management 2023-07-16 18:29:38 +02:00
FC Stegerman
f783be7a4f importer: handle inputFile errors better 2023-07-16 18:16:41 +02:00
FC Stegerman
9ee96b88e8 CatimaImporter: add .close() 2023-07-16 18:04:26 +02:00
FC Stegerman
ba896fc1db DBHelper: don't use DEFAULT_ZOOM_LEVEL in migration 2023-07-16 18:02:52 +02:00
Sylvia van Os
47f1ea80b6 Merge pull request #1424 from weblate/weblate-catima-catima
Translations update from Hosted Weblate
2023-07-16 15:14:48 +02:00
Projjal Moitra
b5efa28e85 Translated using Weblate (Bengali (India))
Currently translated at 96.3% (287 of 298 strings)

Translation: Catima/Android
Translate-URL: https://hosted.weblate.org/projects/catima/catima/bn_IN/
2023-07-16 15:05:41 +02:00
solokot
d456a8920d Translated using Weblate (Russian)
Currently translated at 100.0% (298 of 298 strings)

Translation: Catima/Android
Translate-URL: https://hosted.weblate.org/projects/catima/catima/ru/
2023-07-16 15:05:41 +02:00
Sylvia van Os
4ef0c96b29 Merge pull request #1423 from CatimaLoyalty/create-pull-request/patch-1689492043
Update contributors
2023-07-16 10:03:08 +02:00
TheLastProject
343c77f9e3 Update contributors 2023-07-16 07:20:43 +00:00
FC Stegerman
1425d4af58 CatimaImporter: add saveAndDeduplicate() & refactor 2023-07-16 05:08:44 +02:00
FC Stegerman
48510494eb Utils: split off CARD_IMAGE_FILENAME_REGEX 2023-07-16 05:05:44 +02:00
FC Stegerman
d5d53b241a Utils: add getRenamedCardImageFileName() 2023-07-16 04:44:24 +02:00
FC Stegerman
901c2d8154 LoyaltyCard: fix isDuplicate() 2023-07-16 04:43:27 +02:00
FC Stegerman
84d7e15b5c LoyaltyCard: add isDuplicate(); Utils: add equals() 2023-07-16 03:29:31 +02:00
FC Stegerman
b8fa4d7060 DBHelper: add DEFAULT_ZOOM_LEVEL 2023-07-16 03:12:29 +02:00
FC Stegerman
da9e3bb6b2 CatimaImporter: read ZIP twice, get checksums 2023-07-16 02:23:54 +02:00
FC Stegerman
3a5973a04d Utils: add checksum() 2023-07-16 02:13:34 +02:00
Sylvia van Os
673e64924b Add security policy 2023-07-16 01:25:53 +02:00
FC Stegerman
5f99f2b17e Utils: add imageFiles() 2023-07-16 00:57:00 +02:00
FC Stegerman
bf05103955 import: copy ZIP, use File instead of InputStream 2023-07-16 00:45:18 +02:00
Sylvia van Os
5ea6155c39 Merge pull request #1421 from obfusk/patch-5
revert s/Frie/Gratis/ in nb-rNO translation
2023-07-14 20:07:25 +02:00
FC Stegerman
abb1cd29f0 revert s/Frie/Gratis/ in nb-rNO translation 2023-07-14 17:31:56 +00:00
Sylvia van Os
52363cdff4 Merge pull request #1420 from weblate/weblate-catima-catima
Translations update from Hosted Weblate
2023-07-14 17:52:25 +02:00
Bottan Hermawan
ac4f4e3a9a Translated using Weblate (Indonesian)
Currently translated at 95.2% (120 of 126 strings)

Translation: Catima/Android (Fastlane)
Translate-URL: https://hosted.weblate.org/projects/catima/fastlane/id/
2023-07-14 16:53:20 +02:00
Bottan Hermawan
555387e20d Translated using Weblate (Indonesian)
Currently translated at 100.0% (298 of 298 strings)

Translation: Catima/Android
Translate-URL: https://hosted.weblate.org/projects/catima/catima/id/
2023-07-14 16:53:20 +02:00
Sylvia van Os
70ae8ff167 Merge pull request #1419 from weblate/weblate-catima-catima
Translations update from Hosted Weblate
2023-07-13 19:03:47 +02:00
Balázs Meskó
86512532f1 Translated using Weblate (Hungarian)
Currently translated at 30.9% (39 of 126 strings)

Translation: Catima/Android (Fastlane)
Translate-URL: https://hosted.weblate.org/projects/catima/fastlane/hu/
2023-07-13 14:45:59 +02:00
Eric
ebf6318aa2 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (126 of 126 strings)

Translation: Catima/Android (Fastlane)
Translate-URL: https://hosted.weblate.org/projects/catima/fastlane/zh_Hans/
2023-07-13 14:45:59 +02:00
Slávek Banko
fd482a4cba Translated using Weblate (Czech)
Currently translated at 100.0% (126 of 126 strings)

Translation: Catima/Android (Fastlane)
Translate-URL: https://hosted.weblate.org/projects/catima/fastlane/cs/
2023-07-13 14:45:59 +02:00
Erik Spjelkavik
54d91dc8a1 Translated using Weblate (Norwegian Bokmål)
Currently translated at 99.3% (296 of 298 strings)

Translation: Catima/Android
Translate-URL: https://hosted.weblate.org/projects/catima/catima/nb_NO/
2023-07-13 14:45:59 +02:00
J. Lavoie
9fa7fe388f Translated using Weblate (French)
Currently translated at 100.0% (298 of 298 strings)

Translation: Catima/Android
Translate-URL: https://hosted.weblate.org/projects/catima/catima/fr/
2023-07-13 14:45:59 +02:00
Kamborio
1415d8da3e Translated using Weblate (Spanish)
Currently translated at 100.0% (298 of 298 strings)

Translation: Catima/Android
Translate-URL: https://hosted.weblate.org/projects/catima/catima/es/
2023-07-13 14:45:59 +02:00
J. Lavoie
0ad5de18e1 Translated using Weblate (German)
Currently translated at 100.0% (298 of 298 strings)

Translation: Catima/Android
Translate-URL: https://hosted.weblate.org/projects/catima/catima/de/
2023-07-13 14:45:59 +02:00
Sylvia van Os
9659a2a2cd Merge pull request #1418 from weblate/weblate-catima-catima
Translations update from Hosted Weblate
2023-07-12 17:12:10 +02:00
Giovanni Donisi
1dd894bd27 Translated using Weblate (Italian)
Currently translated at 100.0% (298 of 298 strings)

Translation: Catima/Android
Translate-URL: https://hosted.weblate.org/projects/catima/catima/it/
2023-07-12 16:52:59 +02:00
Sylvia van Os
0d9151294e Merge pull request #1416 from CatimaLoyalty/create-pull-request/patch-1689096450
Update Fastlane changelogs
2023-07-11 19:31:39 +02:00
Sylvia van Os
09dda99afc Merge pull request #1415 from obfusk/fix-shortcuts
ShortcutHelper: trim after skipping missing cards
2023-07-11 19:31:18 +02:00
TheLastProject
a4fb91b9aa Update Fastlane changelogs 2023-07-11 17:27:30 +00:00
Sylvia van Os
036b821b28 Update CHANGELOG 2023-07-11 19:27:12 +02:00
Sylvia van Os
479a35657f Merge pull request #1414 from obfusk/fix-shortcuts
ShortcutHelper: skip outdated cards that no longer exist
2023-07-11 19:19:14 +02:00
FC Stegerman
eca9d1c74c ShortcutHelper: trim after skipping missing cards 2023-07-11 19:18:41 +02:00
FC Stegerman
95055f1ce6 ShortcutHelper: skip outdated cards that no longer exist 2023-07-11 18:14:34 +02:00
107 changed files with 8474 additions and 7622 deletions

View File

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

View File

@@ -1,5 +1,15 @@
# 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
## v2.25.0 - 127 (2023-07-09)
- Barcode rendering improvements

View File

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

13
SECURITY.md Normal file
View File

@@ -0,0 +1,13 @@
# Security Policy
Catima is designed to use as little permissions as possible to limit both the attack surface as well as the damage that can be done when abusing a security flaw.
## Supported Versions
Only the most recent stable release is supported.
## Reporting a Vulnerability
Security vulnerabilities can be reported through [GitHub Security Advisories](https://github.com/CatimaLoyalty/Android/security/advisories) or [the contact info written on my personal website](https://sylviavanos.nl/#contact). Currently, Matrix is the only end-to-end encrypted option.
Please note that only security vulnerabilities in Catima should be reported as stated above. For other issues, including antivirus false positives and malicious applications trying to trick people into granting them Catima's "Read Cards" permission, please use [regular issues](https://github.com/CatimaLoyalty/Android/issues).

View File

@@ -19,8 +19,8 @@ android {
applicationId "me.hackerchick.catima"
minSdk 21
targetSdk 33
versionCode 127
versionName "2.25.0"
versionCode 129
versionName "2.25.2"
vectorDrawables.useSupportLibrary true
multiDexEnabled true

View File

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

View File

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

View File

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

View File

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

View File

@@ -70,19 +70,13 @@ class ShortcutHelper {
ShortcutInfoCompat found = list.remove(foundIndex.intValue());
list.addFirst(found);
} else {
// The item is new to the list. First, we need to trim the list
// until it is able to accept a new item, then the item is
// inserted.
while (list.size() >= MAX_SHORTCUTS) {
list.pollLast();
}
// The item is new to the list. We add it and trim the list later.
ShortcutInfoCompat shortcut = createShortcutBuilder(context, card).build();
list.addFirst(shortcut);
}
LinkedList<ShortcutInfoCompat> finalList = new LinkedList<>();
int rank = 0;
// The ranks are now updated; the order in the list is the rank.
for (int index = 0; index < list.size(); index++) {
@@ -90,11 +84,20 @@ class ShortcutHelper {
LoyaltyCard loyaltyCard = DBHelper.getLoyaltyCard(database, Integer.parseInt(prevShortcut.getId()));
ShortcutInfoCompat updatedShortcut = createShortcutBuilder(context, loyaltyCard)
.setRank(index)
.build();
// skip outdated cards that no longer exist
if (loyaltyCard != null) {
ShortcutInfoCompat updatedShortcut = createShortcutBuilder(context, loyaltyCard)
.setRank(rank)
.build();
finalList.addLast(updatedShortcut);
finalList.addLast(updatedShortcut);
rank++;
// trim the list
if (rank >= MAX_SHORTCUTS) {
break;
}
}
}
ShortcutManagerCompat.setDynamicShortcuts(context, finalList);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
<vector android:height="24dp" android:tint="?attr/colorControlNormal"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M7,14l5,-5 5,5z"/>
<path android:fillColor="@android:color/white" android:pathData="M7.41,8.59L12,13.17l4.59,-4.58L18,10l-6,6 -6,-6 1.41,-1.41z"/>
</vector>

View File

@@ -1,5 +1,5 @@
<vector android:height="24dp" android:tint="?attr/colorControlNormal"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M7,10l5,5 5,-5z"/>
<path android:fillColor="@android:color/white" android:pathData="M7.41,15.41L12,10.83l4.59,4.58L18,14l-6,-6 -6,6z"/>
</vector>

View File

@@ -43,7 +43,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
app:icon="@drawable/ic_baseline_arrow_drop_up_24"
app:icon="@drawable/ic_baseline_keyboard_arrow_up_24"
app:iconGravity="textStart"
app:tint="?attr/colorOnPrimary"
android:contentDescription="@string/moveUp"/>
@@ -54,7 +54,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
app:icon="@drawable/ic_baseline_arrow_drop_down_24"
app:icon="@drawable/ic_baseline_keyboard_arrow_down_24"
app:iconGravity="textStart"
app:tint="?attr/colorOnPrimary"
android:contentDescription="@string/moveDown"/>

View File

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

View File

@@ -7,9 +7,10 @@ Heimen Stoffels
Oğuz Ersen
Katharine Chui
mondstern
SlavekB
StoyanDimitrov
IllusiveMan196
SlavekB
FC Stegerman
Altonss
Michael Moroni
Gediminas Murauskas
@@ -18,26 +19,25 @@ Joel A
laralem
Taco
pfaffenrodt
FC Stegerman
gallegonovato
Nyatsuki
HudobniVolk
Samantaz Fox
Eric
arno-github
Ankit Tiwari
Sergio Paredes
Clxff H3r4ld0
Aayush Gupta
huuhaa
Quentin PAGÈS
Eric
Balázs Meskó
huuhaa
Projjal Moitra
Quentin PAGÈS
Giovanni Donisi
Alexander Ivanov
arshbeerSingh
Denis Shilin
Freddo espresso
Giovanni Donisi
Silvério Santos
Miha Frangež
Eryk Michalak
@@ -45,6 +45,7 @@ Arnis Jaundžeikars
Dan
sr093906
mdvhimself
Jiri Grönroos
Katarzyna
echo r"0xX4H" | rev
Magnitudee
@@ -57,7 +58,7 @@ enolp
Evgeniy Khramov
Jane Kong
Jean Mareilles
Jiri Grönroos
José Rebelo
K. Herbert
Lisa A.
Mawuena M. KODZO A.
@@ -79,6 +80,8 @@ Astrohops1
BMN
balaraz
BootVirtual
Bottan Hermawan
zChiip
Clonewayx
D. Domig
Diego
@@ -89,7 +92,6 @@ francescbassas
Jason Li
Jean-Luc Tibaux
Jesse Davids
José Rebelo
Kis Dominik
Lukas Grassauer
Luna Jernberg
@@ -154,6 +156,7 @@ danieluhrinyi
Daniele Tricoli
Kasina Dheeraj
Donno
Erik Spjelkavik
Flav
Franciszek Stefan
Gael Caraballo
@@ -171,6 +174,7 @@ Jean-Baptiste
Kung-chih
Karvjorm
polar
Kamborio
krkk
Laura Ferraz
Lucas da Costa
@@ -182,6 +186,7 @@ Mateo Gomez
Mattia
Md. Al-Amin
Michael Gangolf
Milan Šalka
3DN1M
Minecraft boom
Mobashir Raihan
@@ -197,6 +202,7 @@ vandman
Piotr Strebski
Piotr Zet
Poorva Patidar
Quang Trung
Quang Nguyen
Ratnesh
Reza
@@ -214,6 +220,7 @@ Subhradeep Bera
Swayam Khare
SziaTomi
Mehedi Hasan
Tim Trek
Titas Pažereckas
atakujonc
tkraljevic
@@ -221,6 +228,7 @@ Tony C
Vancha March
tyap-lyap-ivprod
Waldemar Stoczkowski
Wiktor Kwapisiewicz
Yevgeny M
Yusril A
Ziad OUALHADJ

View File

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

View File

File diff suppressed because it is too large Load Diff

View File

@@ -68,7 +68,7 @@
<string name="sort_by_expiry">মেয়াদ শেষ করে সাজান</string>
<string name="reverse">...উল্টো ক্রমে</string>
<string name="sort_by">ক্রমানুসার</string>
<string name="noCardExistsError">কার্ডটি পাওয়া যায়নি</string>
<string name="noCardExistsError">কার্ডটি খুঁজে পাওয়া গেল না</string>
<string name="noStoreError">স্টোরেজ ত্রুটি নেই</string>
<string name="card_ids_copied">আইডি কপি করা হয়েছে</string>
<string name="noCardsMessage">কোন কার্ড বার্তা নেই</string>
@@ -124,22 +124,22 @@
<string name="settings_portrait_orientation">প্রতিকৃতি</string>
<string name="barcodeImageDescriptionWithType">ছবি <xliff:g>%s</xliff:g> বারকোড</string>
<string name="exportName">রপ্তানি</string>
<string name="failedParsingImportUriError">আমদানির ইউআরআই বোঝা যাচ্ছে না</string>
<string name="failedParsingImportUriError">আমদানির URI-টি বোঝা যাচ্ছে না</string>
<string name="importExport">আমদানি/রপ্তানি</string>
<string name="cardShortcut">কার্ড শর্টকাট</string>
<string name="exportFailed">বার করা যাচ্ছে না</string>
<string name="exportFailed">প্তানি করা যাচ্ছে না</string>
<string name="copy_to_clipboard_toast">আইডি ক্লিপবোর্ডে নকল করা হল</string>
<string name="noCardIdError">কোনো আইডি দওয়া হয়নি</string>
<string name="importExportHelp">নিজের তথ্য অন্য কোথাও সংরক্ষণ করে রাখলে পরে সেটা অন্য ফোনে আবার নিয়ে নাওয়া যাই</string>
<string name="importFailed">না যাচ্ছে না</string>
<string name="importExportHelp">নিজের ডেটা অন্য কোথাও সংরক্ষণ করে রাখলে পরে সেটা অন্য ডিভাইসে সরিয়ে নিতে পারবেন</string>
<string name="importFailed">মদানি করা গেল না</string>
<string name="noGiftCardsGroup">কিছু কার্ড বানান আর এই গ্রুপে স্থির করুন।</string>
<string name="scanCardBarcode">বারকোড স্ক্যান করুন</string>
<string name="importSuccessfulTitle">না শেষ</string>
<string name="importFailedTitle">না ব্যর্থ</string>
<string name="exportSuccessfulTitle">বার করা শেষ</string>
<string name="exportFailedTitle">বার করা ব্যর্থ</string>
<string name="importing">া হচ্ছে…</string>
<string name="exporting">বার করা হচ্ছে…</string>
<string name="importSuccessfulTitle">মদানি শেষ</string>
<string name="importFailedTitle">মদানি ব্যর্থ</string>
<string name="exportSuccessfulTitle">রপ্তানি শেষ</string>
<string name="exportFailedTitle">রপ্তানি ব্যর্থ</string>
<string name="importing">মদানি করা হচ্ছে…</string>
<string name="exporting">প্তানি করা হচ্ছে…</string>
<string name="storageReadPermissionRequired">এই কাজটির জন্য ফোনের স্টোরেজ দেখার অনুমতি লাগবে…</string>
<string name="exportOptionExplanation">ডেটাটি আপনার পছন্দের জায়গায় রাখা হবে।</string>
<string name="importOptionFilesystemTitle">স্টোরেজ থেকে আমদানি করুন</string>
@@ -272,4 +272,4 @@
<string name="icon_header_click_text">দীর্ঘক্ষন টাচ করে থাম্বনেইল এডিট করবেন</string>
<string name="show_balance">ব্যালান্স দেখান</string>
<string name="donate">দান করুন</string>
</resources>
</resources>

View File

@@ -270,10 +270,17 @@
<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>
<string name="permissionReadCardsDescription">Lesen Sie Ihre Karten mit allen Details, einschließlich Notizen und Bildern</string>
<string name="settings_allow_content_provider_read_summary">Anwendungen müssen weiterhin eine Genehmigung beantragen, um Zugriff zu erhalten</string>
</resources>
<string name="settings_display_barcode_max_brightness_summary">Erforderlich für das Funktionieren einiger Scanner</string>
<string name="settings_keep_screen_on_summary">Deaktiviert die Bildschirmzeitüberschreitung beim Anzeigen einer Karte</string>
<string name="settings_disable_lockscreen_while_viewing_card_summary">Deaktiviert die Bildschirmsperre während der Anzeige einer Karte</string>
<string name="settings_oled_dark_summary">Reduziert den Batterieverbrauch bei OLED-Displays</string>
<string name="settings_category_title_cards">Karten</string>
<string name="settings_category_title_privacy">Datenschutz</string>
<string name="settings_category_title_general">Allgemein</string>
</resources>

View File

@@ -279,4 +279,15 @@
<string name="show_note">Mostrar la nota</string>
<string name="show_validity">Mostrar la validez</string>
<string name="show_balance">Mostrar el saldo</string>
</resources>
<string name="permissionReadCardsLabel">Leer Tarjetas Catima</string>
<string name="permissionReadCardsDescription">Lee tus tarjetas y todos sus detalles, incluidas notas e imágenes</string>
<string name="settings_allow_content_provider_read_title">Permite a otras aplicaciones acceder a mis datos</string>
<string name="settings_display_barcode_max_brightness_summary">Necesario para que funcione en algunos escáneres</string>
<string name="settings_keep_screen_on_summary">Deshabilita el tiempo de espera de la pantalla mientras se ve una tarjeta</string>
<string name="settings_allow_content_provider_read_summary">Las aplicaciones todavía tendrán que solicitar permiso para conseguir acceso</string>
<string name="settings_oled_dark_summary">Reduce el uso de batería en pantallas OLED</string>
<string name="settings_category_title_cards">Tarjetas</string>
<string name="settings_category_title_general">General</string>
<string name="settings_disable_lockscreen_while_viewing_card_summary">Deshabilita el bloqueo de pantalla mientras se ve una tarjeta</string>
<string name="settings_category_title_privacy">Privacidad</string>
</resources>

View File

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

View File

@@ -283,4 +283,11 @@
<string name="permissionReadCardsDescription">Lisez vos cartes et tous ses détails, y compris les notes et les images</string>
<string name="settings_allow_content_provider_read_title">Autoriser d\'autres applications à accéder à mes données</string>
<string name="settings_allow_content_provider_read_summary">Les applications devront toujours demander une autorisation pour obtenir l\'accès</string>
</resources>
<string name="settings_display_barcode_max_brightness_summary">Nécessaire au fonctionnement de certains scanneurs</string>
<string name="settings_keep_screen_on_summary">Désactive la temporisation de l\'écran lors de la visualisation d\'une carte</string>
<string name="settings_oled_dark_summary">Réduit l\'utilisation de la batterie sur les écrans OLED</string>
<string name="settings_category_title_cards">Cartes</string>
<string name="settings_category_title_general">Généraux</string>
<string name="settings_category_title_privacy">Confidentialité</string>
<string name="settings_disable_lockscreen_while_viewing_card_summary">Désactive le verrouillage de l\'écran pendant la visualisation d\'une carte</string>
</resources>

View File

@@ -265,4 +265,15 @@
<string name="icon_header_click_text">Tekan lama untuk mengedit thumbnail</string>
<string name="show_name_below_image_thumbnail">Tampilkan nama di bawah thumbnail gambar</string>
<string name="show_note">Tampilkan catatan</string>
</resources>
<string name="permissionReadCardsLabel">Baca Kartu Catima</string>
<string name="permissionReadCardsDescription">Baca kartu Anda dan semua detailnya, termasuk catatan dan gambar</string>
<string name="settings_allow_content_provider_read_title">Izinkan aplikasi lain mengakses data saya</string>
<string name="settings_allow_content_provider_read_summary">Aplikasi masih harus meminta izin untuk diberikan akses</string>
<string name="settings_keep_screen_on_summary">Menonaktifkan batas waktu layar saat melihat kartu</string>
<string name="settings_oled_dark_summary">Mengurangi penggunaan baterai pada layar OLED</string>
<string name="settings_category_title_general">Umum</string>
<string name="settings_display_barcode_max_brightness_summary">Diperlukan agar beberapa pemindai dapat berfungsi</string>
<string name="settings_disable_lockscreen_while_viewing_card_summary">Menonaktifkan kunci layar saat melihat kartu</string>
<string name="settings_category_title_cards">Kartu</string>
<string name="settings_category_title_privacy">Privasi</string>
</resources>

View File

@@ -283,4 +283,11 @@
<string name="settings_allow_content_provider_read_summary">Le applicazioni dovranno comunque richiedere l\'autorizzazione per ottenere l\'accesso</string>
<string name="settings_allow_content_provider_read_title">Consenti ad altre applicazioni di accedere ai miei dati</string>
<string name="permissionReadCardsDescription">Leggi le tue carte e tutti i suoi dettagli, comprese le note e le immagini</string>
</resources>
<string name="settings_display_barcode_max_brightness_summary">Necessario per il funzionamento di alcuni scanner</string>
<string name="settings_keep_screen_on_summary">Disattiva il timeout dello schermo durante la visualizzazione di una carta</string>
<string name="settings_disable_lockscreen_while_viewing_card_summary">Disattiva il blocco dello schermo durante la visualizzazione di una carta</string>
<string name="settings_oled_dark_summary">Riduce il consumo della batteria sui display OLED</string>
<string name="settings_category_title_cards">Carte</string>
<string name="settings_category_title_general">Generali</string>
<string name="settings_category_title_privacy">Privacy</string>
</resources>

View File

@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>

View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2" xmlns:tools="http://schemas.android.com/tools">
<string name="action_add">Legg til</string>
<string name="noGiftCards">Klikk på «+» (pluss)-knappen for å legge til eller importer kort fra «⋮»-menyen</string>
<string name="noGiftCards">Klikk på «+» (pluss)-knappen for å legge til eller importere kort fra «⋮»-menyen.</string>
<string name="storeName">Navn</string>
<string name="note">Merknad</string>
<string name="cardId">Kort-ID</string>
@@ -62,7 +62,7 @@
<string name="failedParsingImportUriError">Kunne ikke tolke importerings-URI</string>
<string name="share">Del</string>
<string name="barcodeNoBarcode">Det er ingen strekkode</string>
<string name="noMatchingGiftCards">Resultatløst. Prøv å endre søket.</string>
<string name="noMatchingGiftCards">Ingen resultater. Prøv å endre søket.</string>
<string name="action_search">Søk</string>
<string name="starImage">Favorittstjerne</string>
<string name="unstar">Fjern fra favoritter</string>
@@ -187,7 +187,7 @@
<string name="license">Lisens</string>
<string name="version_history">Versjonshistorikk</string>
<string name="importCatimaMessage">Velg din <i>catima.zip</i>-eksport fra Catima å importere.
\nOpprett den fra «Importer/Eksporter»-menyen i en annen Catima-app ved å trykke på «Eksporter» der først</string>
\nOpprett den fra «Importer/Eksporter»-menyen i en annen Catima-app ved å trykke på «Eksporter» der først.</string>
<string name="source_repository">Kildekode</string>
<string name="on_github">på GitHub</string>
<string name="and_data_usage">og bruk av data</string>
@@ -264,7 +264,7 @@
<string name="switchToBackImage">Bytt til baksidebildet</string>
<string name="switchToBarcode">Bytt til strekkode</string>
<string name="icon_header_click_text">Lang-trykk for å redigere miniatyrbilde</string>
<string name="show_name_below_image_thumbnail">Vis navn under bilde-miniatyrbilde</string>
<string name="show_name_below_image_thumbnail">Vis navn under miniatyrbilde</string>
<string name="show_balance">Vis saldo</string>
<string name="show_note">Vis notat</string>
<string name="show_validity">Vis gyldighet</string>
@@ -283,4 +283,4 @@
<string name="settings_category_title_general">Generelt</string>
<string name="settings_category_title_privacy">Personvern</string>
<string name="settings_oled_dark_summary">Reduserer batteribruk for OLED-skjermer</string>
</resources>
</resources>

View File

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

View File

@@ -8,12 +8,12 @@
<string name="storeName">Nome</string>
<string name="note">Nota</string>
<string name="barcodeType">Tipo de código de barras</string>
<string name="barcodeNoBarcode">Sem código de barras</string>
<string name="barcodeNoBarcode">Não há código de barras</string>
<string name="cancel">Cancelar</string>
<string name="save">Guardar</string>
<string name="edit">Editar</string>
<string name="noGiftCards">Clique no botão + para adicionar um cartão ou importe-o no menu ⋮.</string>
<string name="noBarcode">Sem código de barras</string>
<string name="noBarcode">sem código de barras</string>
<string name="unstar">Retirar dos favoritos</string>
<string name="importOptionFilesystemButton">Do sistema de ficheiros</string>
<string name="importOptionApplicationTitle">Usar outra aplicação</string>
@@ -208,7 +208,7 @@
<string name="editGroup">A editar grupo: <xliff:g>%s</xliff:g></string>
<string name="noGiftCardsGroup">Crie alguns cartões e atribua-os depois ao grupo aqui.</string>
<string name="selectColor">Selecionar cor</string>
<string name="setIcon">Definir ícone</string>
<string name="setIcon">Definir miniatura</string>
<string name="action_show_details">Mostrar detalhes</string>
<string name="action_hide_details">Ocultar detalhes</string>
<string name="shortcutSelectCard">Selecione um cartão</string>
@@ -274,4 +274,20 @@
<string name="openBackImageInGalleryApp">Abrir a imagem traseira na aplicação da galeria</string>
<string name="setBarcodeHeight">Definir altura do código de barras</string>
<string name="donate">Doar</string>
</resources>
<string name="show_validity">Mostrar validade</string>
<string name="show_balance">Mostrar saldo</string>
<string name="permissionReadCardsLabel">Ler Cartas Catima</string>
<string name="permissionReadCardsDescription">Leia seus cartões e todos os seus detalhes, incluindo notas e imagens</string>
<string name="show_note">Mostrar nota</string>
<string name="show_name_below_image_thumbnail">Mostrar nome abaixo da miniatura do ícone</string>
<string name="settings_disable_lockscreen_while_viewing_card_summary">Desativa o bloqueio de tela ao visualizar um cartão</string>
<string name="settings_display_barcode_max_brightness_summary">Necessário para alguns scanners funcionarem</string>
<string name="settings_keep_screen_on_summary">Desativa o tempo limite da tela ao visualizar um cartão</string>
<string name="settings_allow_content_provider_read_title">Permitir que outros aplicativos acessem meus dados</string>
<string name="settings_allow_content_provider_read_summary">Os aplicativos ainda terão que solicitar permissão para receber acesso</string>
<string name="settings_oled_dark_summary">Reduz o uso da bateria em telas OLED</string>
<string name="icon_header_click_text">Pressione e segure para editar o icone</string>
<string name="settings_category_title_cards">cartões</string>
<string name="settings_category_title_general">geral</string>
<string name="settings_category_title_privacy">Privacidade</string>
</resources>

View File

@@ -213,8 +213,8 @@
<string name="noGiftCardsGroup">Создайте несколько карт, а затем распределите их по группам здесь.</string>
<string name="setIcon">Выбор миниатюры</string>
<string name="selectColor">Выбрать цвет</string>
<string name="action_hide_details">Скрыть детали</string>
<string name="action_show_details">Показать детали</string>
<string name="action_hide_details">Скрытие подробностей</string>
<string name="action_show_details">Отображение подробностей</string>
<string name="translate_platform">на Weblate</string>
<string name="shortcutSelectCard">Выбор карты</string>
<string name="options">Параметры</string>

View File

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

View File

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

View File

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

View File

Binary file not shown.

5
docs/STOCARD.md Normal file
View 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.

View File

@@ -0,0 +1 @@
- Oprava občasné havárie

View 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í

View File

@@ -0,0 +1 @@
- Fix rare crash

View 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

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 84 KiB

After

Width:  |  Height:  |  Size: 87 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 81 KiB

After

Width:  |  Height:  |  Size: 81 KiB

View File

@@ -0,0 +1 @@
- Corrección de color de texto incorrecto en el botón "No hay código de barras"

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

View 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

View File

@@ -0,0 +1 @@
- Hacer más visible la posibilidad de crear una cabecera personalizada

View 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

View 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

View 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

View 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

View File

@@ -0,0 +1,2 @@
- Eliminados permisos innecesarios
- Objetivo Android 13

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

View File

@@ -0,0 +1 @@
- Uso de colores de Material You en más dispositivos (actualización de la biblioteca de Google)

View File

@@ -0,0 +1 @@
- Prevenir un fallo al rotar la pantalla en la primera ejecución del asistente.

View 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

View File

@@ -0,0 +1 @@
- Actualizar bibliotecas usadas

View File

@@ -0,0 +1,2 @@
- Pequeñas mejoras de UI
- Corrección del nuevo diseño no disponible en dispositivos con pantallas cuadradas

View File

@@ -0,0 +1 @@
- Soporte para seleccionar exactamente que detalles ver en la vista general de tarjeta

View File

@@ -0,0 +1 @@
- Correcciones varias de RTL

View 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

View File

@@ -0,0 +1 @@
- Correción de error inusual

View 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

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

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

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

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

View File

@@ -0,0 +1 @@
- Actualizar y añadir traducciones

View File

@@ -0,0 +1 @@
- Actualizar traducciones en ruso

View File

@@ -0,0 +1,2 @@
- Habilitar las copias de seguridad de la aplicación
- Actualizar las traducciones en francés y esloveno

View File

@@ -0,0 +1 @@
- A kártyaáttekintőben megjelenítendő pontos részletek kiválasztása

View File

@@ -1,3 +1,4 @@
- Vonalkód renderelés javítások
- Vonalkód-megjelenítési javítások
- Alapvető együttműködési lehetőség külső alkalmazásokkal (Android 6.0+)
- Átszervezett beállítási képernyő
- Az olyan böngészőkből történő importálás javítása, amelyek záró / jelet adnak hozzá a megosztott webcímekhez

View File

@@ -0,0 +1 @@
- Ritka összeomlás javítása

View File

@@ -0,0 +1 @@
- Javított Catima importáló (javítja az importálás után hiányzó kártyákat)

View File

@@ -1,5 +1,5 @@
- Saat mengedit ID kartu, isi dulu ID yang ada untuk memulai. (pull #94 (https://github.com/brarcher/loyalty-card-locker/pull/94))
- Batasi lebar kode batang yang dihasilkan untuk mengurangi penggunaan memori dan kesalahan memori. (pull #103 (https://github.com/brarcher/loyalty-card-locker/pull/103))
- Saat mengedit ID kartu, isi terlebih dahulu ID yang sudah ada untuk memulai. (pull #94 (https://github.com/brarcher/loyalty-card-locker/pull/94))
- Batasi lebar barcode yang dihasilkan untuk mengurangi penggunaan memori dan kesalahan karena kehabisan memori. (pull #103 (https://github.com/brarcher/loyalty-card-locker/pull/103))
- Saat mengedit kartu, ubah tombol "Masukkan Kartu" menjadi "Edit Kartu" jika ID kartu sudah ada. (pull #104 (https://github.com/brarcher/loyalty-card-locker/pull/104))
- Ubah skema warna menjadi lebih lembut dan kompatibel dengan ikon aplikasi, dan ubah tata letak saat melihat kartu menjadi lebih bersih. (pull #107 (https://github.com/brarcher/loyalty-card-locker/pull/107))
- Tambahkan wizard intro yang diluncurkan pada peluncuran pertama aplikasi. (pull #108 (https://github.com/brarcher/loyalty-card-locker/pull/108))
- Menambahkan wizard intro yang diluncurkan pada saat pertama kali aplikasi dijalankan. (pull #108 (https://github.com/brarcher/loyalty-card-locker/pull/108))

View File

@@ -0,0 +1 @@
- Menangani warna header yang hilang dengan lebih anggun

View File

@@ -0,0 +1 @@
- Berbagai perbaikan RTL

View File

@@ -0,0 +1,4 @@
- Peningkatan tampilan barcode
- Interoperabilitas dasar dengan aplikasi eksternal (Android 6.0+)
- Tampilan pengaturan yang ditata ulang
- Perbaikan proses impor dari beberapa browser yang menambahkan garis miring (/) di akhir URL berbagi

View File

@@ -0,0 +1 @@
- Perbaikan pada kerusakan yang jarang terjadi

View File

@@ -1,5 +1,5 @@
- Tambahkan dukungan untuk menambahkan pintasan ke layar beranda saat menambahkan atau mengedit kartu. (pull #155 (https://github.com/brarcher/loyalty-card-locker/pull/155))
- Hapus widget, karena merupakan pengganti pintasan yang buruk. (pull #155 (https://github.com/brarcher/loyalty-card-locker/pull/155))
- Perbaiki mengekspor cadangan di Android 7+. (pull #153 (https://github.com/brarcher/loyalty-card-locker/pull/153))
- Laporkan jenis pantomim yang lebih akurat saat mengekspor data cadangan. (pull #156 (https://github.com/brarcher/loyalty-card-locker/pull/156))
- Menambahkan dukungan untuk menambahkan pintasan ke layar beranda saat menambahkan atau mengedit kartu. (pull #155 (https://github.com/brarcher/loyalty-card-locker/pull/155))
- Widget dihapus karena pengganti pintasan yang buruk. (pull #155 (https://github.com/brarcher/loyalty-card-locker/pull/155))
- Perbaikan pada ekspor cadangan di Android 7+. (pull #153 (https://github.com/brarcher/loyalty-card-locker/pull/153))
- Laporkan jenis mime yang lebih akurat saat mengekspor data cadangan. (pull #156 (https://github.com/brarcher/loyalty-card-locker/pull/156))
- Memperbaiki bug di mana kartu tidak dapat diedit. (pull #155 (https://github.com/brarcher/loyalty-card-locker/pull/155))

View File

@@ -1,4 +1,4 @@
- Tambahkan pengaturan untuk mengontrol kecerahan layar saat menampilkan kode batang (pull #259 (https://github.com/brarcher/loyalty-card-locker/pull/259))
- Tambahkan terjemahan bahasa Yunani (pull #252 (https://github.com/brarcher/loyalty-card-locker/pull/252))
- Tambahkan terjemahan bahasa Slovenia (pull #260 (https://github.com/brarcher/loyalty-card-locker/pull/260))
- Perbarui terjemahan (pull #260 (https://github.com/brarcher/loyalty-card-locker/pull/260), pull #254 (https://github.com/brarcher/loyalty-card-locker/pull /254))
- Tambah pengaturan kecerahan layar saat menampilkan barcode (pull #259 (https://github.com/brarcher/loyalty-card-locker/pull/259))
- Tambah terjemahan Yunani (pull #252 (https://github.com/brarcher/loyalty-card-locker/pull/252))
- Tambah terjemahan Slovenia (pull #260 (https://github.com/brarcher/loyalty-card-locker/pull/260))
- Perbarui terjemahan (pull #260 (https://github.com/brarcher/loyalty-card-locker/pull/260), pull #254 (https://github.com/brarcher/loyalty-card-locker/pull/254))

View File

@@ -1,12 +1,12 @@
- Tambahkan kemampuan untuk mencari kartu (#320 (https://github.com/brarcher/loyalty-card-locker/pull/320))
- Tambahkan kemampuan untuk berbagi dan menerima kartu loyalitas (#321 (https://github.com/brarcher/loyalty-card-locker/pull/321))
- Menambahkan kemampuan untuk mencari kartu (#320 (https://github.com/brarcher/loyalty-card-locker/pull/320))
- Menambahkan kemampuan untuk berbagi dan menerima kartu loyalitas (#211 (https://github.com/brarcher/loyalty-card-locker/pull/321))
- Dukungan mode gelap (#322 (https://github.com/brarcher/loyalty-card-locker/pull/322))
- Kartu loyalitas sekarang dapat tanpa kode batang (misalnya tidak memiliki kode batang) (#324 (https://github.com/brarcher/loyalty-card-locker/pull/324))
- Kartu loyalitas sekarang dapat menjadi barcodeless (misalnya tidak memiliki barcode) (#324 (https://github.com/brarcher/loyalty-card-locker/pull/324))
- Catatan dapat menjangkau beberapa baris (#326 (https://github.com/brarcher/loyalty-card-locker/pull/326))
- Perbaikan dengan ukuran catatan (#319 (https://github.com/brarcher/loyalty-card-locker/pull/319))
- Tingkatkan visibilitas ikon notifikasi dan aplikasi (#330 (https://github.com/brarcher/loyalty-card-locker/pull/330))
- Perbarui SDK target ke Android 10
- Tingkatkan terjemahan berikut:
- Meningkatkan visibilitas notifikasi dan ikon aplikasi (#330 (https://github.com/brarcher/loyalty-card-locker/pull/330))
- Memperbarui SDK target ke Android 10
- Memperbaiki terjemahan berikut ini:
- Jerman
- Italia
- Belanda

View File

@@ -1,2 +1,2 @@
- Izinkan pengguna memasukkan kode batang secara manual. Jika pengguna memilih untuk memasukkan kode batang secara manual, daftar semua gambar kode batang yang valid dan didukung akan ditampilkan. Pengguna kemudian dapat memilih gambar barcode yang sesuai dengan keinginan pengguna. issue #33 (https://github.com/brarcher/loyalty-card-locker/issues/33), pull #44 (https://github.com/brarcher/loyalty-card-locker/pull/44)
- Selesaikan masalah di mana beberapa barcode yang ditampilkan buram. (issue #37 (https://github.com/brarcher/loyalty-card-locker/issues/37))
- Izinkan pengguna memasukkan barcode secara manual. Jika pengguna memilih memasukkan barcode secara manual, tampilkan daftar gambar barcode yang valid dan didukung. Pengguna dapat memilih gambar barcode yang sesuai dengan keinginannya. issue #33 (https://github.com/brarcher/loyalty-card-locker/issues/33), pull #44 (https://github.com/brarcher/loyalty-card-locker/pull/44)
- Mengatasi masalah barcode yang ditampilkan buram. (issue #37 (https://github.com/brarcher/loyalty-card-locker/issues/37))

View File

@@ -1,11 +1,11 @@
- BREAKING CHANGE: Format cadangan berubah, lihat https://github.com/TheLastProject/Catima/wiki/Export-format
- BREAKING CHANGE: Format pencadangan berubah, lihat https://github.com/TheLastProject/Catima/wiki/Export-format
- BREAKING CHANGE: Format berbagi URL berubah, lihat https://github.com/TheLastProject/Catima/wiki/Card-sharing-URL-format
- Memungkinkan untuk aktifkan atau nonaktifkan senter saat memindai
- Tambahkan dukungan UPC-E
- Dukungan penambahan foto depan dan belakang ke setiap kartu
- Dukungan mengimpor file zip yang dilindungi kata sandi
- Dukungan mengimpor dari Stocard (Beta)
- Perbaiki spasi kosong yang tidak berguna dalam catatan dari impor Fidme
- Mendukung format ekspor Voucher Vault baru
- Perbaiki Tombol Mengambang di belakang elemen UI di Android 4
- Memungkinkan untuk mengaktifkan atau menonaktifkan senter saat memindai
- Menambahkan dukungan UPC-E
- Mendukung penambahan foto depan dan belakang ke setiap kartu
- Mendukung pengimporan file zip yang dilindungi kata sandi
- Mendukung pengimporan dari Stocard (Beta)
- Memperbaiki spasi yang tidak berguna pada catatan dari impor Fidme
- Mendukung format ekspor Voucher Vault yang baru
- Perbaiki Tombol Aksi Mengambang yang berada di belakang elemen UI lain di Android 4
- Perbaiki margin atas appbar penampil kartu loyalitas

View File

@@ -1,4 +1,4 @@
- Kecerahan layar meningkat secara maksimal saat menampilkan kartu, untuk membantu pemindai kode batang berhasil menangkap kode batang. (pull #54 (https://github.com/brarcher/loyalty-card-locker/pull/54))
- Tambahkan konfirmasi hapus saat menghapus kartu. (pull #55 (https://github.com/brarcher/loyalty-card-locker/pull/55))
- Tambahkan terjemahan untuk bahasa Jerman (pull #57 (https://github.com/brarcher/loyalty-card-locker/pull/57)) dan Ceko (pull #58 (https://github.com/brarcher/loyalty-card-locker/pull/58)).
- Perubahan klarifikasi untuk terjemahan Italia. (pull #66 (https://github.com/brarcher/loyalty-card-locker/pull/66))
- Kecerahan layar ditingkatkan hingga maksimum ketika menampilkan kartu, untuk membantu pemindai barcode berhasil menangkap barcode. (pull #54 (https://github.com/brarcher/loyalty-card-locker/pull/54))
- Menambahkan konfirmasi hapus saat menghapus kartu. (pull #55 (https://github.com/brarcher/loyalty-card-locker/pull/55))
- Menambahkan terjemahan untuk bahasa Jerman (pull #57 (https://github.com/brarcher/loyalty-card-locker/pull/57)) dan Ceko (pull #58 (https://github.com/brarcher/loyalty-card-locker/pull/58)).
- Perubahan klarifikasi untuk terjemahan bahasa Italia. (pull #66 (https://github.com/brarcher/loyalty-card-locker/pull/66))

View File

@@ -1,7 +1,7 @@
Bagian "Locker" dari nama itu tidak intuitif. Untuk membantu memperbaiki ini, ikon aplikasi baru dibuat oleh betsythefc yang lebih mewakili tujuan aplikasi: untuk menyimpan kartu loyalitas yang menggunakan kode batang. Seiring dengan ikon baru ini, nama aplikasi telah diubah menjadi "Loyalty Card Keychain".
Bagian "Locker" dari nama itu tidak intuitif. Untuk membantu memperbaiki hal ini, sebuah ikon aplikasi baru telah dibuat oleh betsythefc yang lebih baik mewakili tujuan dari aplikasi ini: untuk menyimpan kartu loyalitas yang menggunakan barcode. Seiring dengan ikon baru ini, nama aplikasi telah diubah menjadi "Loyalty Card Keychain".
Fitur tambahan/peningkatan:
- Mengimpor/Mengekspor kartu diubah menjadi lebih fleksibel. (pull #76 (https://github.com/brarcher/loyalty-card-locker/pull/76))
- Mengimpor / Mengekspor kartu diubah menjadi lebih fleksibel. (pull #76 (https://github.com/brarcher/loyalty-card-locker/pull/76))
- Terjemahan untuk bahasa Lituania ditambahkan. (pull #62 (https://github.com/brarcher/loyalty-card-locker/pull/62))
- Terjemahan untuk bahasa Prancis ditambahkan. (pull #80 (https://github.com/brarcher/loyalty-card-locker/pull/80))

View File

@@ -0,0 +1 @@
- Atualizar bibliotecas usadas

View File

@@ -0,0 +1,3 @@
- Pressione e segure o ícone do cartão na atividade de exibição para alterá-lo
- Melhorar o estilo dos botões na tela Grupos
- Corrigir valores longos de código de barras, fazendo com que o código de barras reduza para nada

View File

@@ -0,0 +1,2 @@
- Pequenas melhorias na interface do usuário
- Corrige o novo design que não pode ser usado em dispositivos com telas quadradas

View File

@@ -0,0 +1 @@
- Suporte para selecionar exatamente quais detalhes visualizar na visão geral do cartão

View File

@@ -0,0 +1 @@
- Lide com mais elegância com cores de cabeçalho ausentes

View File

@@ -0,0 +1 @@
- Várias correções de RTL

View File

@@ -0,0 +1,4 @@
- Melhorias na renderização do código de barras
- Interoperabilidade básica com aplicativos externos (Android 6.0+)
- Tela de configurações reorganizada
- Corrige a importação de alguns navegadores que adicionam um / à direita no URL de compartilhamento

View File

@@ -0,0 +1 @@
- Corrigir falha rara

View File

@@ -1,3 +1,4 @@
- Улучшения визуализации штрих‐кодов
- Базовая совместимость с внешними приложениями (Android 6.0+)
- Реорганизация экрана настроек
- Исправление импорта из некоторых браузеров, добавляющих в URL-адрес ресурса концевой символ /

Some files were not shown because too many files have changed in this diff Show More