mirror of
https://github.com/CatimaLoyalty/Android.git
synced 2025-12-24 15:47:53 -05:00
Compare commits
989 Commits
v2.21.0
...
feature/ma
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0ddb88b45c | ||
|
|
b326778219 | ||
|
|
81e16d95ac | ||
|
|
ef18bbdf7c | ||
|
|
6562a0177b | ||
|
|
275a427355 | ||
|
|
ce6a79f03d | ||
|
|
dc792c8425 | ||
|
|
680d2173d1 | ||
|
|
e95a20b971 | ||
|
|
56de06abac | ||
|
|
488c542cd5 | ||
|
|
0e0693bdad | ||
|
|
86267a2e38 | ||
|
|
e2171856ff | ||
|
|
3c3a37a9e8 | ||
|
|
573da1e4ff | ||
|
|
1f70446bce | ||
|
|
ff4ef15dfc | ||
|
|
926f3adb18 | ||
|
|
4b2d981cc6 | ||
|
|
93d5c5e03e | ||
|
|
475f071575 | ||
|
|
f77fa88ced | ||
|
|
7fe5c18cb8 | ||
|
|
7e16798b33 | ||
|
|
3edfb68bd7 | ||
|
|
4f307af6bd | ||
|
|
08848af908 | ||
|
|
f4e9b85ae3 | ||
|
|
73ccb0ce65 | ||
|
|
15c5ee7f2c | ||
|
|
f08be0d13f | ||
|
|
aa115e36fd | ||
|
|
cfa45013f4 | ||
|
|
cca01ae5a3 | ||
|
|
44af31f6d4 | ||
|
|
8fb076421a | ||
|
|
89f8ca1387 | ||
|
|
fddb462618 | ||
|
|
97343515a3 | ||
|
|
dcbd2aa390 | ||
|
|
12e7aaa615 | ||
|
|
50f93b87f5 | ||
|
|
b4cefbc15e | ||
|
|
4ce17c3742 | ||
|
|
5035b83ae1 | ||
|
|
dc327a3e24 | ||
|
|
4ddceb83d5 | ||
|
|
31048218af | ||
|
|
6e3a9ca380 | ||
|
|
09134f4c5f | ||
|
|
89c9271726 | ||
|
|
473f8e6b72 | ||
|
|
8310f09641 | ||
|
|
31b9123a8f | ||
|
|
cc4aed6773 | ||
|
|
a868aac579 | ||
|
|
da48eec787 | ||
|
|
4aa56a55fe | ||
|
|
c3daa1cc86 | ||
|
|
00d08d741c | ||
|
|
2f33be6b3c | ||
|
|
d54a3127a6 | ||
|
|
40ecf46b40 | ||
|
|
dd9b6a8fed | ||
|
|
194e1d5c26 | ||
|
|
9360f2a1e0 | ||
|
|
2030b6b08d | ||
|
|
0b2ad13df9 | ||
|
|
dbc60c39b1 | ||
|
|
7db52fe1db | ||
|
|
f8bcfd6bdf | ||
|
|
16f79b4e23 | ||
|
|
b1273a1c8a | ||
|
|
d127d7fce5 | ||
|
|
0aaa4120c7 | ||
|
|
9a11669e6d | ||
|
|
3e7859b265 | ||
|
|
34a3f79f9c | ||
|
|
8b8737fa23 | ||
|
|
cf1a53f1b4 | ||
|
|
fea4c63840 | ||
|
|
d0881151fe | ||
|
|
e692d27973 | ||
|
|
c16a5bc6f6 | ||
|
|
df765410d8 | ||
|
|
0b5e10040d | ||
|
|
9ff6d78345 | ||
|
|
7a35cba4de | ||
|
|
cbfe761cb8 | ||
|
|
e0256d07a5 | ||
|
|
39caf35ae5 | ||
|
|
704a420e5e | ||
|
|
f2605ca7f2 | ||
|
|
c75657c58a | ||
|
|
63ec0cf098 | ||
|
|
589b532e9f | ||
|
|
4c9e057394 | ||
|
|
f7f4156a78 | ||
|
|
f9f235af5c | ||
|
|
f6c74a7da5 | ||
|
|
1367e29bd4 | ||
|
|
f025d72a11 | ||
|
|
30f2eb1fb9 | ||
|
|
e8c2e6ecd0 | ||
|
|
025be0e5e3 | ||
|
|
b9f380a2b8 | ||
|
|
b2632a03b7 | ||
|
|
ad4275ae45 | ||
|
|
2376517858 | ||
|
|
a666885b22 | ||
|
|
62d4b3ab0d | ||
|
|
e20ac7738b | ||
|
|
cef64004d1 | ||
|
|
35d2408a0e | ||
|
|
1055ba1ca0 | ||
|
|
2bbdb873cc | ||
|
|
b22d76ac02 | ||
|
|
6975688b9f | ||
|
|
310800f28a | ||
|
|
525fdf064c | ||
|
|
5f74f1fbb5 | ||
|
|
5caae534fc | ||
|
|
9ebd312c08 | ||
|
|
9e77b671e0 | ||
|
|
7a361c9afe | ||
|
|
6ef86ef29b | ||
|
|
b9bf8e166a | ||
|
|
3d55c2c82c | ||
|
|
ef56aea88d | ||
|
|
40a3fa7b5b | ||
|
|
184d2aeebd | ||
|
|
8f6603dc79 | ||
|
|
68b53467d0 | ||
|
|
9c401c7e85 | ||
|
|
3b0440b9fa | ||
|
|
cd148d2bb0 | ||
|
|
487cffac30 | ||
|
|
0ab557e29c | ||
|
|
05125f86cd | ||
|
|
b35ccd7d58 | ||
|
|
eab6538f1c | ||
|
|
53753690aa | ||
|
|
c42a02e532 | ||
|
|
2d75ebe395 | ||
|
|
8ba860f6c3 | ||
|
|
68a638e93e | ||
|
|
306e413d94 | ||
|
|
c57aefa5a3 | ||
|
|
5668fb1152 | ||
|
|
d62de3d641 | ||
|
|
727eb13a6f | ||
|
|
4ebbe758e3 | ||
|
|
c6071263e0 | ||
|
|
ecb861c9a1 | ||
|
|
4cf16d92d4 | ||
|
|
8fde18c46a | ||
|
|
f63eca0747 | ||
|
|
5053d63a33 | ||
|
|
4d08613b3c | ||
|
|
715df432a9 | ||
|
|
92802fd467 | ||
|
|
f0a6593c1a | ||
|
|
86c4e46293 | ||
|
|
2d4fa0fd85 | ||
|
|
42863418a4 | ||
|
|
ac4ccf2635 | ||
|
|
89762864ff | ||
|
|
22b8f4b387 | ||
|
|
aebb0e84dc | ||
|
|
b75862532c | ||
|
|
f30fa04d56 | ||
|
|
053b51f086 | ||
|
|
f8960d9a1e | ||
|
|
eb84900fab | ||
|
|
8949166ed1 | ||
|
|
23c437580a | ||
|
|
3e46e84b5d | ||
|
|
3146e25a46 | ||
|
|
dc7b1b032b | ||
|
|
18716fb333 | ||
|
|
5879b8716b | ||
|
|
970e4b4a31 | ||
|
|
b25e07d37a | ||
|
|
c1041a09f5 | ||
|
|
a4a70f44e0 | ||
|
|
2e52e7b231 | ||
|
|
a7b1864c6b | ||
|
|
ee3af751fe | ||
|
|
34698c7bdd | ||
|
|
a45875ef25 | ||
|
|
c4df103c02 | ||
|
|
44211accc9 | ||
|
|
7be1ee99ca | ||
|
|
b83dbb3a87 | ||
|
|
7e3a5a9831 | ||
|
|
1b2f939c5a | ||
|
|
29919851f5 | ||
|
|
b255cd63de | ||
|
|
5d022ee1d1 | ||
|
|
ecd8fe6d43 | ||
|
|
340046905d | ||
|
|
aba6dc9070 | ||
|
|
7e75f86aba | ||
|
|
8c021141b0 | ||
|
|
1739ac827a | ||
|
|
2c8bbd3f44 | ||
|
|
2afad63f31 | ||
|
|
e4d2196892 | ||
|
|
b26aced825 | ||
|
|
a9625fc1cf | ||
|
|
d86fb9475f | ||
|
|
b6ea845236 | ||
|
|
1e88e0c1cc | ||
|
|
72b8781eec | ||
|
|
dd8c63b088 | ||
|
|
ec9affd8c3 | ||
|
|
605e9711fa | ||
|
|
6dfbb169df | ||
|
|
f671c6b0d1 | ||
|
|
b98ee46566 | ||
|
|
3353cf288f | ||
|
|
a408f8d727 | ||
|
|
2d7bb02d1a | ||
|
|
38c8e38ed6 | ||
|
|
28b95b8f75 | ||
|
|
c1ebbdb997 | ||
|
|
fadba7a11c | ||
|
|
fd61434565 | ||
|
|
398dff4b3c | ||
|
|
5c95b750b2 | ||
|
|
9851c0a2fa | ||
|
|
c121f846c5 | ||
|
|
c20ba027cf | ||
|
|
e5dd26b8ee | ||
|
|
3b449464ac | ||
|
|
3f4d4e38cd | ||
|
|
30ccd03686 | ||
|
|
5493947c28 | ||
|
|
4172903b42 | ||
|
|
00b1368176 | ||
|
|
09fee5628f | ||
|
|
7a7a2f8361 | ||
|
|
a9ced56023 | ||
|
|
a4af171598 | ||
|
|
164c82a779 | ||
|
|
608c3ab863 | ||
|
|
379a71c7ad | ||
|
|
ee6a6dffcf | ||
|
|
1d6a393914 | ||
|
|
5c4a905ac0 | ||
|
|
91ce71ea68 | ||
|
|
f07ac3e026 | ||
|
|
649f2c47b4 | ||
|
|
62dcc373ed | ||
|
|
e9b04adec6 | ||
|
|
636be16bdd | ||
|
|
5ad28f37b8 | ||
|
|
5ff6059e86 | ||
|
|
b9df712394 | ||
|
|
790fd7e48f | ||
|
|
bb43266a01 | ||
|
|
450cfce84a | ||
|
|
6cef56b38b | ||
|
|
fbb7cf7e9c | ||
|
|
9ab2a6a5b2 | ||
|
|
682fc8303c | ||
|
|
aa1274566b | ||
|
|
d8cd581cb0 | ||
|
|
2cd00f9103 | ||
|
|
ae07f94b25 | ||
|
|
22d671263a | ||
|
|
227f30361f | ||
|
|
b964652b83 | ||
|
|
f926ffa1d0 | ||
|
|
d03b8b5635 | ||
|
|
4e5fea7a52 | ||
|
|
95cb6c0a08 | ||
|
|
d350d0b2c7 | ||
|
|
ac0f6f6f3e | ||
|
|
ab030ba002 | ||
|
|
93103c8c6d | ||
|
|
576ec1e6de | ||
|
|
b602ce5d78 | ||
|
|
0ecd38ed1c | ||
|
|
d48e02463c | ||
|
|
b9d9c8d2e3 | ||
|
|
3ae665b70f | ||
|
|
9cf9959b6b | ||
|
|
d11e2c166b | ||
|
|
f783be7a4f | ||
|
|
9ee96b88e8 | ||
|
|
ba896fc1db | ||
|
|
47f1ea80b6 | ||
|
|
b5efa28e85 | ||
|
|
d456a8920d | ||
|
|
4ef0c96b29 | ||
|
|
343c77f9e3 | ||
|
|
1425d4af58 | ||
|
|
48510494eb | ||
|
|
d5d53b241a | ||
|
|
901c2d8154 | ||
|
|
84d7e15b5c | ||
|
|
b8fa4d7060 | ||
|
|
da9e3bb6b2 | ||
|
|
3a5973a04d | ||
|
|
673e64924b | ||
|
|
5f99f2b17e | ||
|
|
bf05103955 | ||
|
|
5ea6155c39 | ||
|
|
abb1cd29f0 | ||
|
|
52363cdff4 | ||
|
|
ac4f4e3a9a | ||
|
|
555387e20d | ||
|
|
70ae8ff167 | ||
|
|
86512532f1 | ||
|
|
ebf6318aa2 | ||
|
|
fd482a4cba | ||
|
|
54d91dc8a1 | ||
|
|
9fa7fe388f | ||
|
|
1415d8da3e | ||
|
|
0ad5de18e1 | ||
|
|
9659a2a2cd | ||
|
|
1dd894bd27 | ||
|
|
0d9151294e | ||
|
|
09dda99afc | ||
|
|
a4fb91b9aa | ||
|
|
036b821b28 | ||
|
|
479a35657f | ||
|
|
eca9d1c74c | ||
|
|
95055f1ce6 | ||
|
|
f4a5ae74d6 | ||
|
|
16952186c7 | ||
|
|
7d12279b0d | ||
|
|
15a49344a3 | ||
|
|
ff15ce0615 | ||
|
|
57144c4cd1 | ||
|
|
50387f55d4 | ||
|
|
0fa07d3d0b | ||
|
|
05516f88fc | ||
|
|
841a5e3ddb | ||
|
|
2889b45c51 | ||
|
|
9ebb53b224 | ||
|
|
14b8b3bd7a | ||
|
|
3e4c3e9852 | ||
|
|
fa417cdfe5 | ||
|
|
a9863d1921 | ||
|
|
f2b7a63a02 | ||
|
|
8e0a40ee3f | ||
|
|
21064803eb | ||
|
|
031b751a9d | ||
|
|
bb9b640aa5 | ||
|
|
1681d077b6 | ||
|
|
3500444e0c | ||
|
|
e05ebbe607 | ||
|
|
b3bb20c795 | ||
|
|
b0315f279a | ||
|
|
9e788cb604 | ||
|
|
090f3dc80b | ||
|
|
e4f2f8f46b | ||
|
|
e5ecd59569 | ||
|
|
441ae9587e | ||
|
|
0c1e16592d | ||
|
|
02659f840e | ||
|
|
eca5ec10d3 | ||
|
|
dbc8df018d | ||
|
|
874b887da1 | ||
|
|
5367324219 | ||
|
|
6df58e0574 | ||
|
|
c8b6328cc6 | ||
|
|
90616214f0 | ||
|
|
e3bce7eb50 | ||
|
|
b2d1660cf8 | ||
|
|
3de24bb6f0 | ||
|
|
b4e403d214 | ||
|
|
b66351ab58 | ||
|
|
c9d775f426 | ||
|
|
6d4af3d86b | ||
|
|
37803bd906 | ||
|
|
f3fcea7ab1 | ||
|
|
9a67ea039c | ||
|
|
bf94d208bd | ||
|
|
28c0b488e6 | ||
|
|
dc926bbfe7 | ||
|
|
00d5fa4c2d | ||
|
|
9aca91fb86 | ||
|
|
bdab8624b5 | ||
|
|
9bdfe64c79 | ||
|
|
f5f7e40219 | ||
|
|
2f51e7db68 | ||
|
|
2cb9846f42 | ||
|
|
1dbe46b97e | ||
|
|
7e5611b062 | ||
|
|
d3a7acd9ab | ||
|
|
c023a9787d | ||
|
|
6de0ed6956 | ||
|
|
aa5f0a7bf8 | ||
|
|
6dd5bd957c | ||
|
|
c69df5edfd | ||
|
|
758d034c31 | ||
|
|
7bcf82a619 | ||
|
|
fd755185b9 | ||
|
|
0ac57be5f1 | ||
|
|
bb929a48ea | ||
|
|
218850be33 | ||
|
|
ec07cceced | ||
|
|
71ac1c5dad | ||
|
|
b16ecfbb2c | ||
|
|
c64bfbabc0 | ||
|
|
fc8073f717 | ||
|
|
7f56617a3c | ||
|
|
f5dc2930b5 | ||
|
|
4bdc0fef70 | ||
|
|
2f3c96b97c | ||
|
|
6ff11e22e1 | ||
|
|
0b188f2ba7 | ||
|
|
827d5ba3f9 | ||
|
|
05fea6e208 | ||
|
|
6064d29cb0 | ||
|
|
0a92daa77a | ||
|
|
bb065aead4 | ||
|
|
56467c2680 | ||
|
|
1941db1e52 | ||
|
|
c595981996 | ||
|
|
0145a820de | ||
|
|
224ab7f2e7 | ||
|
|
1b47a62c70 | ||
|
|
f241bad3e4 | ||
|
|
c8657870af | ||
|
|
0ed7a0a6e1 | ||
|
|
325330c542 | ||
|
|
2506f60348 | ||
|
|
5bf6f52425 | ||
|
|
fb1728e5de | ||
|
|
d321f733c1 | ||
|
|
8ff46f8754 | ||
|
|
a061978c93 | ||
|
|
f3bfc9b278 | ||
|
|
43ebe5e029 | ||
|
|
2357f4d430 | ||
|
|
7869feb3a1 | ||
|
|
53ce856c33 | ||
|
|
aa321a71ad | ||
|
|
f721237fbc | ||
|
|
194a7ae365 | ||
|
|
1aa1d8bcc1 | ||
|
|
a8355f609b | ||
|
|
7c766d0832 | ||
|
|
3abe99470e | ||
|
|
6937342226 | ||
|
|
e229aa9565 | ||
|
|
5fd629bdbc | ||
|
|
4c261e1e04 | ||
|
|
c8be038b81 | ||
|
|
268b4cf827 | ||
|
|
ada8fd9bd4 | ||
|
|
285c3d6a38 | ||
|
|
07efe99630 | ||
|
|
a7d3a1c30d | ||
|
|
fcc995397f | ||
|
|
cef0d037f7 | ||
|
|
6e390717da | ||
|
|
f785586c71 | ||
|
|
e66c5e36fb | ||
|
|
fd0ca37a6a | ||
|
|
e926a397a7 | ||
|
|
4f9a6b307f | ||
|
|
df60f6adc2 | ||
|
|
d6287a3cf1 | ||
|
|
7f81d727f0 | ||
|
|
03239f0cab | ||
|
|
4b6497cba2 | ||
|
|
99008291fb | ||
|
|
079049722c | ||
|
|
73200e1ac3 | ||
|
|
39b816ddb1 | ||
|
|
be43c1633d | ||
|
|
f6c9613731 | ||
|
|
99de8cffd8 | ||
|
|
a867265c66 | ||
|
|
f43df37c80 | ||
|
|
cb70b1229f | ||
|
|
b0b54a1065 | ||
|
|
4ccfa7fb84 | ||
|
|
9932b10652 | ||
|
|
60fe6a13ff | ||
|
|
0bf30f08a1 | ||
|
|
112fca041b | ||
|
|
59af5cb07b | ||
|
|
9182837ae8 | ||
|
|
3f3d85c8b6 | ||
|
|
29c068df47 | ||
|
|
03a480f3b9 | ||
|
|
0feb0fcd16 | ||
|
|
7f1a221fec | ||
|
|
7b52338669 | ||
|
|
0a2925eafa | ||
|
|
92f37376a6 | ||
|
|
08fbc30a29 | ||
|
|
91dbcf339f | ||
|
|
24fe6886c9 | ||
|
|
270546d022 | ||
|
|
fe278324f3 | ||
|
|
c7c18a0a14 | ||
|
|
d6fcb71722 | ||
|
|
32ce1e02aa | ||
|
|
8a8faac201 | ||
|
|
2793f38d19 | ||
|
|
654e3f7843 | ||
|
|
2a6fe92b1b | ||
|
|
b3cd102332 | ||
|
|
e12bfa81dc | ||
|
|
e859e7849d | ||
|
|
7a4c9ce84f | ||
|
|
6419ca7d11 | ||
|
|
d1b23c712a | ||
|
|
ef5b525401 | ||
|
|
a8d5cb9ef8 | ||
|
|
42090310d4 | ||
|
|
9d8da2a451 | ||
|
|
fb488af2b7 | ||
|
|
71052c9f69 | ||
|
|
2a285f6d21 | ||
|
|
e668ae7d9d | ||
|
|
4a197c542a | ||
|
|
720c6b6510 | ||
|
|
d932ebb9f2 | ||
|
|
77b6d8724b | ||
|
|
5ff002da51 | ||
|
|
16b3fbdf9c | ||
|
|
4f11eceb3b | ||
|
|
c11d777bcc | ||
|
|
49cda90ac6 | ||
|
|
63e4f4fd0d | ||
|
|
c44845582c | ||
|
|
b0771d3b01 | ||
|
|
285a7a9aa7 | ||
|
|
818c5e1643 | ||
|
|
7ffaf65635 | ||
|
|
bf50dcbaf3 | ||
|
|
3e83035cf6 | ||
|
|
e921aab322 | ||
|
|
1ea125d7f9 | ||
|
|
c828ec516c | ||
|
|
5704ce1381 | ||
|
|
ca33184e81 | ||
|
|
82db76f3a9 | ||
|
|
ab6955cf9b | ||
|
|
a1c81c3223 | ||
|
|
43e7114f16 | ||
|
|
d1b84097f6 | ||
|
|
2c9b3896c9 | ||
|
|
3dbee69847 | ||
|
|
4f9414ab4f | ||
|
|
5a4043bf90 | ||
|
|
f742639925 | ||
|
|
e50cca781e | ||
|
|
1abff7c900 | ||
|
|
8f24fcee96 | ||
|
|
f3e1bfc503 | ||
|
|
65e3b87df4 | ||
|
|
2dbe9ebb8a | ||
|
|
ccfbe1f679 | ||
|
|
3a4467db83 | ||
|
|
419d28163e | ||
|
|
89483c4c4e | ||
|
|
4e921eb5d7 | ||
|
|
0cf266c6f1 | ||
|
|
20f9f971a1 | ||
|
|
0150ca81cf | ||
|
|
4fd6092f8d | ||
|
|
a488003e4a | ||
|
|
9e0e8bab75 | ||
|
|
0eca05d656 | ||
|
|
1c5d102a59 | ||
|
|
400d0f4e22 | ||
|
|
4c0593d333 | ||
|
|
87d82b1b6d | ||
|
|
8ea4a3309d | ||
|
|
18088b30bd | ||
|
|
875309bc5e | ||
|
|
95520a7051 | ||
|
|
ab9391db38 | ||
|
|
e43e4b37ae | ||
|
|
f2d856b661 | ||
|
|
8c6c5fb767 | ||
|
|
bb4ec8f8be | ||
|
|
e5fd277198 | ||
|
|
93b089c646 | ||
|
|
5e37a9b89f | ||
|
|
27cc7b9008 | ||
|
|
0749bbd432 | ||
|
|
9b1e0537c9 | ||
|
|
3b2b45f08f | ||
|
|
60a5e60ae0 | ||
|
|
77513d5528 | ||
|
|
15fa483490 | ||
|
|
40ba47d60c | ||
|
|
986327d141 | ||
|
|
36f0dde3b2 | ||
|
|
f0a695a616 | ||
|
|
810b0f3c14 | ||
|
|
fcf08a8900 | ||
|
|
939bf6a788 | ||
|
|
935f899d0f | ||
|
|
6c8d79ef36 | ||
|
|
178707b482 | ||
|
|
0f2c9cb63d | ||
|
|
0869a00f0c | ||
|
|
b63b8c4ea7 | ||
|
|
76d77a2af4 | ||
|
|
dade0b7a54 | ||
|
|
68801d6958 | ||
|
|
5c3ec85043 | ||
|
|
6332e396cb | ||
|
|
7f022feb6a | ||
|
|
1fa8487474 | ||
|
|
0e040a73eb | ||
|
|
65acb885af | ||
|
|
ee28957cbd | ||
|
|
0e9f4d9f2b | ||
|
|
58974cf738 | ||
|
|
c56c931a70 | ||
|
|
c83e8b6682 | ||
|
|
449ab86b5a | ||
|
|
8f18bbe0ac | ||
|
|
992e61b88a | ||
|
|
47b92fb38c | ||
|
|
b9963bb967 | ||
|
|
d642d57b85 | ||
|
|
9e0e8f6d7f | ||
|
|
e486755e6a | ||
|
|
1bb803ef0a | ||
|
|
f729f9758b | ||
|
|
abe3bc7d87 | ||
|
|
454bb6a1aa | ||
|
|
2d4d4e2309 | ||
|
|
e7c4010e8d | ||
|
|
cc363e0c04 | ||
|
|
deacc4a69b | ||
|
|
ac6e6e0985 | ||
|
|
fd37a2708f | ||
|
|
85ea314dc9 | ||
|
|
7fd5fe6ee0 | ||
|
|
2c35ad3044 | ||
|
|
210e305bae | ||
|
|
b48de921fc | ||
|
|
ebc2bfcbbb | ||
|
|
b1ec67928f | ||
|
|
2beafe954b | ||
|
|
25b6c4d8cc | ||
|
|
a3f1b9c0b9 | ||
|
|
14d5545c88 | ||
|
|
0977b9d20a | ||
|
|
bad0990dfb | ||
|
|
98c34abd66 | ||
|
|
b4d1463453 | ||
|
|
659b86e31a | ||
|
|
d63e269172 | ||
|
|
77aa768c5c | ||
|
|
8c74d4c2d0 | ||
|
|
a6280108ec | ||
|
|
829630219f | ||
|
|
22a736d7fd | ||
|
|
4418492e7c | ||
|
|
c21e12bbf0 | ||
|
|
9af05be128 | ||
|
|
e036443233 | ||
|
|
7e605f284d | ||
|
|
e712b8e18d | ||
|
|
2ff137ee04 | ||
|
|
3b45802a7f | ||
|
|
0de4847f6a | ||
|
|
47e2566043 | ||
|
|
18c17796b9 | ||
|
|
4b2b999653 | ||
|
|
3b51121c9e | ||
|
|
2fc9581643 | ||
|
|
68f3f37e23 | ||
|
|
38658d0aa7 | ||
|
|
339750e97c | ||
|
|
510995a5c5 | ||
|
|
a039d5de9e | ||
|
|
df11bd8f69 | ||
|
|
17c80573bd | ||
|
|
bc71c02e87 | ||
|
|
d892fe40ba | ||
|
|
06839f6ddb | ||
|
|
ee57703ffc | ||
|
|
e780f5fb87 | ||
|
|
6b9a0a0696 | ||
|
|
6653a940ed | ||
|
|
47821752f0 | ||
|
|
bf05bd7e56 | ||
|
|
2dd622b9c4 | ||
|
|
643527a7fb | ||
|
|
0aae4c9c64 | ||
|
|
14c5b756f7 | ||
|
|
ded3c63ec5 | ||
|
|
e3a22e425b | ||
|
|
275d387f3c | ||
|
|
fbbad75c15 | ||
|
|
a55e9da067 | ||
|
|
a5116395c8 | ||
|
|
5cf41ed664 | ||
|
|
bbde3ec3b8 | ||
|
|
db56d56e3b | ||
|
|
f4c0628366 | ||
|
|
1e6641a884 | ||
|
|
b9bd3f5967 | ||
|
|
28eaac0c67 | ||
|
|
5690ca03e7 | ||
|
|
71f7b21112 | ||
|
|
f9ece83f2b | ||
|
|
3bab0f43eb | ||
|
|
941fa929dd | ||
|
|
46846d2448 | ||
|
|
7b7c1b88b9 | ||
|
|
0926bb71e1 | ||
|
|
cfc4ce7c3c | ||
|
|
1f217dd846 | ||
|
|
6c74b95e90 | ||
|
|
9fca77d561 | ||
|
|
1f0873aab4 | ||
|
|
ca3a09740a | ||
|
|
c104de839e | ||
|
|
0f4380c1e2 | ||
|
|
64e3b047d9 | ||
|
|
729639e0e6 | ||
|
|
3e79147673 | ||
|
|
59a656c422 | ||
|
|
bdf8994fed | ||
|
|
3aa595083b | ||
|
|
37f1183208 | ||
|
|
839496aa04 | ||
|
|
4a7a6b109c | ||
|
|
2b2d5ca7cf | ||
|
|
463af746fa | ||
|
|
d75b79fce4 | ||
|
|
2ec29da6b1 | ||
|
|
e80bebe887 | ||
|
|
1555b3b24b | ||
|
|
12f42f86a5 | ||
|
|
a74e17db10 | ||
|
|
96335d3ee8 | ||
|
|
fb3d945f51 | ||
|
|
843ffbb87e | ||
|
|
99e19321f0 | ||
|
|
4e5a90eb93 | ||
|
|
9745ea671e | ||
|
|
94a63d6e0c | ||
|
|
725ca3b9ca | ||
|
|
46a2164143 | ||
|
|
5304be6f54 | ||
|
|
8321e796f5 | ||
|
|
c1f82e90be | ||
|
|
758f265638 | ||
|
|
590020bb6f | ||
|
|
f18c2a2d0c | ||
|
|
3a17ee83e0 | ||
|
|
f36d4aebb6 | ||
|
|
71cb1cace4 | ||
|
|
1e4e035281 | ||
|
|
7da4eb6587 | ||
|
|
d454864fa7 | ||
|
|
f5e6d7be71 | ||
|
|
967a17242d | ||
|
|
6c5dd7a713 | ||
|
|
139b144cb3 | ||
|
|
5deacf7ecc | ||
|
|
6e49aea713 | ||
|
|
68257ce3ad | ||
|
|
149a1caeff | ||
|
|
07e5788cb2 | ||
|
|
c072e2e70d | ||
|
|
0701b9b3de | ||
|
|
f84242d97c | ||
|
|
79f35ed715 | ||
|
|
eff4f3f8df | ||
|
|
ee405f670d | ||
|
|
7dcb9b336d | ||
|
|
923e6dc062 | ||
|
|
2ea04e8715 | ||
|
|
f264941ae4 | ||
|
|
e6ef5c9bb2 | ||
|
|
024cc2d50e | ||
|
|
48b62f6aea | ||
|
|
ff07fe71cc | ||
|
|
402cf57c29 | ||
|
|
e19ac0d0c2 | ||
|
|
fc5d4a6435 | ||
|
|
50754a3430 | ||
|
|
0375f6dbe5 | ||
|
|
deb808ffb6 | ||
|
|
166c1e7bc6 | ||
|
|
bfa19d0166 | ||
|
|
f92805ec64 | ||
|
|
73e6e9f34a | ||
|
|
6a76681412 | ||
|
|
f05a5dde1e | ||
|
|
4a93ba3478 | ||
|
|
af43138ae9 | ||
|
|
dcd6e4b9a9 | ||
|
|
0ce2395605 | ||
|
|
89dbc9e9aa | ||
|
|
788a8f0efe | ||
|
|
d0c9ae2a4a | ||
|
|
c000a8129c | ||
|
|
bfb4fdb61c | ||
|
|
1929401d3e | ||
|
|
eef13a1a91 | ||
|
|
64f340d798 | ||
|
|
f12422fc07 | ||
|
|
d78315ef12 | ||
|
|
af8c3eeb54 | ||
|
|
16e4205028 | ||
|
|
399aa767d2 | ||
|
|
96c735cf80 | ||
|
|
50a8395ec6 | ||
|
|
c720ca1085 | ||
|
|
d82088f66a | ||
|
|
1fce3e17f6 | ||
|
|
569db96f81 | ||
|
|
b7cb1dffc1 | ||
|
|
e17fc66d35 | ||
|
|
8dac7ae9d1 | ||
|
|
28a0417fa8 | ||
|
|
58d4bd7f47 | ||
|
|
1779aef162 | ||
|
|
557ec68428 | ||
|
|
96a7c8ee36 | ||
|
|
3a9b92231e | ||
|
|
f1753ea943 | ||
|
|
36ab78ec6e | ||
|
|
8a3a782558 | ||
|
|
2c5606bf0a | ||
|
|
5eb36aad5b | ||
|
|
b028d32e2c | ||
|
|
dfefdda9f9 | ||
|
|
bc1cc68f27 | ||
|
|
386a5f75b0 | ||
|
|
d30a9fd1e5 | ||
|
|
0c8e5576b8 | ||
|
|
93e40c08aa | ||
|
|
24eb1e9627 | ||
|
|
19e6b8bea9 | ||
|
|
87f844943e | ||
|
|
718738ff78 | ||
|
|
d7bb019068 | ||
|
|
f925404ab7 | ||
|
|
38495546e0 | ||
|
|
36e7ea5b20 | ||
|
|
85d53ae3c7 | ||
|
|
e9b6b5682e | ||
|
|
0abc583d10 | ||
|
|
8c91d30b4c | ||
|
|
76e8715ab2 | ||
|
|
f5f2edca75 | ||
|
|
e648e22ecc | ||
|
|
02b020e9e5 | ||
|
|
e7dc3cd511 | ||
|
|
eb3ac53e46 | ||
|
|
dd683b4a8c | ||
|
|
e90ba5d2db | ||
|
|
07cff7eac4 | ||
|
|
3eea18fc82 | ||
|
|
5d3ceb6d49 | ||
|
|
3dc7a25d88 | ||
|
|
95e02d1646 | ||
|
|
81ecf8ba92 | ||
|
|
bf0e1a2e77 | ||
|
|
4d1c9244fd | ||
|
|
1f1e523100 | ||
|
|
cb32cf9e52 | ||
|
|
af812d8cda | ||
|
|
c8a7addc52 | ||
|
|
3e64d7c5ad | ||
|
|
277462a939 | ||
|
|
24623d5c58 | ||
|
|
4b25b7ad39 | ||
|
|
4016d5499b | ||
|
|
d8b96a8c5f | ||
|
|
614753303f | ||
|
|
50a344b97f | ||
|
|
2cd6da6ffc | ||
|
|
c03fba133f | ||
|
|
f3cba588f6 | ||
|
|
ad364ad0ac | ||
|
|
f4cff85d93 | ||
|
|
cbc86ff131 | ||
|
|
5f733474a9 | ||
|
|
89a13cecf5 | ||
|
|
fd2400eaf5 | ||
|
|
9598f2f4ff | ||
|
|
acca53787c | ||
|
|
060e360344 | ||
|
|
9ef014e05c | ||
|
|
4cb27d3bd5 | ||
|
|
95e21cc112 | ||
|
|
d5f915a290 | ||
|
|
42e89bb5cc | ||
|
|
048fb48300 | ||
|
|
b1c82dbae0 | ||
|
|
ea8c6f96f7 | ||
|
|
201ec78694 | ||
|
|
05a03455c1 | ||
|
|
63f4cbb8ca | ||
|
|
a444607476 | ||
|
|
89102ad0bf | ||
|
|
d2623b8690 | ||
|
|
80ddd48184 | ||
|
|
410a619a70 | ||
|
|
1771f42860 | ||
|
|
1e7af7ab4e | ||
|
|
f4c5af04e3 | ||
|
|
5bcd2cdc32 | ||
|
|
c69a5ae4d2 | ||
|
|
6bd750a60b | ||
|
|
8d77cc3565 | ||
|
|
7a99e0056d | ||
|
|
03e07bc48d | ||
|
|
460d6c2b71 | ||
|
|
9f946094db | ||
|
|
4e92f82176 | ||
|
|
7b905ac120 | ||
|
|
55c5ec929c | ||
|
|
5441231f03 | ||
|
|
1818d24bc0 | ||
|
|
ca88c070c3 | ||
|
|
1be387c4ec | ||
|
|
fa3a956d69 | ||
|
|
8fd244e3a3 | ||
|
|
a0e2fe11dd | ||
|
|
893f34e72a | ||
|
|
cf13a9fc60 | ||
|
|
c6e6d96313 | ||
|
|
20ed9cac88 | ||
|
|
ae6bd937a9 | ||
|
|
e4b69e5cc5 | ||
|
|
5b1062b8d1 | ||
|
|
e06009852e | ||
|
|
50268f6bd1 | ||
|
|
a85e28d46d | ||
|
|
78b6be911f | ||
|
|
bb80478650 | ||
|
|
e0c06cc480 | ||
|
|
b8a508649c | ||
|
|
a8ce37d936 | ||
|
|
43015abbad | ||
|
|
47c8dff52d | ||
|
|
436cf7a068 | ||
|
|
31cc3cd5d0 | ||
|
|
3c11c2ef1e | ||
|
|
fac70f0210 | ||
|
|
6e99a29312 | ||
|
|
5b67ecf157 | ||
|
|
312470cf20 | ||
|
|
4371f46ff8 | ||
|
|
5468415b04 | ||
|
|
6a68ad5d19 | ||
|
|
7576505044 | ||
|
|
b34a43902a | ||
|
|
d3524a50a3 | ||
|
|
6508a6d5f7 | ||
|
|
49a6cf8ae3 | ||
|
|
b4238e0072 | ||
|
|
374170bf05 | ||
|
|
ab11345c3d | ||
|
|
10498ce1a4 | ||
|
|
28901487ff | ||
|
|
8414f51ee8 | ||
|
|
94f8adb6d7 | ||
|
|
844a921a1a | ||
|
|
ae8be3eda8 | ||
|
|
c4c15dbef8 | ||
|
|
ac72035500 | ||
|
|
abff3bcd39 | ||
|
|
05aea28602 | ||
|
|
e4c4dbf5a0 | ||
|
|
bb2393b6c6 | ||
|
|
fb330d16b5 | ||
|
|
c8f1b986ec | ||
|
|
49a2c93d28 | ||
|
|
0992ac4099 | ||
|
|
9ff29af616 | ||
|
|
fbc696047b | ||
|
|
63c3330571 | ||
|
|
e97f7c8645 |
13
.github/dependabot.yml
vendored
13
.github/dependabot.yml
vendored
@@ -1,11 +1,10 @@
|
||||
# To get started with Dependabot version updates, you'll need to specify which
|
||||
# package ecosystems to update and where the package manifests are located.
|
||||
# Please see the documentation for all configuration options:
|
||||
# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
|
||||
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "gradle" # See documentation for possible values
|
||||
directory: "/" # Location of package manifests
|
||||
- package-ecosystem: "gradle"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
|
||||
41
.github/workflows/android.yml
vendored
41
.github/workflows/android.yml
vendored
@@ -1,41 +1,54 @@
|
||||
name: Android CI
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- main
|
||||
- staging
|
||||
- trying
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
|
||||
- main
|
||||
permissions:
|
||||
actions: none
|
||||
checks: none
|
||||
contents: read
|
||||
deployments: none
|
||||
discussions: none
|
||||
id-token: none
|
||||
issues: none
|
||||
packages: none
|
||||
pages: none
|
||||
pull-requests: none
|
||||
repository-projects: none
|
||||
security-events: none
|
||||
statuses: none
|
||||
env:
|
||||
JAVA_HOME: /usr/lib/jvm/java-17-openjdk-amd64
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v4.0.0
|
||||
- name: Fail on bad translations
|
||||
run: if grep -ri "<xliff" app/src/main/res/values*/strings.xml; then echo "Invalidly escaped translations found"; exit 1; fi
|
||||
- uses: gradle/wrapper-validation-action@v1
|
||||
- name: set up JDK 11
|
||||
uses: actions/setup-java@v2
|
||||
with:
|
||||
distribution: 'adopt'
|
||||
java-version: '11'
|
||||
- name: set up OpenJDK 17
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y openjdk-17-jdk-headless
|
||||
sudo update-alternatives --auto java
|
||||
- name: Build
|
||||
run: ./gradlew assembleRelease
|
||||
- name: Check lint
|
||||
run: ./gradlew lintRelease
|
||||
- name: Run unit tests
|
||||
run: ./gradlew testReleaseUnitTest || ./gradlew testReleaseUnitTest
|
||||
run: timeout 5m ./gradlew testReleaseUnitTest || { ./gradlew --stop && timeout 5m ./gradlew testReleaseUnitTest; }
|
||||
- name: SpotBugs
|
||||
run: ./gradlew spotbugsRelease
|
||||
- name: Archive test results
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v2
|
||||
uses: actions/upload-artifact@v3.1.3
|
||||
with:
|
||||
name: test-results
|
||||
path: app/build/reports
|
||||
|
||||
24
.github/workflows/autoclose-needs-info.yml
vendored
24
.github/workflows/autoclose-needs-info.yml
vendored
@@ -1,24 +0,0 @@
|
||||
name: 'Close issues and PRs needing info for too long'
|
||||
on:
|
||||
schedule:
|
||||
- cron: '30 1 * * *'
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
stale:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/stale@v4
|
||||
with:
|
||||
days-before-stale: -1
|
||||
days-before-close: 90
|
||||
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'
|
||||
remove-stale-when-updated: false
|
||||
enable-statistics: true
|
||||
34
.github/workflows/calibreapp-image-actions.yml
vendored
34
.github/workflows/calibreapp-image-actions.yml
vendored
@@ -1,34 +0,0 @@
|
||||
name: Compress Images on Push to Master
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
paths:
|
||||
- '**.jpg'
|
||||
- '**.jpeg'
|
||||
- '**.png'
|
||||
- '**.webp'
|
||||
jobs:
|
||||
build:
|
||||
# Only run on Pull Requests within the same repository, and not from forks.
|
||||
if: github.event.pull_request.head.repo.full_name == github.repository
|
||||
name: calibreapp/image-actions
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Repo
|
||||
uses: actions/checkout@v2
|
||||
- name: Compress Images
|
||||
id: calibre
|
||||
uses: calibreapp/image-actions@1.1.0
|
||||
with:
|
||||
githubToken: ${{ secrets.GITHUB_TOKEN }}
|
||||
ignorePaths: 'app/src/test'
|
||||
compressOnly: true
|
||||
- name: Create New Pull Request If Needed
|
||||
if: steps.calibre.outputs.markdown != ''
|
||||
uses: peter-evans/create-pull-request@v3
|
||||
with:
|
||||
title: Compressed Images
|
||||
branch-suffix: timestamp
|
||||
commit-message: Compressed Images
|
||||
body: ${{ steps.calibre.outputs.markdown }}
|
||||
26
.github/workflows/changelog-to-fastlane.yml
vendored
26
.github/workflows/changelog-to-fastlane.yml
vendored
@@ -1,9 +1,25 @@
|
||||
name: Convert CHANGELOG to Fastlane
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
||||
- main
|
||||
paths:
|
||||
- 'CHANGELOG.md'
|
||||
permissions:
|
||||
actions: none
|
||||
checks: none
|
||||
contents: write
|
||||
deployments: none
|
||||
discussions: none
|
||||
id-token: none
|
||||
issues: none
|
||||
packages: none
|
||||
pages: none
|
||||
pull-requests: write
|
||||
repository-projects: none
|
||||
security-events: none
|
||||
statuses: none
|
||||
jobs:
|
||||
convert_changelog_to_fastlane:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -11,15 +27,15 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
id: checkout
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4.0.0
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v2
|
||||
uses: actions/setup-python@v4.7.0
|
||||
with:
|
||||
python-version: '3.x'
|
||||
- name: Run converter script
|
||||
run: python .scripts/changelog_to_fastlane.py
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v3
|
||||
uses: peter-evans/create-pull-request@v5.0.2
|
||||
with:
|
||||
title: "Update Fastlane changelogs"
|
||||
commit-message: "Update Fastlane changelogs"
|
||||
|
||||
24
.github/workflows/contributors-to-file.yml
vendored
24
.github/workflows/contributors-to-file.yml
vendored
@@ -1,24 +1,38 @@
|
||||
name: Write contributors to file
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: '3 4 * * 0'
|
||||
|
||||
permissions:
|
||||
actions: none
|
||||
checks: none
|
||||
contents: write
|
||||
deployments: none
|
||||
discussions: none
|
||||
id-token: none
|
||||
issues: none
|
||||
packages: none
|
||||
pages: none
|
||||
pull-requests: write
|
||||
repository-projects: none
|
||||
security-events: none
|
||||
statuses: none
|
||||
jobs:
|
||||
contributors_to_file:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.ref == 'refs/heads/master'
|
||||
if: github.ref == 'refs/heads/main'
|
||||
name: Write contributors to file
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
id: checkout
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4.0.0
|
||||
- name: Update contributors
|
||||
id: update_contributors
|
||||
uses: TheLastProject/contributors-to-file-action@v2
|
||||
uses: TheLastProject/contributors-to-file-action@v3.0.1
|
||||
with:
|
||||
file_in_repo: app/src/main/res/raw/contributors.txt
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v3
|
||||
uses: peter-evans/create-pull-request@v5.0.2
|
||||
with:
|
||||
title: "Update contributors"
|
||||
commit-message: "Update contributors"
|
||||
|
||||
77
.github/workflows/generate-feature-graphic.yml
vendored
Normal file
77
.github/workflows/generate-feature-graphic.yml
vendored
Normal file
@@ -0,0 +1,77 @@
|
||||
name: Generate feature graphic
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- 'fastlane/**/title.txt'
|
||||
permissions:
|
||||
actions: none
|
||||
checks: none
|
||||
contents: write
|
||||
deployments: none
|
||||
discussions: none
|
||||
id-token: none
|
||||
issues: none
|
||||
packages: none
|
||||
pages: none
|
||||
pull-requests: write
|
||||
repository-projects: none
|
||||
security-events: none
|
||||
statuses: none
|
||||
jobs:
|
||||
generate-feature-graphic:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4.0.0
|
||||
- name: Install requirements
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install optipng mat2
|
||||
# Install 200 weight versions of relevant Noto (to use for languages not supported by Lexend Deca)
|
||||
sudo apt-get install fonts-noto-extra fonts-noto-cjk-extra
|
||||
# Custom fonts
|
||||
mkdir "$HOME/.fonts"
|
||||
find .scripts/generate_feature_graphic/fonts -name '*.ttf' -exec cp {} "$HOME/.fonts" \;
|
||||
fc-cache
|
||||
- name: Generate featureGraphic.png for each language
|
||||
run: |
|
||||
for lang in fastlane/metadata/android/*; do
|
||||
pushd "$lang"
|
||||
# Place temporary copy for editing if needed
|
||||
cp ../../../../.scripts/generate_feature_graphic/featureGraphic.svg featureGraphic.svg
|
||||
# Extract text after 'Catima - '
|
||||
export subtext="$(grep -oP '(?<=Catima \S ).*' title.txt || true)"
|
||||
# If there is subtext, change the .svg accordingly
|
||||
if [ -n "$subtext" ]; then
|
||||
perl -pi -e 's/Loyalty Card Wallet/$ENV{subtext}/' featureGraphic.svg
|
||||
# Set correct font for language if needed (Lexend Deca has limited support)
|
||||
# We specifically need the Serif version because of the 200 weight
|
||||
case "$(basename "$lang")" in
|
||||
bg|el-GR|ru-RU|uk) sed -i "s/Lexend Deca/Noto Serif/" featureGraphic.svg ;;
|
||||
ja-JP) sed -i "s/Lexend Deca/Noto Serif CJK JP/" featureGraphic.svg ;;
|
||||
ko) sed -i "s/Lexend Deca/Noto Serif CJK KR/" featureGraphic.svg ;;
|
||||
zh-CN) sed -i "s/Lexend Deca/Noto Serif CJK SC/" featureGraphic.svg ;;
|
||||
zh-TW) sed -i "s/Lexend Deca/Noto Serif CJK TC/" featureGraphic.svg ;;
|
||||
*) ;;
|
||||
esac
|
||||
fi
|
||||
# Ensure images directory exists
|
||||
mkdir -p images
|
||||
# Generate .png
|
||||
convert featureGraphic.svg images/featureGraphic.png
|
||||
# Optimize .png
|
||||
optipng images/featureGraphic.png
|
||||
# Remove metadata (timestamps) from .png
|
||||
mat2 --inplace images/featureGraphic.png
|
||||
# Remove temporary .svg
|
||||
rm featureGraphic.svg
|
||||
popd
|
||||
done
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v5.0.2
|
||||
with:
|
||||
title: "Update feature graphic"
|
||||
commit-message: "Update feature graphic"
|
||||
branch-suffix: timestamp
|
||||
35
.github/workflows/update-locales.yml
vendored
Normal file
35
.github/workflows/update-locales.yml
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
name: Update locales
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- app/src/main/res/values/settings.xml
|
||||
permissions:
|
||||
actions: none
|
||||
checks: none
|
||||
contents: write
|
||||
deployments: none
|
||||
discussions: none
|
||||
id-token: none
|
||||
issues: none
|
||||
packages: none
|
||||
pages: none
|
||||
pull-requests: write
|
||||
repository-projects: none
|
||||
security-events: none
|
||||
statuses: none
|
||||
jobs:
|
||||
update-locales:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4.0.0
|
||||
- name: Update locales
|
||||
run: .scripts/locales.py
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v5.0.2
|
||||
with:
|
||||
title: "Update locales"
|
||||
commit-message: "Update locales"
|
||||
branch-suffix: timestamp
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -8,3 +8,8 @@ captures/
|
||||
**/release
|
||||
**/debug
|
||||
app/*.log
|
||||
|
||||
# Bundle
|
||||
/.bundle/
|
||||
/vendor/bundle
|
||||
/lib/bundler/man/
|
||||
|
||||
44
.scripts/dump_stocard_stores.py
Executable file
44
.scripts/dump_stocard_stores.py
Executable file
@@ -0,0 +1,44 @@
|
||||
#!/usr/bin/python3
|
||||
|
||||
import csv
|
||||
import json
|
||||
import msgpack
|
||||
|
||||
MSGPACK = "bootstrapdata.msgpack"
|
||||
OUTFILE = "stocard_stores.csv"
|
||||
|
||||
|
||||
def load(fh):
|
||||
data = []
|
||||
for r in msgpack.Unpacker(fh, raw=False):
|
||||
if r["collection"] == "/loyalty-card-providers/":
|
||||
d = json.loads(r["data"])
|
||||
data.append([r["resource_id"], d["name"], d["default_barcode_format"]])
|
||||
return data
|
||||
|
||||
|
||||
def save(data, output_file=OUTFILE):
|
||||
with open(output_file, "w") as fh:
|
||||
writer = csv.writer(fh, lineterminator="\n")
|
||||
writer.writerow(["_id", "name", "barcodeFormat"])
|
||||
for row in data:
|
||||
writer.writerow(row)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import argparse
|
||||
parser = argparse.ArgumentParser(
|
||||
epilog=f"INPUT_FILE must be a .msgpack or .apk and defaults to {MSGPACK}; "
|
||||
f"OUTPUT_FILE defaults to {OUTFILE}")
|
||||
parser.add_argument("input_file", metavar="INPUT_FILE", nargs="?", default=MSGPACK)
|
||||
parser.add_argument("output_file", metavar="OUTPUT_FILE", nargs="?", default=OUTFILE)
|
||||
args = parser.parse_args()
|
||||
if args.input_file.lower().endswith(".apk"):
|
||||
import zipfile
|
||||
with zipfile.ZipFile(args.input_file) as zf:
|
||||
with zf.open(f"assets/{MSGPACK}") as fh:
|
||||
data = load(fh)
|
||||
else:
|
||||
with open(args.input_file, "rb") as fh:
|
||||
data = load(fh)
|
||||
save(data, args.output_file)
|
||||
15
.scripts/generate_feature_graphic/featureGraphic.svg
Normal file
15
.scripts/generate_feature_graphic/featureGraphic.svg
Normal file
@@ -0,0 +1,15 @@
|
||||
<svg width="1024" height="500" viewBox="0 0 1024 500" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="1024" height="500" fill="#223355"/>
|
||||
<text fill="white" xml:space="preserve" style="" font-family="Yesteryear" font-size="150" letter-spacing="0em"><tspan x="470.082" y="285.511">Catima
|
||||
</tspan></text>
|
||||
<path d="M381.046 147.001L236.3 211.446L276.524 301.79L421.27 237.345L381.046 147.001Z" fill="#F0F0F0" stroke="#C80000" stroke-width="2"/>
|
||||
<path d="M402.077 219.13L240.07 147L191.984 255.004L353.99 327.135L402.077 219.13Z" fill="#F0F0F0" stroke="#C80000" stroke-width="2"/>
|
||||
<path d="M437.17 236.241L251.831 183.096L220.071 293.855L405.41 347L437.17 236.241Z" fill="#C80000" stroke="#C80000" stroke-width="6" stroke-linejoin="round"/>
|
||||
<path d="M412.879 178.633H220.071V293.855H412.879V178.633Z" fill="#FF0000" stroke="#FF0000" stroke-width="6" stroke-linejoin="round"/>
|
||||
<path d="M221.482 296.217C238.316 296.217 251.963 269.366 251.963 236.244C251.963 203.121 238.316 176.27 221.482 176.27C204.647 176.27 191 203.121 191 236.244C191 269.366 204.647 296.217 221.482 296.217Z" fill="#FF0000" stroke="#FF0000" stroke-width="3.44232" stroke-linejoin="round"/>
|
||||
<path d="M307.256 250.444C307.256 253.187 306.289 255.842 304.526 257.944C302.763 260.045 300.316 261.458 297.614 261.934C294.913 262.41 292.13 261.92 289.755 260.548C287.379 259.177 285.563 257.012 284.625 254.435" stroke="#F0F0F0" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M330.301 254.298C329.363 256.875 327.547 259.04 325.171 260.411C322.796 261.783 320.013 262.273 317.312 261.797C314.61 261.321 312.163 259.908 310.4 257.807C308.637 255.706 307.671 253.05 307.671 250.307" stroke="#F0F0F0" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M248.345 225.937L266.818 207.465L285.29 225.937" stroke="#F0F0F0" stroke-width="2"/>
|
||||
<path d="M329.625 225.937L348.098 207.465L366.571 225.937" stroke="#F0F0F0" stroke-width="2"/>
|
||||
<text fill="white" xml:space="preserve" style="" font-family="Lexend Deca" font-size="35" font-weight="200" letter-spacing="0em"><tspan x="466" y="340">Loyalty Card Wallet</tspan></text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.1 KiB |
Binary file not shown.
93
.scripts/generate_feature_graphic/fonts/Lexend_Deca/OFL.txt
Normal file
93
.scripts/generate_feature_graphic/fonts/Lexend_Deca/OFL.txt
Normal file
@@ -0,0 +1,93 @@
|
||||
Copyright 2018 The Lexend Project Authors (https://github.com/googlefonts/lexend), with Reserved Font Name “RevReading Lexend”.
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
http://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
94
.scripts/generate_feature_graphic/fonts/Yesteryear/OFL.txt
Normal file
94
.scripts/generate_feature_graphic/fonts/Yesteryear/OFL.txt
Normal file
@@ -0,0 +1,94 @@
|
||||
Copyright (c) 2011 by Brian J. Bonislawsky DBA Astigmatic (AOETI)
|
||||
(astigma@astigmatic.com), with Reserved Font Names "Yesteryear"
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
http://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
Binary file not shown.
36
.scripts/locales.py
Executable file
36
.scripts/locales.py
Executable file
@@ -0,0 +1,36 @@
|
||||
#!/usr/bin/python3
|
||||
|
||||
import subprocess
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
root = ET.parse("app/src/main/res/values/settings.xml").getroot()
|
||||
for e in root.findall("string-array"):
|
||||
if e.get("name") == "locale_values":
|
||||
locales = [x.text for x in e if x.text]
|
||||
break
|
||||
|
||||
locales = [
|
||||
# e.g. de or es-rAR (not es-AR)
|
||||
loc.replace("-", "-r") if "-" in loc and loc[loc.index("-") + 1] != "r" else loc
|
||||
for loc in locales
|
||||
]
|
||||
|
||||
res = ", ".join(f'"{loc}"' for loc in locales)
|
||||
sed = [
|
||||
"sed",
|
||||
"-i",
|
||||
f"s/resourceConfigurations .*/resourceConfigurations += [{res}]/",
|
||||
"app/build.gradle"
|
||||
]
|
||||
subprocess.run(sed, check=True)
|
||||
|
||||
with open("app/src/main/res/xml/locales_config.xml", "w") as fh:
|
||||
fh.write('<?xml version="1.0" encoding="utf-8"?>\n')
|
||||
fh.write('<locale-config xmlns:android="http://schemas.android.com/apk/res/android">\n')
|
||||
fh.write(' <locale android:name="en-US" />\n')
|
||||
for loc in locales:
|
||||
if loc != "en":
|
||||
# e.g. de or en-AR (not es-rAR)
|
||||
loc = loc.replace("-r", "-")
|
||||
fh.write(f' <locale android:name="{loc}" />\n')
|
||||
fh.write('</locale-config>\n')
|
||||
103
CHANGELOG.md
103
CHANGELOG.md
@@ -1,29 +1,116 @@
|
||||
# Changelog
|
||||
|
||||
## v2.21.0 - 115
|
||||
## Unreleased - 132
|
||||
|
||||
- Refine "Add card" workflow
|
||||
- Validation flow improvements
|
||||
|
||||
## v2.26.0 - 131 (2023-09-14)
|
||||
|
||||
- Move "Archive mode" into "Display options" (previously "Show details") menu
|
||||
- Android 13 per-app language support
|
||||
- Embed privacy policy, changelog and license in the app
|
||||
|
||||
## v2.25.3 - 130 (2023-08-25)
|
||||
|
||||
- Minor UI fixes
|
||||
- Fix valid from and expiry dates being reset when rotating the card editing screen
|
||||
- Fix crash when rotating screen while the color picker is shown
|
||||
- Stocard import fixes
|
||||
|
||||
## 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
|
||||
- Basic interoperability with external apps (Android 6.0+)
|
||||
- Reorganized settings screen
|
||||
- Fix importing from some browsers that add a trailing / to the share URL
|
||||
|
||||
## v2.24.2 - 126 (2023-06-18)
|
||||
|
||||
- Various RTL fixes
|
||||
|
||||
## v2.24.1 - 125 (2023-06-11)
|
||||
|
||||
- Deal more gracefully with missing header colours
|
||||
|
||||
## v2.24.0 - 124 (2023-06-10)
|
||||
|
||||
- Support selecting exactly which details to view in card overview
|
||||
|
||||
## v2.23.3 - 123 (2023-06-03)
|
||||
|
||||
- Minor UI improvements
|
||||
- Fix new design not being usable on devices with square screens
|
||||
|
||||
## v2.23.2 - 122 (2023-05-30)
|
||||
|
||||
- Long-press card icon in view activity to change it
|
||||
- Improve button styling in Groups screen
|
||||
- Fix long barcode values causing barcode to scale down to nothing
|
||||
|
||||
## v2.23.1 - 121 (2023-05-27)
|
||||
|
||||
- Update used libraries
|
||||
|
||||
## v2.23.0 - 120 (2023-05-25)
|
||||
|
||||
- Complete redesign of main and loyalty card view screens
|
||||
- Material You design for the settings screen
|
||||
- Fix crash when using "Take a photo" with disabled camera app
|
||||
|
||||
## v2.22.1 - 119 (2023-04-14)
|
||||
|
||||
- Use Material You colours on more devices (Google library update)
|
||||
|
||||
## v2.22.0 - 118 (2023-03-18)
|
||||
|
||||
- Support setting start of card validity
|
||||
- Fix Stocard import (Stocard's export format changed)
|
||||
|
||||
## v2.21.2 - 117 (2023-01-27)
|
||||
|
||||
- Remove unnecessary permissions
|
||||
- Target Android 13
|
||||
|
||||
## v2.21.1 - 116 (2022-12-06)
|
||||
|
||||
- Fix quick spend dialog not allowing , separator
|
||||
- Support loading image from file manager
|
||||
|
||||
## v2.21.0 - 115 (2022-11-06)
|
||||
|
||||
- Open image in gallery on long-press
|
||||
- Apply Material style to dialogs
|
||||
- Support creating card by sharing an image to Catima
|
||||
- Add quick spend button to card screen
|
||||
|
||||
## v2.20.0 - 114
|
||||
## v2.20.0 - 114 (2022-09-21)
|
||||
|
||||
- Add Monochrome icon for Android 13
|
||||
- Improve first launch screen
|
||||
- Fidme import fixes
|
||||
|
||||
## v2.19.0 - 113
|
||||
## v2.19.0 - 113 (2022-08-14)
|
||||
|
||||
- Add previous and next buttons to the loyalty card view
|
||||
- Fix foreground colour on edit button
|
||||
- Replace floppy disk save icon with checkmark
|
||||
|
||||
## v2.18.2 - 112
|
||||
## v2.18.2 - 112 (2022-07-29)
|
||||
|
||||
- Make the possibility to set a custom header more visible
|
||||
|
||||
## v2.18.1 - 111
|
||||
## v2.18.1 - 111 (2022-07-24)
|
||||
|
||||
- Arabic language support
|
||||
- Display archived card count in group overview
|
||||
@@ -33,11 +120,11 @@
|
||||
- Fix crash when leaving cardview in RTL layouts for cards with expiry or balance
|
||||
- Fix back arrow in card view pointing the wrong way in RTL layouts
|
||||
|
||||
## v2.17.1 - 109
|
||||
## v2.17.1 - 109 (2022-06-28)
|
||||
|
||||
- Fix incorrect text colour on "No barcode" button
|
||||
|
||||
## v2.17.0 - 108
|
||||
## v2.17.0 - 108 (2022-06-24)
|
||||
|
||||
- Add card duplication feature
|
||||
- Don't allow choosing expiry before 1970 (they never worked anyway)
|
||||
@@ -599,7 +686,7 @@ Additional features/improvements:
|
||||
## v0.7 - 7 (2016-07-14)
|
||||
|
||||
- Long-click of a card brings up option to copy card ID to the clipboard. ([pull #49](https://github.com/brarcher/loyalty-card-locker/issues/49))
|
||||
- Back button on Input/Export view now works, moving user to main view
|
||||
- Back button on Import/Export view now works, moving user to main view
|
||||
|
||||
## v0.6 - 6 (2016-05-23)
|
||||
|
||||
|
||||
@@ -100,5 +100,5 @@ your real name, saying:
|
||||
Finally, you will need to submit your patches so that they can be reviewed
|
||||
and potentially merged into the main Catima repository. The preferred
|
||||
way to do this is to submit a Pull Request to the Catima project.
|
||||
Changes need to apply cleanly onto the master branch and pass all
|
||||
Changes need to apply cleanly onto the main branch and pass all
|
||||
unit tests and produce no errors during static analysis.
|
||||
|
||||
107
Gemfile.lock
107
Gemfile.lock
@@ -1,27 +1,27 @@
|
||||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
CFPropertyList (3.0.5)
|
||||
CFPropertyList (3.0.6)
|
||||
rexml
|
||||
addressable (2.8.0)
|
||||
public_suffix (>= 2.0.2, < 5.0)
|
||||
addressable (2.8.5)
|
||||
public_suffix (>= 2.0.2, < 6.0)
|
||||
artifactory (3.0.15)
|
||||
atomos (0.1.3)
|
||||
aws-eventstream (1.2.0)
|
||||
aws-partitions (1.597.0)
|
||||
aws-sdk-core (3.131.1)
|
||||
aws-partitions (1.824.0)
|
||||
aws-sdk-core (3.181.1)
|
||||
aws-eventstream (~> 1, >= 1.0.2)
|
||||
aws-partitions (~> 1, >= 1.525.0)
|
||||
aws-sigv4 (~> 1.1)
|
||||
aws-partitions (~> 1, >= 1.651.0)
|
||||
aws-sigv4 (~> 1.5)
|
||||
jmespath (~> 1, >= 1.6.1)
|
||||
aws-sdk-kms (1.57.0)
|
||||
aws-sdk-core (~> 3, >= 3.127.0)
|
||||
aws-sdk-kms (1.71.0)
|
||||
aws-sdk-core (~> 3, >= 3.177.0)
|
||||
aws-sigv4 (~> 1.1)
|
||||
aws-sdk-s3 (1.114.0)
|
||||
aws-sdk-core (~> 3, >= 3.127.0)
|
||||
aws-sdk-s3 (1.134.0)
|
||||
aws-sdk-core (~> 3, >= 3.181.0)
|
||||
aws-sdk-kms (~> 1)
|
||||
aws-sigv4 (~> 1.4)
|
||||
aws-sigv4 (1.5.0)
|
||||
aws-sigv4 (~> 1.6)
|
||||
aws-sigv4 (1.6.0)
|
||||
aws-eventstream (~> 1, >= 1.0.2)
|
||||
babosa (1.0.4)
|
||||
claide (1.1.0)
|
||||
@@ -30,14 +30,14 @@ 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.7.6)
|
||||
dotenv (2.8.1)
|
||||
emoji_regex (3.2.3)
|
||||
excon (0.92.3)
|
||||
faraday (1.10.0)
|
||||
excon (0.103.0)
|
||||
faraday (1.10.3)
|
||||
faraday-em_http (~> 1.0)
|
||||
faraday-em_synchrony (~> 1.0)
|
||||
faraday-excon (~> 1.1)
|
||||
@@ -65,8 +65,8 @@ GEM
|
||||
faraday-retry (1.0.3)
|
||||
faraday_middleware (1.2.0)
|
||||
faraday (~> 1.0)
|
||||
fastimage (2.2.6)
|
||||
fastlane (2.206.2)
|
||||
fastimage (2.2.7)
|
||||
fastlane (2.215.1)
|
||||
CFPropertyList (>= 2.3, < 4.0.0)
|
||||
addressable (>= 2.8, < 3.0.0)
|
||||
artifactory (~> 3.0)
|
||||
@@ -87,10 +87,11 @@ GEM
|
||||
google-apis-playcustomapp_v1 (~> 0.1)
|
||||
google-cloud-storage (~> 1.31)
|
||||
highline (~> 2.0)
|
||||
http-cookie (~> 1.0.5)
|
||||
json (< 3.0.0)
|
||||
jwt (>= 2.1.0, < 3)
|
||||
mini_magick (>= 4.9.4, < 5.0.0)
|
||||
multipart-post (~> 2.0.0)
|
||||
multipart-post (>= 2.0.0, < 3.0.0)
|
||||
naturally (~> 2.2)
|
||||
optparse (~> 0.1.1)
|
||||
plist (>= 3.1.0, < 4.0.0)
|
||||
@@ -98,7 +99,7 @@ GEM
|
||||
security (= 0.1.3)
|
||||
simctl (~> 1.6.3)
|
||||
terminal-notifier (>= 2.0.0, < 3.0.0)
|
||||
terminal-table (>= 1.4.5, < 2.0.0)
|
||||
terminal-table (~> 3)
|
||||
tty-screen (>= 0.6.3, < 1.0.0)
|
||||
tty-spinner (>= 0.8.0, < 1.0.0)
|
||||
word_wrap (~> 1.0.0)
|
||||
@@ -106,9 +107,9 @@ GEM
|
||||
xcpretty (~> 0.3.0)
|
||||
xcpretty-travis-formatter (>= 0.0.3)
|
||||
gh_inspector (1.1.3)
|
||||
google-apis-androidpublisher_v3 (0.21.0)
|
||||
google-apis-core (>= 0.4, < 2.a)
|
||||
google-apis-core (0.5.0)
|
||||
google-apis-androidpublisher_v3 (0.49.0)
|
||||
google-apis-core (>= 0.11.0, < 2.a)
|
||||
google-apis-core (0.11.1)
|
||||
addressable (~> 2.5, >= 2.5.1)
|
||||
googleauth (>= 0.16.2, < 2.a)
|
||||
httpclient (>= 2.8.1, < 3.a)
|
||||
@@ -117,30 +118,29 @@ GEM
|
||||
retriable (>= 2.0, < 4.a)
|
||||
rexml
|
||||
webrick
|
||||
google-apis-iamcredentials_v1 (0.10.0)
|
||||
google-apis-core (>= 0.4, < 2.a)
|
||||
google-apis-playcustomapp_v1 (0.7.0)
|
||||
google-apis-core (>= 0.4, < 2.a)
|
||||
google-apis-storage_v1 (0.14.0)
|
||||
google-apis-core (>= 0.4, < 2.a)
|
||||
google-apis-iamcredentials_v1 (0.17.0)
|
||||
google-apis-core (>= 0.11.0, < 2.a)
|
||||
google-apis-playcustomapp_v1 (0.13.0)
|
||||
google-apis-core (>= 0.11.0, < 2.a)
|
||||
google-apis-storage_v1 (0.19.0)
|
||||
google-apis-core (>= 0.9.0, < 2.a)
|
||||
google-cloud-core (1.6.0)
|
||||
google-cloud-env (~> 1.0)
|
||||
google-cloud-errors (~> 1.0)
|
||||
google-cloud-env (1.6.0)
|
||||
faraday (>= 0.17.3, < 3.0)
|
||||
google-cloud-errors (1.2.0)
|
||||
google-cloud-storage (1.36.2)
|
||||
google-cloud-errors (1.3.1)
|
||||
google-cloud-storage (1.44.0)
|
||||
addressable (~> 2.8)
|
||||
digest-crc (~> 0.4)
|
||||
google-apis-iamcredentials_v1 (~> 0.1)
|
||||
google-apis-storage_v1 (~> 0.1)
|
||||
google-apis-storage_v1 (~> 0.19.0)
|
||||
google-cloud-core (~> 1.6)
|
||||
googleauth (>= 0.16.2, < 2.a)
|
||||
mini_mime (~> 1.0)
|
||||
googleauth (1.1.3)
|
||||
googleauth (1.8.0)
|
||||
faraday (>= 0.17.3, < 3.a)
|
||||
jwt (>= 1.4, < 3.0)
|
||||
memoist (~> 0.16)
|
||||
multi_json (~> 1.11)
|
||||
os (>= 0.9, < 2.0)
|
||||
signet (>= 0.16, < 2.a)
|
||||
@@ -148,42 +148,41 @@ GEM
|
||||
http-cookie (1.0.5)
|
||||
domain_name (~> 0.5)
|
||||
httpclient (2.8.3)
|
||||
jmespath (1.6.1)
|
||||
json (2.6.2)
|
||||
jwt (2.4.1)
|
||||
memoist (0.16.2)
|
||||
mini_magick (4.11.0)
|
||||
mini_mime (1.1.2)
|
||||
jmespath (1.6.2)
|
||||
json (2.6.3)
|
||||
jwt (2.7.1)
|
||||
mini_magick (4.12.0)
|
||||
mini_mime (1.1.5)
|
||||
multi_json (1.15.0)
|
||||
multipart-post (2.0.0)
|
||||
multipart-post (2.3.0)
|
||||
nanaimo (0.3.0)
|
||||
naturally (2.2.1)
|
||||
optparse (0.1.1)
|
||||
os (1.1.4)
|
||||
plist (3.6.0)
|
||||
public_suffix (4.0.7)
|
||||
plist (3.7.0)
|
||||
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)
|
||||
security (0.1.3)
|
||||
signet (0.16.1)
|
||||
signet (0.18.0)
|
||||
addressable (~> 2.8)
|
||||
faraday (>= 0.17.5, < 3.0)
|
||||
faraday (>= 0.17.5, < 3.a)
|
||||
jwt (>= 1.5, < 3.0)
|
||||
multi_json (~> 1.10)
|
||||
simctl (1.6.8)
|
||||
simctl (1.6.10)
|
||||
CFPropertyList
|
||||
naturally
|
||||
terminal-notifier (2.0.0)
|
||||
terminal-table (1.8.0)
|
||||
unicode-display_width (~> 1.1, >= 1.1.1)
|
||||
terminal-table (3.0.2)
|
||||
unicode-display_width (>= 1.1.1, < 3)
|
||||
trailblazer-option (0.1.2)
|
||||
tty-cursor (0.7.1)
|
||||
tty-screen (0.8.1)
|
||||
@@ -193,10 +192,10 @@ GEM
|
||||
unf (0.1.4)
|
||||
unf_ext
|
||||
unf_ext (0.0.8.2)
|
||||
unicode-display_width (1.8.0)
|
||||
webrick (1.7.0)
|
||||
unicode-display_width (2.4.2)
|
||||
webrick (1.8.1)
|
||||
word_wrap (1.0.0)
|
||||
xcodeproj (1.21.0)
|
||||
xcodeproj (1.22.0)
|
||||
CFPropertyList (>= 2.3.3, < 4.0)
|
||||
atomos (~> 0.1.3)
|
||||
claide (>= 1.0.2, < 2.0)
|
||||
@@ -215,4 +214,4 @@ DEPENDENCIES
|
||||
fastlane
|
||||
|
||||
BUNDLED WITH
|
||||
2.1.4
|
||||
2.3.26
|
||||
|
||||
18
PRIVACY.md
Normal file
18
PRIVACY.md
Normal file
@@ -0,0 +1,18 @@
|
||||
**Last updated**
|
||||
August 30 2023
|
||||
|
||||
# Privacy Policy
|
||||
Catima does not collect or transmit any personal information.
|
||||
|
||||
To ensure correct app functionality, we require access to the following:
|
||||
|
||||
- Camera: We need access to your camera to be able to scan barcodes. The app can still be used when camera access is denied, but you will have to manually type the barcode information.
|
||||
- Storage (Android 5 and 6 only): We need access to your device storage to create or import backups. The app can still be used when storage access is denied, but you will not be able to create or import backups.
|
||||
|
||||
Catima offers a feature to share cards with other users. All the relevant data is in the generated shareable URLs and never transmitted to our servers. When viewed through catima.app, the data in the URL is rendered using client-side Javascript to further ensure no data is ever transmitted to us.
|
||||
|
||||
# Changes
|
||||
This Privacy Policy may be updated from time to time for any reason. We will notify you of any changes to our Privacy Policy by posting the new Privacy Policy to https://catima.app/privacy-policy/. A snapshot of the Privacy Policy is available within the Catima app, though it may be outdated. When the Privacy Policy on the website and in the app differ, the website should be considered leading. You are advised to consult the Privacy Policy regularly for any changes, as continued use is deemed approval of all changes.
|
||||
|
||||
# Contact us
|
||||
If you have any questions regarding privacy while using the Application, or have questions about our practices, please contact us via email at catima.g9ex3@hackerchick.me.
|
||||
13
SECURITY.md
Normal file
13
SECURITY.md
Normal 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).
|
||||
@@ -13,17 +13,19 @@ spotbugs {
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdk 31
|
||||
compileSdk 33
|
||||
|
||||
defaultConfig {
|
||||
applicationId "me.hackerchick.catima"
|
||||
minSdk 21
|
||||
targetSdk 31
|
||||
versionCode 115
|
||||
versionName "2.21.0"
|
||||
targetSdk 33
|
||||
versionCode 131
|
||||
versionName "2.26.0"
|
||||
|
||||
vectorDrawables.useSupportLibrary true
|
||||
multiDexEnabled true
|
||||
|
||||
resourceConfigurations += ["ar", "bg", "bn", "bn-rIN", "bs", "cs", "da", "de", "el-rGR", "en", "eo", "es", "es-rAR", "fi", "fr", "he-rIL", "hi", "hr", "hu", "in-rID", "is", "it", "ja", "ko", "lt", "lv", "nb-rNO", "nl", "oc", "pl", "pt", "ro-rRO", "ru", "sk", "sl", "sv", "tr", "uk", "zh-rTW", "zh-rCN"]
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
@@ -58,9 +60,6 @@ android {
|
||||
targetCompatibility JavaVersion.VERSION_11
|
||||
}
|
||||
|
||||
lintOptions {
|
||||
lintConfig file("lint.xml")
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
test {
|
||||
@@ -80,37 +79,40 @@ android {
|
||||
includeAndroidResources true
|
||||
}
|
||||
}
|
||||
lint {
|
||||
lintConfig file('lint.xml')
|
||||
}
|
||||
namespace 'protect.card_locker'
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// AndroidX
|
||||
implementation 'androidx.appcompat:appcompat:1.4.2'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
|
||||
implementation 'androidx.exifinterface:exifinterface:1.3.3'
|
||||
implementation 'androidx.appcompat:appcompat:1.6.1'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
||||
implementation 'androidx.exifinterface:exifinterface:1.3.6'
|
||||
implementation 'androidx.palette:palette:1.0.0'
|
||||
implementation 'androidx.preference:preference:1.2.0'
|
||||
implementation 'com.google.android.material:material:1.6.1'
|
||||
implementation 'com.google.android.material:material:1.9.0'
|
||||
implementation 'com.github.yalantis:ucrop:2.2.8'
|
||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.6'
|
||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.3'
|
||||
|
||||
// Splash Screen
|
||||
implementation 'androidx.core:core-splashscreen:1.0.0'
|
||||
implementation 'androidx.core:core-splashscreen:1.0.1'
|
||||
|
||||
// Third-party
|
||||
implementation 'com.journeyapps:zxing-android-embedded:4.3.0@aar'
|
||||
implementation 'com.google.zxing:core:3.5.0'
|
||||
implementation 'com.google.zxing:core:3.5.2'
|
||||
implementation 'org.apache.commons:commons-csv:1.9.0'
|
||||
implementation 'com.jaredrummler:colorpicker:1.1.0'
|
||||
implementation 'com.github.invissvenska:NumberPickerPreference:1.0.4'
|
||||
implementation 'net.lingala.zip4j:zip4j:2.11.2'
|
||||
implementation 'net.lingala.zip4j:zip4j:2.11.5'
|
||||
|
||||
// SpotBugs
|
||||
implementation 'io.wcm.tooling.spotbugs:io.wcm.tooling.spotbugs.annotations:1.0.0'
|
||||
|
||||
// Testing
|
||||
testImplementation 'androidx.test:core:1.4.0'
|
||||
testImplementation 'androidx.test:core:1.5.0'
|
||||
testImplementation 'junit:junit:4.13.2'
|
||||
testImplementation 'org.robolectric:robolectric:4.9'
|
||||
testImplementation 'org.robolectric:robolectric:4.10.3'
|
||||
}
|
||||
|
||||
tasks.withType(SpotBugsTask) {
|
||||
@@ -127,3 +129,18 @@ tasks.withType(SpotBugsTask) {
|
||||
html.enabled = true
|
||||
}
|
||||
}
|
||||
|
||||
tasks.register('copyRawResFiles', Copy) {
|
||||
from layout.projectDirectory.file("../CHANGELOG.md"),
|
||||
layout.projectDirectory.file("../PRIVACY.md")
|
||||
into layout.projectDirectory.dir("src/main/res/raw")
|
||||
rename { String fileName -> fileName.toLowerCase() }
|
||||
}
|
||||
|
||||
project.afterEvaluate {
|
||||
tasks.each { task ->
|
||||
if (task != copyRawResFiles) {
|
||||
task.dependsOn(copyRawResFiles)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="protect.card_locker">
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<permission
|
||||
android:description="@string/permissionReadCardsDescription"
|
||||
android:icon="@drawable/ic_launcher_foreground"
|
||||
android:label="@string/permissionReadCardsLabel"
|
||||
android:name="${applicationId}.READ_CARDS"
|
||||
android:protectionLevel="dangerous" />
|
||||
|
||||
<uses-sdk tools:overrideLibrary="com.google.zxing.client.android" />
|
||||
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="23" />
|
||||
|
||||
<uses-feature
|
||||
android:name="android.hardware.camera"
|
||||
@@ -22,7 +27,8 @@
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/AppTheme">
|
||||
android:theme="@style/AppTheme"
|
||||
android:localeConfig="@xml/locales_config">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
@@ -157,6 +163,12 @@
|
||||
android:name=".UCropWrapper"
|
||||
android:theme="@style/AppTheme.NoActionBar" />
|
||||
|
||||
<provider
|
||||
android:name=".contentprovider.CardsContentProvider"
|
||||
android:authorities="${applicationId}.contentprovider.cards"
|
||||
android:exported="true"
|
||||
android:readPermission="${applicationId}.READ_CARDS"/>
|
||||
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="${applicationId}"
|
||||
@@ -173,4 +185,4 @@
|
||||
</intent-filter>
|
||||
</service>
|
||||
</application>
|
||||
</manifest>
|
||||
</manifest>
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
package protect.card_locker;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.text.Spanned;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.widget.ScrollView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.StringRes;
|
||||
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
|
||||
|
||||
@@ -28,17 +32,24 @@ public class AboutActivity extends CatimaAppCompatActivity {
|
||||
enableToolbarBackButton();
|
||||
|
||||
TextView copyright = binding.creditsSub;
|
||||
copyright.setText(content.getCopyright());
|
||||
copyright.setText(content.getCopyrightShort());
|
||||
TextView versionHistory = binding.versionHistorySub;
|
||||
versionHistory.setText(content.getVersionHistory());
|
||||
|
||||
binding.versionHistory.setTag("https://catima.app/changelog/");
|
||||
binding.translate.setTag("https://hosted.weblate.org/engage/catima/");
|
||||
binding.license.setTag("https://github.com/CatimaLoyalty/Android/blob/master/LICENSE");
|
||||
binding.license.setTag("https://github.com/CatimaLoyalty/Android/blob/main/LICENSE");
|
||||
binding.repo.setTag("https://github.com/CatimaLoyalty/Android/");
|
||||
binding.privacy.setTag("https://catima.app/privacy-policy/");
|
||||
binding.reportError.setTag("https://github.com/CatimaLoyalty/Android/issues");
|
||||
binding.rate.setTag("https://play.google.com/store/apps/details?id=me.hackerchick.catima");
|
||||
binding.donate.setTag("https://catima.app/contribute/#donating");
|
||||
|
||||
boolean installedFromGooglePlay = Utils.installedFromGooglePlay(this);
|
||||
// Hide Google Play rate button if not on Google Play
|
||||
binding.rate.setVisibility(installedFromGooglePlay ? View.VISIBLE : View.GONE);
|
||||
// Hide donate button on Google Play (Google Play doesn't allow donation links)
|
||||
binding.donate.setVisibility(installedFromGooglePlay ? View.GONE : View.VISIBLE);
|
||||
|
||||
bindClickListeners();
|
||||
}
|
||||
@@ -61,19 +72,14 @@ public class AboutActivity extends CatimaAppCompatActivity {
|
||||
}
|
||||
|
||||
private void bindClickListeners() {
|
||||
View.OnClickListener openExternalBrowser = view -> {
|
||||
Object tag = view.getTag();
|
||||
if (tag instanceof String && ((String) tag).startsWith("https://")) {
|
||||
(new OpenWebLinkHandler()).openBrowser(this, (String) tag);
|
||||
}
|
||||
};
|
||||
binding.versionHistory.setOnClickListener(openExternalBrowser);
|
||||
binding.translate.setOnClickListener(openExternalBrowser);
|
||||
binding.license.setOnClickListener(openExternalBrowser);
|
||||
binding.repo.setOnClickListener(openExternalBrowser);
|
||||
binding.privacy.setOnClickListener(openExternalBrowser);
|
||||
binding.reportError.setOnClickListener(openExternalBrowser);
|
||||
binding.rate.setOnClickListener(openExternalBrowser);
|
||||
binding.versionHistory.setOnClickListener(this::showHistory);
|
||||
binding.translate.setOnClickListener(this::openExternalBrowser);
|
||||
binding.license.setOnClickListener(this::showLicense);
|
||||
binding.repo.setOnClickListener(this::openExternalBrowser);
|
||||
binding.privacy.setOnClickListener(this::showPrivacy);
|
||||
binding.reportError.setOnClickListener(this::openExternalBrowser);
|
||||
binding.rate.setOnClickListener(this::openExternalBrowser);
|
||||
binding.donate.setOnClickListener(this::openExternalBrowser);
|
||||
|
||||
binding.credits.setOnClickListener(view -> showCredits());
|
||||
}
|
||||
@@ -86,6 +92,8 @@ public class AboutActivity extends CatimaAppCompatActivity {
|
||||
binding.privacy.setOnClickListener(null);
|
||||
binding.reportError.setOnClickListener(null);
|
||||
binding.rate.setOnClickListener(null);
|
||||
binding.donate.setOnClickListener(null);
|
||||
|
||||
binding.credits.setOnClickListener(null);
|
||||
}
|
||||
|
||||
@@ -96,4 +104,39 @@ public class AboutActivity extends CatimaAppCompatActivity {
|
||||
.setPositiveButton(R.string.ok, null)
|
||||
.show();
|
||||
}
|
||||
|
||||
private void showHistory(View view) {
|
||||
showHTML(R.string.version_history, content.getHistoryInfo(), view);
|
||||
}
|
||||
|
||||
private void showLicense(View view) {
|
||||
showHTML(R.string.license, content.getLicenseInfo(), view);
|
||||
}
|
||||
|
||||
private void showPrivacy(View view) {
|
||||
showHTML(R.string.privacy_policy, content.getPrivacyInfo(), view);
|
||||
}
|
||||
|
||||
private void showHTML(@StringRes int title, final Spanned text, View view) {
|
||||
int dialogContentPadding = getResources().getDimensionPixelSize(R.dimen.alert_dialog_content_padding);
|
||||
TextView textView = new TextView(this);
|
||||
textView.setText(text);
|
||||
Utils.makeTextViewLinksClickable(textView, text);
|
||||
ScrollView scrollView = new ScrollView(this);
|
||||
scrollView.addView(textView);
|
||||
scrollView.setPadding(dialogContentPadding, dialogContentPadding / 2, dialogContentPadding, 0);
|
||||
new MaterialAlertDialogBuilder(this)
|
||||
.setTitle(title)
|
||||
.setView(scrollView)
|
||||
.setPositiveButton(R.string.ok, null)
|
||||
.setNeutralButton(R.string.view_online, (dialog, which) -> openExternalBrowser(view))
|
||||
.show();
|
||||
}
|
||||
|
||||
private void openExternalBrowser(View view) {
|
||||
Object tag = view.getTag();
|
||||
if (tag instanceof String && ((String) tag).startsWith("https://")) {
|
||||
(new OpenWebLinkHandler()).openBrowser(this, (String) tag);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package protect.card_locker;
|
||||
import android.content.Context;
|
||||
import android.content.pm.PackageInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.text.Spanned;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.core.text.HtmlCompat;
|
||||
@@ -50,6 +51,10 @@ public class AboutContent {
|
||||
return String.format(context.getString(R.string.app_copyright_fmt), getCurrentYear());
|
||||
}
|
||||
|
||||
public String getCopyrightShort() {
|
||||
return context.getString(R.string.app_copyright_short);
|
||||
}
|
||||
|
||||
public String getContributors() {
|
||||
String contributors;
|
||||
try {
|
||||
@@ -60,6 +65,38 @@ public class AboutContent {
|
||||
return contributors.replace("\n", "<br />");
|
||||
}
|
||||
|
||||
public String getHistory() {
|
||||
String versionHistory;
|
||||
try {
|
||||
versionHistory = Utils.readTextFile(context, R.raw.changelog)
|
||||
.replace("# Changelog\n\n", "");
|
||||
} catch (IOException ignored) {
|
||||
return "";
|
||||
}
|
||||
return Utils.linkify(Utils.basicMDToHTML(versionHistory))
|
||||
.replace("\n", "<br />");
|
||||
}
|
||||
|
||||
public String getLicense() {
|
||||
try {
|
||||
return Utils.readTextFile(context, R.raw.license);
|
||||
} catch (IOException ignored) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
public String getPrivacy() {
|
||||
String privacyPolicy;
|
||||
try {
|
||||
privacyPolicy = Utils.readTextFile(context, R.raw.privacy)
|
||||
.replace("# Privacy Policy\n", "");
|
||||
} catch (IOException ignored) {
|
||||
return "";
|
||||
}
|
||||
return Utils.linkify(Utils.basicMDToHTML(privacyPolicy))
|
||||
.replace("\n", "<br />");
|
||||
}
|
||||
|
||||
public String getThirdPartyLibraries() {
|
||||
final List<ThirdPartyInfo> usedLibraries = new ArrayList<>();
|
||||
usedLibraries.add(new ThirdPartyInfo("Color Picker", "https://github.com/jaredrummler/ColorPicker", "Apache 2.0"));
|
||||
@@ -94,10 +131,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));
|
||||
@@ -105,6 +144,18 @@ public class AboutContent {
|
||||
return contributorInfo.toString();
|
||||
}
|
||||
|
||||
public Spanned getHistoryInfo() {
|
||||
return HtmlCompat.fromHtml(getHistory(), HtmlCompat.FROM_HTML_MODE_COMPACT);
|
||||
}
|
||||
|
||||
public Spanned getLicenseInfo() {
|
||||
return HtmlCompat.fromHtml(getLicense(), HtmlCompat.FROM_HTML_MODE_LEGACY);
|
||||
}
|
||||
|
||||
public Spanned getPrivacyInfo() {
|
||||
return HtmlCompat.fromHtml(getPrivacy(), HtmlCompat.FROM_HTML_MODE_COMPACT);
|
||||
}
|
||||
|
||||
public String getVersionHistory() {
|
||||
return String.format(context.getString(R.string.debug_version_fmt), getAppVersion());
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
package protect.card_locker;
|
||||
|
||||
public interface BarcodeImageWriterResultCallback {
|
||||
void onBarcodeImageWriterResult(boolean success);
|
||||
}
|
||||
@@ -41,13 +41,15 @@ public class BarcodeImageWriterTask implements CompatCallable<Bitmap> {
|
||||
private final CatimaBarcode format;
|
||||
private final int imageHeight;
|
||||
private final int imageWidth;
|
||||
private final int imagePadding;
|
||||
private final boolean widthPadding;
|
||||
private final boolean showFallback;
|
||||
private final Runnable callback;
|
||||
private final BarcodeImageWriterResultCallback callback;
|
||||
|
||||
BarcodeImageWriterTask(
|
||||
Context context, ImageView imageView, String cardIdString,
|
||||
CatimaBarcode barcodeFormat, TextView textView,
|
||||
boolean showFallback, Runnable callback, boolean roundCornerPadding
|
||||
boolean showFallback, BarcodeImageWriterResultCallback callback, boolean roundCornerPadding
|
||||
) {
|
||||
mContext = context;
|
||||
|
||||
@@ -61,32 +63,39 @@ public class BarcodeImageWriterTask implements CompatCallable<Bitmap> {
|
||||
cardId = cardIdString;
|
||||
format = barcodeFormat;
|
||||
|
||||
int padding = 0;
|
||||
int imageViewHeight = imageView.getHeight();
|
||||
int imageViewWidth = imageView.getWidth();
|
||||
|
||||
// Some barcodes already have internal whitespace and shouldn't get extra padding
|
||||
// TODO: Get rid of this hack by somehow detecting this extra whitespace
|
||||
if (roundCornerPadding && !barcodeFormat.hasInternalPadding()) {
|
||||
padding = Math.round(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 10, context.getResources().getDisplayMetrics()));
|
||||
imagePadding = Math.round(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 10, context.getResources().getDisplayMetrics()));
|
||||
} else {
|
||||
imagePadding = 0;
|
||||
}
|
||||
|
||||
if (format.isSquare() && imageViewWidth > imageViewHeight) {
|
||||
imageViewWidth -= imagePadding;
|
||||
widthPadding = true;
|
||||
} else {
|
||||
imageViewHeight -= imagePadding;
|
||||
widthPadding = false;
|
||||
}
|
||||
|
||||
final int MAX_WIDTH = getMaxWidth(format);
|
||||
|
||||
int tempImageHeight;
|
||||
int tempImageWidth;
|
||||
|
||||
if (imageView.getWidth() < MAX_WIDTH) {
|
||||
tempImageHeight = imageView.getHeight();
|
||||
tempImageWidth = imageView.getWidth();
|
||||
if (format.isSquare()) {
|
||||
imageHeight = imageWidth = Math.min(imageViewHeight, Math.min(MAX_WIDTH, imageViewWidth));
|
||||
} else if (imageView.getWidth() < MAX_WIDTH) {
|
||||
imageHeight = imageViewHeight;
|
||||
imageWidth = imageViewWidth;
|
||||
} else {
|
||||
// Scale down the image to reduce the memory needed to produce it
|
||||
tempImageWidth = MAX_WIDTH;
|
||||
double ratio = (double) MAX_WIDTH / (double) imageView.getWidth();
|
||||
tempImageHeight = (int) (imageView.getHeight() * ratio);
|
||||
imageWidth = MAX_WIDTH;
|
||||
double ratio = (double) MAX_WIDTH / (double) imageViewWidth;
|
||||
imageHeight = (int) (imageViewHeight * ratio);
|
||||
}
|
||||
|
||||
// Ensure space for padding if wanted
|
||||
imageWidth = tempImageWidth;
|
||||
imageHeight = tempImageHeight - padding;
|
||||
|
||||
this.showFallback = showFallback;
|
||||
}
|
||||
|
||||
@@ -94,12 +103,15 @@ public class BarcodeImageWriterTask implements CompatCallable<Bitmap> {
|
||||
switch (format.format()) {
|
||||
// 2D barcodes
|
||||
case AZTEC:
|
||||
case DATA_MATRIX:
|
||||
case MAXICODE:
|
||||
case PDF_417:
|
||||
case QR_CODE:
|
||||
return MAX_WIDTH_2D;
|
||||
|
||||
// 2D but rectangular versions get blurry otherwise
|
||||
case DATA_MATRIX:
|
||||
return MAX_WIDTH_1D;
|
||||
|
||||
// 1D barcodes:
|
||||
case CODABAR:
|
||||
case CODE_39:
|
||||
@@ -261,6 +273,11 @@ public class BarcodeImageWriterTask implements CompatCallable<Bitmap> {
|
||||
|
||||
if (result != null) {
|
||||
Log.i(TAG, "Displaying barcode");
|
||||
if (widthPadding) {
|
||||
imageView.setPadding(imagePadding / 2, 0, imagePadding / 2, 0);
|
||||
} else {
|
||||
imageView.setPadding(0, imagePadding / 2, 0, imagePadding / 2);
|
||||
}
|
||||
imageView.setVisibility(View.VISIBLE);
|
||||
|
||||
if (isSuccesful) {
|
||||
@@ -282,7 +299,7 @@ public class BarcodeImageWriterTask implements CompatCallable<Bitmap> {
|
||||
}
|
||||
|
||||
if (callback != null) {
|
||||
callback.run();
|
||||
callback.onBarcodeImageWriterResult(isSuccesful);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,6 @@ import com.google.zxing.BarcodeFormat;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
|
||||
import protect.card_locker.databinding.BarcodeSelectorActivityBinding;
|
||||
@@ -66,10 +65,6 @@ public class BarcodeSelectorActivity extends CatimaAppCompatActivity implements
|
||||
|
||||
runOnUiThread(() -> {
|
||||
generateBarcodes(s.toString());
|
||||
|
||||
View noBarcodeButtonView = binding.noBarcode;
|
||||
setButtonListener(noBarcodeButtonView, s.toString());
|
||||
noBarcodeButtonView.setEnabled(s.length() > 0);
|
||||
});
|
||||
}, INPUT_DELAY);
|
||||
}
|
||||
@@ -95,17 +90,6 @@ public class BarcodeSelectorActivity extends CatimaAppCompatActivity implements
|
||||
mAdapter.setBarcodes(barcodes);
|
||||
}
|
||||
|
||||
private void setButtonListener(final View button, final String cardId) {
|
||||
button.setOnClickListener(view -> {
|
||||
Log.d(TAG, "Selected no barcode");
|
||||
Intent result = new Intent();
|
||||
result.putExtra(BARCODE_FORMAT, "");
|
||||
result.putExtra(BARCODE_CONTENTS, cardId);
|
||||
BarcodeSelectorActivity.this.setResult(RESULT_OK, result);
|
||||
finish();
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
if (item.getItemId() == android.R.id.home) {
|
||||
|
||||
@@ -47,25 +47,19 @@ public class CardShortcutConfigure extends CatimaAppCompatActivity implements Lo
|
||||
finish();
|
||||
}
|
||||
|
||||
// If all cards are archived, bail
|
||||
if (DBHelper.getArchivedCardsCount(mDatabase) == cardCount) {
|
||||
Toast.makeText(this, R.string.noUnarchivedCardsMessage, Toast.LENGTH_LONG).show();
|
||||
finish();
|
||||
}
|
||||
|
||||
final RecyclerView cardList = binding.list;
|
||||
GridLayoutManager layoutManager = (GridLayoutManager) cardList.getLayoutManager();
|
||||
if (layoutManager != null) {
|
||||
layoutManager.setSpanCount(getResources().getInteger(R.integer.main_view_card_columns));
|
||||
}
|
||||
|
||||
Cursor cardCursor = DBHelper.getLoyaltyCardCursor(mDatabase, DBHelper.LoyaltyCardArchiveFilter.Unarchived);
|
||||
mAdapter = new LoyaltyCardCursorAdapter(this, cardCursor, this);
|
||||
Cursor cardCursor = DBHelper.getLoyaltyCardCursor(mDatabase, DBHelper.LoyaltyCardArchiveFilter.All);
|
||||
mAdapter = new LoyaltyCardCursorAdapter(this, cardCursor, this, null);
|
||||
cardList.setAdapter(mAdapter);
|
||||
}
|
||||
|
||||
private void onClickAction(int position) {
|
||||
Cursor selected = DBHelper.getLoyaltyCardCursor(mDatabase, DBHelper.LoyaltyCardArchiveFilter.Unarchived);
|
||||
Cursor selected = DBHelper.getLoyaltyCardCursor(mDatabase, DBHelper.LoyaltyCardArchiveFilter.All);
|
||||
selected.moveToPosition(position);
|
||||
LoyaltyCard loyaltyCard = LoyaltyCard.toLoyaltyCard(selected);
|
||||
|
||||
@@ -81,7 +75,7 @@ public class CardShortcutConfigure extends CatimaAppCompatActivity implements Lo
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu inputMenu) {
|
||||
getMenuInflater().inflate(R.menu.card_details_menu, inputMenu);
|
||||
Utils.updateMenuCardDetailsButtonState(inputMenu.findItem(R.id.action_unfold), mAdapter.showingDetails());
|
||||
|
||||
return super.onCreateOptionsMenu(inputMenu);
|
||||
}
|
||||
|
||||
@@ -89,8 +83,8 @@ public class CardShortcutConfigure extends CatimaAppCompatActivity implements Lo
|
||||
public boolean onOptionsItemSelected(MenuItem inputItem) {
|
||||
int id = inputItem.getItemId();
|
||||
|
||||
if (id == R.id.action_unfold) {
|
||||
mAdapter.showDetails(!mAdapter.showingDetails());
|
||||
if (id == R.id.action_display_options) {
|
||||
mAdapter.showDisplayOptionsDialog();
|
||||
invalidateOptionsMenu();
|
||||
|
||||
return true;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package protect.card_locker;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
@@ -69,10 +68,9 @@ public class CardsOnPowerScreenService extends ControlsProviderService {
|
||||
subscriber.onSubscribe(new NoOpSubscription());
|
||||
for (String controlId : controlIds) {
|
||||
Control control;
|
||||
|
||||
try {
|
||||
Integer cardId = this.controlIdToCardId(controlId);
|
||||
LoyaltyCard card = DBHelper.getLoyaltyCard(mDatabase, cardId);
|
||||
Integer cardId = this.controlIdToCardId(controlId);
|
||||
LoyaltyCard card = DBHelper.getLoyaltyCard(mDatabase, cardId);
|
||||
if (card != null) {
|
||||
Intent openIntent = new Intent(this, LoyaltyCardViewActivity.class)
|
||||
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
.putExtra("id", card.id);
|
||||
@@ -85,7 +83,7 @@ public class CardsOnPowerScreenService extends ControlsProviderService {
|
||||
.setControlTemplate(new StatelessTemplate(controlId))
|
||||
.setCustomIcon(Icon.createWithBitmap(getIcon(this, card)))
|
||||
.build();
|
||||
} catch (NullPointerException ignored) {
|
||||
} else {
|
||||
Intent mainScreenIntent = new Intent(this, MainActivity.class)
|
||||
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
PendingIntent pendingIntent = PendingIntent.getActivity(getBaseContext(), -1, mainScreenIntent, PendingIntent.FLAG_IMMUTABLE);
|
||||
|
||||
@@ -6,6 +6,7 @@ import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
@@ -49,4 +50,7 @@ public class CatimaAppCompatActivity extends AppCompatActivity {
|
||||
actionBar.setDisplayHomeAsUpEnabled(true);
|
||||
}
|
||||
}
|
||||
|
||||
public void onMockedRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,7 +67,6 @@ public class CatimaBarcode {
|
||||
|
||||
public boolean isSquare() {
|
||||
return mBarcodeFormat == BarcodeFormat.AZTEC
|
||||
|| mBarcodeFormat == BarcodeFormat.DATA_MATRIX
|
||||
|| mBarcodeFormat == BarcodeFormat.MAXICODE
|
||||
|| mBarcodeFormat == BarcodeFormat.QR_CODE;
|
||||
}
|
||||
|
||||
@@ -16,12 +16,17 @@ 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 = 15;
|
||||
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";
|
||||
@@ -33,6 +38,7 @@ public class DBHelper extends SQLiteOpenHelper {
|
||||
public static final String TABLE = "cards";
|
||||
public static final String ID = "_id";
|
||||
public static final String STORE = "store";
|
||||
public static final String VALID_FROM = "validfrom";
|
||||
public static final String EXPIRY = "expiry";
|
||||
public static final String BALANCE = "balance";
|
||||
public static final String BALANCE_TYPE = "balancetype";
|
||||
@@ -95,6 +101,7 @@ public class DBHelper extends SQLiteOpenHelper {
|
||||
LoyaltyCardDbIds.ID + " INTEGER primary key autoincrement," +
|
||||
LoyaltyCardDbIds.STORE + " TEXT not null," +
|
||||
LoyaltyCardDbIds.NOTE + " TEXT not null," +
|
||||
LoyaltyCardDbIds.VALID_FROM + " INTEGER," +
|
||||
LoyaltyCardDbIds.EXPIRY + " INTEGER," +
|
||||
LoyaltyCardDbIds.BALANCE + " TEXT not null DEFAULT '0'," +
|
||||
LoyaltyCardDbIds.BALANCE_TYPE + " TEXT," +
|
||||
@@ -104,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
|
||||
@@ -314,6 +321,26 @@ public class DBHelper extends SQLiteOpenHelper {
|
||||
db.execSQL("ALTER TABLE " + LoyaltyCardDbIds.TABLE
|
||||
+ " ADD COLUMN " + LoyaltyCardDbIds.ARCHIVE_STATUS + " INTEGER DEFAULT '0' ");
|
||||
}
|
||||
|
||||
if (oldVersion < 16 && newVersion >= 16) {
|
||||
db.execSQL("ALTER TABLE " + LoyaltyCardDbIds.TABLE
|
||||
+ " ADD COLUMN " + LoyaltyCardDbIds.VALID_FROM + " INTEGER");
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
@@ -358,8 +385,8 @@ public class DBHelper extends SQLiteOpenHelper {
|
||||
}
|
||||
|
||||
public static long insertLoyaltyCard(
|
||||
final SQLiteDatabase database, final String store, final String note, final Date expiry,
|
||||
final BigDecimal balance, final Currency balanceType, final String cardId,
|
||||
final SQLiteDatabase database, final String store, final String note, final Date validFrom,
|
||||
final Date expiry, final BigDecimal balance, final Currency balanceType, final String cardId,
|
||||
final String barcodeId, final CatimaBarcode barcodeType, final Integer headerColor,
|
||||
final int starStatus, final Long lastUsed, final int archiveStatus) {
|
||||
database.beginTransaction();
|
||||
@@ -368,6 +395,7 @@ public class DBHelper extends SQLiteOpenHelper {
|
||||
ContentValues contentValues = new ContentValues();
|
||||
contentValues.put(LoyaltyCardDbIds.STORE, store);
|
||||
contentValues.put(LoyaltyCardDbIds.NOTE, note);
|
||||
contentValues.put(LoyaltyCardDbIds.VALID_FROM, validFrom != null ? validFrom.getTime() : null);
|
||||
contentValues.put(LoyaltyCardDbIds.EXPIRY, expiry != null ? expiry.getTime() : null);
|
||||
contentValues.put(LoyaltyCardDbIds.BALANCE, balance.toString());
|
||||
contentValues.put(LoyaltyCardDbIds.BALANCE_TYPE, balanceType != null ? balanceType.getCurrencyCode() : null);
|
||||
@@ -391,9 +419,10 @@ public class DBHelper extends SQLiteOpenHelper {
|
||||
|
||||
public static long insertLoyaltyCard(
|
||||
final SQLiteDatabase database, final int id, final String store, final String note,
|
||||
final Date expiry, final BigDecimal balance, final Currency balanceType,
|
||||
final String cardId, final String barcodeId, final CatimaBarcode barcodeType,
|
||||
final Integer headerColor, final int starStatus, final Long lastUsed, final int archiveStatus) {
|
||||
final Date validFrom, final Date expiry, final BigDecimal balance,
|
||||
final Currency balanceType, final String cardId, final String barcodeId,
|
||||
final CatimaBarcode barcodeType, final Integer headerColor, final int starStatus,
|
||||
final Long lastUsed, final int archiveStatus) {
|
||||
database.beginTransaction();
|
||||
|
||||
// Card
|
||||
@@ -401,6 +430,7 @@ public class DBHelper extends SQLiteOpenHelper {
|
||||
contentValues.put(LoyaltyCardDbIds.ID, id);
|
||||
contentValues.put(LoyaltyCardDbIds.STORE, store);
|
||||
contentValues.put(LoyaltyCardDbIds.NOTE, note);
|
||||
contentValues.put(LoyaltyCardDbIds.VALID_FROM, validFrom != null ? validFrom.getTime() : null);
|
||||
contentValues.put(LoyaltyCardDbIds.EXPIRY, expiry != null ? expiry.getTime() : null);
|
||||
contentValues.put(LoyaltyCardDbIds.BALANCE, balance.toString());
|
||||
contentValues.put(LoyaltyCardDbIds.BALANCE_TYPE, balanceType != null ? balanceType.getCurrencyCode() : null);
|
||||
@@ -424,15 +454,17 @@ public class DBHelper extends SQLiteOpenHelper {
|
||||
|
||||
public static boolean updateLoyaltyCard(
|
||||
SQLiteDatabase database, final int id, final String store, final String note,
|
||||
final Date expiry, final BigDecimal balance, final Currency balanceType,
|
||||
final String cardId, final String barcodeId, final CatimaBarcode barcodeType,
|
||||
final Integer headerColor, final int starStatus, final Long lastUsed, final int archiveStatus) {
|
||||
final Date validFrom, final Date expiry, final BigDecimal balance,
|
||||
final Currency balanceType, final String cardId, final String barcodeId,
|
||||
final CatimaBarcode barcodeType, final Integer headerColor, final int starStatus,
|
||||
final Long lastUsed, final int archiveStatus) {
|
||||
database.beginTransaction();
|
||||
|
||||
// Card
|
||||
ContentValues contentValues = new ContentValues();
|
||||
contentValues.put(LoyaltyCardDbIds.STORE, store);
|
||||
contentValues.put(LoyaltyCardDbIds.NOTE, note);
|
||||
contentValues.put(LoyaltyCardDbIds.VALID_FROM, validFrom != null ? validFrom.getTime() : null);
|
||||
contentValues.put(LoyaltyCardDbIds.EXPIRY, expiry != null ? expiry.getTime() : null);
|
||||
contentValues.put(LoyaltyCardDbIds.BALANCE, balance.toString());
|
||||
contentValues.put(LoyaltyCardDbIds.BALANCE_TYPE, balanceType != null ? balanceType.getCurrencyCode() : null);
|
||||
|
||||
@@ -7,6 +7,7 @@ import android.database.sqlite.SQLiteDatabase;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Button;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.TextView;
|
||||
|
||||
@@ -17,7 +18,6 @@ import protect.card_locker.databinding.GroupLayoutBinding;
|
||||
import protect.card_locker.preferences.Settings;
|
||||
|
||||
public class GroupCursorAdapter extends BaseCursorAdapter<GroupCursorAdapter.GroupListItemViewHolder> {
|
||||
Settings mSettings;
|
||||
public final Context mContext;
|
||||
private final GroupAdapterListener mListener;
|
||||
SQLiteDatabase mDatabase;
|
||||
@@ -25,7 +25,6 @@ public class GroupCursorAdapter extends BaseCursorAdapter<GroupCursorAdapter.Gro
|
||||
public GroupCursorAdapter(Context inputContext, Cursor inputCursor, GroupAdapterListener inputListener) {
|
||||
super(inputCursor, DBHelper.LoyaltyCardDbGroups.ORDER);
|
||||
setHasStableIds(true);
|
||||
mSettings = new Settings(inputContext);
|
||||
mContext = inputContext;
|
||||
mListener = inputListener;
|
||||
mDatabase = new DBHelper(inputContext).getReadableDatabase();
|
||||
@@ -63,8 +62,6 @@ public class GroupCursorAdapter extends BaseCursorAdapter<GroupCursorAdapter.Gro
|
||||
}
|
||||
|
||||
inputHolder.mCardCount.setText(cardCountText);
|
||||
inputHolder.mName.setTextSize(mSettings.getFontSizeMax(mSettings.getMediumFont()));
|
||||
inputHolder.mCardCount.setTextSize(mSettings.getFontSizeMax(mSettings.getSmallFont()));
|
||||
|
||||
applyClickEvents(inputHolder);
|
||||
}
|
||||
@@ -88,7 +85,7 @@ public class GroupCursorAdapter extends BaseCursorAdapter<GroupCursorAdapter.Gro
|
||||
|
||||
public static class GroupListItemViewHolder extends RecyclerView.ViewHolder {
|
||||
public TextView mName, mCardCount;
|
||||
public ImageButton mMoveUp, mMoveDown, mEdit, mDelete;
|
||||
public Button mMoveUp, mMoveDown, mEdit, mDelete;
|
||||
|
||||
public GroupListItemViewHolder(GroupLayoutBinding groupLayoutBinding) {
|
||||
super(groupLayoutBinding.getRoot());
|
||||
|
||||
@@ -43,8 +43,6 @@ public class ImportExportActivity extends CatimaAppCompatActivity {
|
||||
private ImportExportActivityBinding binding;
|
||||
private static final String TAG = "Catima";
|
||||
|
||||
private static final int PERMISSIONS_EXTERNAL_STORAGE = 1;
|
||||
|
||||
private ImportExportTask importExporter;
|
||||
|
||||
private String importAlertTitle;
|
||||
@@ -68,19 +66,6 @@ public class ImportExportActivity extends CatimaAppCompatActivity {
|
||||
setSupportActionBar(toolbar);
|
||||
enableToolbarBackButton();
|
||||
|
||||
// If the application does not have permissions to external
|
||||
// storage, ask for it now
|
||||
|
||||
if (ContextCompat.checkSelfPermission(ImportExportActivity.this,
|
||||
Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED ||
|
||||
ContextCompat.checkSelfPermission(ImportExportActivity.this,
|
||||
Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
|
||||
ActivityCompat.requestPermissions(ImportExportActivity.this,
|
||||
new String[]{Manifest.permission.READ_EXTERNAL_STORAGE,
|
||||
Manifest.permission.WRITE_EXTERNAL_STORAGE},
|
||||
PERMISSIONS_EXTERNAL_STORAGE);
|
||||
}
|
||||
|
||||
Intent fileIntent = getIntent();
|
||||
if (fileIntent != null && fileIntent.getType() != null) {
|
||||
chooseImportType(false, fileIntent.getData());
|
||||
@@ -312,30 +297,6 @@ public class ImportExportActivity extends CatimaAppCompatActivity {
|
||||
mTasks.executeTask(TaskHandler.TYPE.EXPORT, importExporter);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
||||
|
||||
if (requestCode == PERMISSIONS_EXTERNAL_STORAGE) {
|
||||
// If request is cancelled, the result arrays are empty.
|
||||
boolean success = grantResults.length > 0;
|
||||
|
||||
for (int grant : grantResults) {
|
||||
if (grant != PackageManager.PERMISSION_GRANTED) {
|
||||
success = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!success) {
|
||||
// External storage permission rejected, inform user that
|
||||
// import/export is prevented
|
||||
Toast.makeText(getApplicationContext(), R.string.noExternalStoragePermissionError,
|
||||
Toast.LENGTH_LONG).show();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
mTasks.flushTaskList(TaskHandler.TYPE.IMPORT, true, false, false);
|
||||
|
||||
@@ -18,6 +18,7 @@ import java.util.List;
|
||||
public class ImportURIHelper {
|
||||
private static final String STORE = DBHelper.LoyaltyCardDbIds.STORE;
|
||||
private static final String NOTE = DBHelper.LoyaltyCardDbIds.NOTE;
|
||||
private static final String VALID_FROM = DBHelper.LoyaltyCardDbIds.VALID_FROM;
|
||||
private static final String EXPIRY = DBHelper.LoyaltyCardDbIds.EXPIRY;
|
||||
private static final String BALANCE = DBHelper.LoyaltyCardDbIds.BALANCE;
|
||||
private static final String BALANCE_TYPE = DBHelper.LoyaltyCardDbIds.BALANCE_TYPE;
|
||||
@@ -45,8 +46,11 @@ public class ImportURIHelper {
|
||||
}
|
||||
|
||||
private boolean isImportUri(Uri uri) {
|
||||
// Remove trailing slash added by some browsers (if it exists)
|
||||
final String uriPath = uri.getPath().replaceAll("/$", "");
|
||||
|
||||
for (int i = 0; i < hosts.length; i++) {
|
||||
if (uri.getHost().equals(hosts[i]) && uri.getPath().equals(paths[i])) {
|
||||
if (uri.getHost().equals(hosts[i]) && uriPath.equals(paths[i])) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -62,6 +66,7 @@ public class ImportURIHelper {
|
||||
try {
|
||||
// These values are allowed to be null
|
||||
CatimaBarcode barcodeType = null;
|
||||
Date validFrom = null;
|
||||
Date expiry = null;
|
||||
BigDecimal balance = new BigDecimal("0");
|
||||
Currency balanceType = null;
|
||||
@@ -106,6 +111,10 @@ public class ImportURIHelper {
|
||||
if (unparsedBalanceType != null && !unparsedBalanceType.equals("")) {
|
||||
balanceType = Currency.getInstance(unparsedBalanceType);
|
||||
}
|
||||
String unparsedValidFrom = kv.get(VALID_FROM);
|
||||
if (unparsedValidFrom != null && !unparsedValidFrom.equals("")) {
|
||||
validFrom = new Date(Long.parseLong(unparsedValidFrom));
|
||||
}
|
||||
String unparsedExpiry = kv.get(EXPIRY);
|
||||
if (unparsedExpiry != null && !unparsedExpiry.equals("")) {
|
||||
expiry = new Date(Long.parseLong(unparsedExpiry));
|
||||
@@ -116,8 +125,8 @@ public class ImportURIHelper {
|
||||
headerColor = Integer.parseInt(unparsedHeaderColor);
|
||||
}
|
||||
|
||||
return new LoyaltyCard(-1, store, note, expiry, balance, balanceType, cardId, barcodeId, barcodeType, headerColor, 0, Utils.getUnixTime(), 100,0);
|
||||
} catch (NullPointerException | NumberFormatException | UnsupportedEncodingException ex) {
|
||||
return new LoyaltyCard(-1, store, note, validFrom, expiry, balance, balanceType, cardId, barcodeId, barcodeType, headerColor, 0, Utils.getUnixTime(), 100, 0);
|
||||
} catch (NumberFormatException | UnsupportedEncodingException | ArrayIndexOutOfBoundsException ex) {
|
||||
throw new InvalidObjectException("Not a valid import URI");
|
||||
}
|
||||
}
|
||||
@@ -149,6 +158,9 @@ public class ImportURIHelper {
|
||||
if (loyaltyCard.balanceType != null) {
|
||||
fragment = appendFragment(fragment, BALANCE_TYPE, loyaltyCard.balanceType.getCurrencyCode());
|
||||
}
|
||||
if (loyaltyCard.validFrom != null) {
|
||||
fragment = appendFragment(fragment, VALID_FROM, String.valueOf(loyaltyCard.validFrom.getTime()));
|
||||
}
|
||||
if (loyaltyCard.expiry != null) {
|
||||
fragment = appendFragment(fragment, EXPIRY, String.valueOf(loyaltyCard.expiry.getTime()));
|
||||
}
|
||||
|
||||
@@ -8,39 +8,41 @@ import java.math.BigDecimal;
|
||||
import java.util.Currency;
|
||||
import java.util.Date;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
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 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;
|
||||
this.store = store;
|
||||
this.note = note;
|
||||
this.validFrom = validFrom;
|
||||
this.expiry = expiry;
|
||||
this.balance = balance;
|
||||
this.balanceType = balanceType;
|
||||
@@ -58,6 +60,8 @@ public class LoyaltyCard implements Parcelable {
|
||||
id = in.readInt();
|
||||
store = in.readString();
|
||||
note = in.readString();
|
||||
long tmpValidFrom = in.readLong();
|
||||
validFrom = tmpValidFrom != -1 ? new Date(tmpValidFrom) : null;
|
||||
long tmpExpiry = in.readLong();
|
||||
expiry = tmpExpiry != -1 ? new Date(tmpExpiry) : null;
|
||||
balance = (BigDecimal) in.readValue(BigDecimal.class.getClassLoader());
|
||||
@@ -79,6 +83,7 @@ public class LoyaltyCard implements Parcelable {
|
||||
parcel.writeInt(id);
|
||||
parcel.writeString(store);
|
||||
parcel.writeString(note);
|
||||
parcel.writeLong(validFrom != null ? validFrom.getTime() : -1);
|
||||
parcel.writeLong(expiry != null ? expiry.getTime() : -1);
|
||||
parcel.writeValue(balance);
|
||||
parcel.writeValue(balanceType);
|
||||
@@ -96,6 +101,7 @@ public class LoyaltyCard implements Parcelable {
|
||||
int id = cursor.getInt(cursor.getColumnIndexOrThrow(DBHelper.LoyaltyCardDbIds.ID));
|
||||
String store = cursor.getString(cursor.getColumnIndexOrThrow(DBHelper.LoyaltyCardDbIds.STORE));
|
||||
String note = cursor.getString(cursor.getColumnIndexOrThrow(DBHelper.LoyaltyCardDbIds.NOTE));
|
||||
long validFromLong = cursor.getLong(cursor.getColumnIndexOrThrow(DBHelper.LoyaltyCardDbIds.VALID_FROM));
|
||||
long expiryLong = cursor.getLong(cursor.getColumnIndexOrThrow(DBHelper.LoyaltyCardDbIds.EXPIRY));
|
||||
BigDecimal balance = new BigDecimal(cursor.getString(cursor.getColumnIndexOrThrow(DBHelper.LoyaltyCardDbIds.BALANCE)));
|
||||
String cardId = cursor.getString(cursor.getColumnIndexOrThrow(DBHelper.LoyaltyCardDbIds.CARD_ID));
|
||||
@@ -111,6 +117,7 @@ public class LoyaltyCard implements Parcelable {
|
||||
|
||||
CatimaBarcode barcodeType = null;
|
||||
Currency balanceType = null;
|
||||
Date validFrom = null;
|
||||
Date expiry = null;
|
||||
Integer headerColor = null;
|
||||
|
||||
@@ -122,6 +129,10 @@ public class LoyaltyCard implements Parcelable {
|
||||
balanceType = Currency.getInstance(cursor.getString(balanceTypeColumn));
|
||||
}
|
||||
|
||||
if (validFromLong > 0) {
|
||||
validFrom = new Date(validFromLong);
|
||||
}
|
||||
|
||||
if (expiryLong > 0) {
|
||||
expiry = new Date(expiryLong);
|
||||
}
|
||||
@@ -130,7 +141,25 @@ public class LoyaltyCard implements Parcelable {
|
||||
headerColor = cursor.getInt(headerColorColumn);
|
||||
}
|
||||
|
||||
return new LoyaltyCard(id, store, note, expiry, balance, balanceType, cardId, barcodeId, barcodeType, headerColor, starred, lastUsed, zoomLevel,archived);
|
||||
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
|
||||
@@ -138,6 +167,31 @@ public class LoyaltyCard implements Parcelable {
|
||||
return id;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format(
|
||||
"LoyaltyCard{%n id=%s,%n store=%s,%n note=%s,%n validFrom=%s,%n expiry=%s,%n"
|
||||
+ " balance=%s,%n balanceType=%s,%n cardId=%s,%n barcodeId=%s,%n barcodeType=%s,%n"
|
||||
+ " headerColor=%s,%n starStatus=%s,%n lastUsed=%s,%n zoomLevel=%s,%n archiveStatus=%s%n}",
|
||||
this.id,
|
||||
this.store,
|
||||
this.note,
|
||||
this.validFrom,
|
||||
this.expiry,
|
||||
this.balance,
|
||||
this.balanceType,
|
||||
this.cardId,
|
||||
this.barcodeId,
|
||||
this.barcodeType != null ? this.barcodeType.format() : null,
|
||||
this.headerColor,
|
||||
this.starStatus,
|
||||
this.lastUsed,
|
||||
this.zoomLevel,
|
||||
this.archiveStatus
|
||||
);
|
||||
}
|
||||
|
||||
public static final Creator<LoyaltyCard> CREATOR = new Creator<LoyaltyCard>() {
|
||||
@Override
|
||||
public LoyaltyCard createFromParcel(Parcel in) {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package protect.card_locker;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.res.Resources;
|
||||
import android.database.Cursor;
|
||||
import android.graphics.Bitmap;
|
||||
@@ -31,58 +30,40 @@ import androidx.core.graphics.BlendModeCompat;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import protect.card_locker.databinding.LoyaltyCardLayoutBinding;
|
||||
import protect.card_locker.preferences.Settings;
|
||||
|
||||
public class LoyaltyCardCursorAdapter extends BaseCursorAdapter<LoyaltyCardCursorAdapter.LoyaltyCardListItemViewHolder> {
|
||||
private int mCurrentSelectedIndex = -1;
|
||||
Settings mSettings;
|
||||
boolean mDarkModeEnabled;
|
||||
public final Context mContext;
|
||||
private final CardAdapterListener mListener;
|
||||
private final LoyaltyCardListDisplayOptionsManager mLoyaltyCardListDisplayOptions;
|
||||
protected SparseBooleanArray mSelectedItems;
|
||||
protected SparseBooleanArray mAnimationItemsIndex;
|
||||
private boolean mReverseAllAnimations = false;
|
||||
private boolean mShowDetails;
|
||||
|
||||
public LoyaltyCardCursorAdapter(Context inputContext, Cursor inputCursor, CardAdapterListener inputListener) {
|
||||
public LoyaltyCardCursorAdapter(Context inputContext, Cursor inputCursor, CardAdapterListener inputListener, Runnable inputSwapCursorCallback) {
|
||||
super(inputCursor, DBHelper.LoyaltyCardDbIds.ID);
|
||||
setHasStableIds(true);
|
||||
mSettings = new Settings(inputContext);
|
||||
mContext = inputContext;
|
||||
mListener = inputListener;
|
||||
|
||||
Runnable refreshCardsCallback = () -> notifyDataSetChanged();
|
||||
|
||||
mLoyaltyCardListDisplayOptions = new LoyaltyCardListDisplayOptionsManager(mContext, refreshCardsCallback, inputSwapCursorCallback);
|
||||
mSelectedItems = new SparseBooleanArray();
|
||||
mAnimationItemsIndex = new SparseBooleanArray();
|
||||
|
||||
mDarkModeEnabled = Utils.isDarkModeEnabled(inputContext);
|
||||
|
||||
refreshState();
|
||||
|
||||
swapCursor(inputCursor);
|
||||
}
|
||||
|
||||
public void refreshState() {
|
||||
// Retrieve user details preference
|
||||
SharedPreferences cardDetailsPref = mContext.getSharedPreferences(
|
||||
mContext.getString(R.string.sharedpreference_card_details),
|
||||
Context.MODE_PRIVATE);
|
||||
mShowDetails = cardDetailsPref.getBoolean(mContext.getString(R.string.sharedpreference_card_details_show), true);
|
||||
public void showDisplayOptionsDialog() {
|
||||
mLoyaltyCardListDisplayOptions.showDisplayOptionsDialog();
|
||||
}
|
||||
|
||||
public void showDetails(boolean show) {
|
||||
mShowDetails = show;
|
||||
notifyDataSetChanged();
|
||||
|
||||
// Store in Shared Preference to restore next adapter launch
|
||||
SharedPreferences cardDetailsPref = mContext.getSharedPreferences(
|
||||
mContext.getString(R.string.sharedpreference_card_details),
|
||||
Context.MODE_PRIVATE);
|
||||
SharedPreferences.Editor cardDetailsPrefEditor = cardDetailsPref.edit();
|
||||
cardDetailsPrefEditor.putBoolean(mContext.getString(R.string.sharedpreference_card_details_show), show);
|
||||
cardDetailsPrefEditor.apply();
|
||||
}
|
||||
|
||||
public boolean showingDetails() {
|
||||
return mShowDetails;
|
||||
public boolean showingArchivedCards() {
|
||||
return mLoyaltyCardListDisplayOptions.showingArchivedCards();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@@ -103,39 +84,47 @@ public class LoyaltyCardCursorAdapter extends BaseCursorAdapter<LoyaltyCardCurso
|
||||
|
||||
public void onBindViewHolder(LoyaltyCardListItemViewHolder inputHolder, Cursor inputCursor) {
|
||||
// Invisible until we want to show something more
|
||||
boolean showDivider = false;
|
||||
inputHolder.mDivider.setVisibility(View.GONE);
|
||||
|
||||
LoyaltyCard loyaltyCard = LoyaltyCard.toLoyaltyCard(inputCursor);
|
||||
Bitmap icon = Utils.retrieveCardImage(mContext, loyaltyCard.id, ImageLocationType.icon);
|
||||
|
||||
inputHolder.setStoreField(loyaltyCard.store);
|
||||
if (mShowDetails && !loyaltyCard.note.isEmpty()) {
|
||||
if (mLoyaltyCardListDisplayOptions.showingNameBelowThumbnail() && icon != null) {
|
||||
showDivider = true;
|
||||
inputHolder.setStoreField(loyaltyCard.store);
|
||||
} else {
|
||||
inputHolder.setStoreField(null);
|
||||
}
|
||||
|
||||
if (mLoyaltyCardListDisplayOptions.showingNote() && !loyaltyCard.note.isEmpty()) {
|
||||
showDivider = true;
|
||||
inputHolder.setNoteField(loyaltyCard.note);
|
||||
} else {
|
||||
inputHolder.setNoteField(null);
|
||||
}
|
||||
|
||||
if (mShowDetails && !loyaltyCard.balance.equals(new BigDecimal("0"))) {
|
||||
inputHolder.setExtraField(inputHolder.mBalanceField, Utils.formatBalance(mContext, loyaltyCard.balance, loyaltyCard.balanceType), null);
|
||||
if (mLoyaltyCardListDisplayOptions.showingBalance() && !loyaltyCard.balance.equals(new BigDecimal("0"))) {
|
||||
inputHolder.setExtraField(inputHolder.mBalanceField, Utils.formatBalance(mContext, loyaltyCard.balance, loyaltyCard.balanceType), null, showDivider);
|
||||
} else {
|
||||
inputHolder.setExtraField(inputHolder.mBalanceField, null, null);
|
||||
inputHolder.setExtraField(inputHolder.mBalanceField, null, null, false);
|
||||
}
|
||||
|
||||
if (mShowDetails && loyaltyCard.expiry != null) {
|
||||
inputHolder.setExtraField(inputHolder.mExpiryField, DateFormat.getDateInstance(DateFormat.LONG).format(loyaltyCard.expiry), Utils.hasExpired(loyaltyCard.expiry) ? Color.RED : null);
|
||||
if (mLoyaltyCardListDisplayOptions.showingValidity() && loyaltyCard.validFrom != null) {
|
||||
inputHolder.setExtraField(inputHolder.mValidFromField, DateFormat.getDateInstance(DateFormat.LONG).format(loyaltyCard.validFrom), Utils.isNotYetValid(loyaltyCard.validFrom) ? Color.RED : null, showDivider);
|
||||
} else {
|
||||
inputHolder.setExtraField(inputHolder.mExpiryField, null, null);
|
||||
inputHolder.setExtraField(inputHolder.mValidFromField, null, null, false);
|
||||
}
|
||||
|
||||
setHeaderHeight(inputHolder, mShowDetails);
|
||||
Bitmap cardIcon = Utils.retrieveCardImage(mContext, loyaltyCard.id, ImageLocationType.icon);
|
||||
if (cardIcon != null) {
|
||||
inputHolder.mCardIcon.setImageBitmap(cardIcon);
|
||||
inputHolder.mCardIcon.setScaleType(ImageView.ScaleType.CENTER_CROP);
|
||||
if (mLoyaltyCardListDisplayOptions.showingValidity() && loyaltyCard.expiry != null) {
|
||||
inputHolder.setExtraField(inputHolder.mExpiryField, DateFormat.getDateInstance(DateFormat.LONG).format(loyaltyCard.expiry), Utils.hasExpired(loyaltyCard.expiry) ? Color.RED : null, showDivider);
|
||||
} else {
|
||||
inputHolder.mCardIcon.setImageBitmap(Utils.generateIcon(mContext, loyaltyCard.store, loyaltyCard.headerColor).getLetterTile());
|
||||
inputHolder.mCardIcon.setScaleType(ImageView.ScaleType.FIT_CENTER);
|
||||
inputHolder.setExtraField(inputHolder.mExpiryField, null, null, false);
|
||||
}
|
||||
inputHolder.setIconBackgroundColor(loyaltyCard.headerColor != null ? loyaltyCard.headerColor : R.attr.colorPrimary);
|
||||
|
||||
inputHolder.mCardIcon.setContentDescription(loyaltyCard.store);
|
||||
Utils.setIconOrTextWithBackground(mContext, loyaltyCard, icon, inputHolder.mCardIcon, inputHolder.mCardText);
|
||||
inputHolder.setIconBackgroundColor(Utils.getHeaderColor(mContext, loyaltyCard));
|
||||
|
||||
inputHolder.toggleCardStateIcon(loyaltyCard.starStatus != 0, loyaltyCard.archiveStatus != 0, itemSelected(inputCursor.getPosition()));
|
||||
|
||||
@@ -147,19 +136,6 @@ public class LoyaltyCardCursorAdapter extends BaseCursorAdapter<LoyaltyCardCurso
|
||||
inputHolder.mRow.requestLayout();
|
||||
}
|
||||
|
||||
private void setHeaderHeight(LoyaltyCardListItemViewHolder inputHolder, boolean expanded) {
|
||||
int iconHeight;
|
||||
if (expanded) {
|
||||
iconHeight = ViewGroup.LayoutParams.MATCH_PARENT;
|
||||
} else {
|
||||
iconHeight = (int) mContext.getResources().getDimension(R.dimen.cardThumbnailSize);
|
||||
}
|
||||
|
||||
inputHolder.mIconLayout.getLayoutParams().height = expanded ? 0 : iconHeight;
|
||||
inputHolder.mCardIcon.getLayoutParams().height = iconHeight;
|
||||
inputHolder.mTickIcon.getLayoutParams().height = iconHeight;
|
||||
}
|
||||
|
||||
private void applyClickEvents(LoyaltyCardListItemViewHolder inputHolder, final int inputPosition) {
|
||||
inputHolder.mRow.setOnClickListener(inputView -> mListener.onRowClicked(inputPosition));
|
||||
|
||||
@@ -235,16 +211,14 @@ public class LoyaltyCardCursorAdapter extends BaseCursorAdapter<LoyaltyCardCurso
|
||||
|
||||
public class LoyaltyCardListItemViewHolder extends RecyclerView.ViewHolder {
|
||||
|
||||
public TextView mStoreField, mNoteField, mBalanceField, mExpiryField;
|
||||
public TextView mCardText, mStoreField, mNoteField, mBalanceField, mValidFromField, mExpiryField;
|
||||
public ImageView mCardIcon, mStarBackground, mStarBorder, mTickIcon, mArchivedBackground;
|
||||
public MaterialCardView mRow, mIconLayout;
|
||||
public MaterialCardView mRow;
|
||||
public ConstraintLayout mStar, mArchived;
|
||||
public View mDivider;
|
||||
|
||||
private int mIconBackgroundColor;
|
||||
|
||||
|
||||
|
||||
protected LoyaltyCardListItemViewHolder(LoyaltyCardLayoutBinding loyaltyCardLayoutBinding, CardAdapterListener inputListener) {
|
||||
super(loyaltyCardLayoutBinding.getRoot());
|
||||
View inputView = loyaltyCardLayoutBinding.getRoot();
|
||||
@@ -253,9 +227,10 @@ public class LoyaltyCardCursorAdapter extends BaseCursorAdapter<LoyaltyCardCurso
|
||||
mStoreField = loyaltyCardLayoutBinding.store;
|
||||
mNoteField = loyaltyCardLayoutBinding.note;
|
||||
mBalanceField = loyaltyCardLayoutBinding.balance;
|
||||
mValidFromField = loyaltyCardLayoutBinding.validFrom;
|
||||
mExpiryField = loyaltyCardLayoutBinding.expiry;
|
||||
mIconLayout = loyaltyCardLayoutBinding.iconLayout;
|
||||
mCardIcon = loyaltyCardLayoutBinding.thumbnail;
|
||||
mCardText = loyaltyCardLayoutBinding.thumbnailText;
|
||||
mStar = loyaltyCardLayoutBinding.star;
|
||||
mStarBackground = loyaltyCardLayoutBinding.starBackground;
|
||||
mStarBorder = loyaltyCardLayoutBinding.starBorder;
|
||||
@@ -269,7 +244,7 @@ public class LoyaltyCardCursorAdapter extends BaseCursorAdapter<LoyaltyCardCurso
|
||||
});
|
||||
}
|
||||
|
||||
private void setExtraField(TextView field, String text, Integer color) {
|
||||
private void setExtraField(TextView field, String text, Integer color, boolean showDivider) {
|
||||
// If text is null, hide the field
|
||||
// If iconColor is null, use the default text and icon color based on theme
|
||||
if (text == null) {
|
||||
@@ -278,20 +253,18 @@ public class LoyaltyCardCursorAdapter extends BaseCursorAdapter<LoyaltyCardCurso
|
||||
return;
|
||||
}
|
||||
|
||||
int size = mSettings.getFontSizeMax(mSettings.getSmallFont());
|
||||
// Shown when there is a name and/or note and at least 1 extra field
|
||||
if (showDivider) {
|
||||
mDivider.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
field.setVisibility(View.VISIBLE);
|
||||
field.setText(text);
|
||||
field.setTextSize(size);
|
||||
field.setTextColor(color != null ? color : MaterialColors.getColor(mContext, R.attr.colorSecondary, ContextCompat.getColor(mContext, mDarkModeEnabled ? R.color.md_theme_dark_secondary : R.color.md_theme_light_secondary)));
|
||||
|
||||
int drawableSize = dpToPx((size * 24) / 14, mContext);
|
||||
mDivider.setVisibility(View.VISIBLE);
|
||||
field.setTextColor(color != null ? color : MaterialColors.getColor(mContext, com.google.android.material.R.attr.colorSecondary, ContextCompat.getColor(mContext, mDarkModeEnabled ? R.color.md_theme_dark_secondary : R.color.md_theme_light_secondary)));
|
||||
field.setVisibility(View.VISIBLE);
|
||||
|
||||
Drawable icon = field.getCompoundDrawables()[0];
|
||||
if (icon != null) {
|
||||
icon.mutate();
|
||||
icon.setBounds(0, 0, drawableSize, drawableSize);
|
||||
field.setCompoundDrawablesRelative(icon, null, null, null);
|
||||
|
||||
if (color != null) {
|
||||
@@ -305,8 +278,12 @@ public class LoyaltyCardCursorAdapter extends BaseCursorAdapter<LoyaltyCardCurso
|
||||
}
|
||||
|
||||
public void setStoreField(String text) {
|
||||
mStoreField.setText(text);
|
||||
mStoreField.setTextSize(mSettings.getFontSizeMax(mSettings.getMediumFont()));
|
||||
if (text == null) {
|
||||
mStoreField.setVisibility(View.GONE);
|
||||
} else {
|
||||
mStoreField.setVisibility(View.VISIBLE);
|
||||
mStoreField.setText(text);
|
||||
}
|
||||
mStoreField.requestLayout();
|
||||
}
|
||||
|
||||
@@ -316,7 +293,6 @@ public class LoyaltyCardCursorAdapter extends BaseCursorAdapter<LoyaltyCardCurso
|
||||
} else {
|
||||
mNoteField.setVisibility(View.VISIBLE);
|
||||
mNoteField.setText(text);
|
||||
mNoteField.setTextSize(mSettings.getFontSizeMax(mSettings.getSmallFont()));
|
||||
}
|
||||
mNoteField.requestLayout();
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package protect.card_locker;
|
||||
|
||||
import android.Manifest;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.app.DatePickerDialog;
|
||||
@@ -24,6 +23,7 @@ import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.ViewTreeObserver;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.ArrayAdapter;
|
||||
@@ -31,10 +31,25 @@ import android.widget.AutoCompleteTextView;
|
||||
import android.widget.Button;
|
||||
import android.widget.DatePicker;
|
||||
import android.widget.EditText;
|
||||
import android.widget.FrameLayout;
|
||||
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;
|
||||
@@ -66,24 +81,14 @@ 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.appcompat.app.ActionBar;
|
||||
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;
|
||||
|
||||
public class LoyaltyCardEditActivity extends CatimaAppCompatActivity {
|
||||
public class LoyaltyCardEditActivity extends CatimaAppCompatActivity implements BarcodeImageWriterResultCallback, ColorPickerDialogListener {
|
||||
private LoyaltyCardEditActivityBinding binding;
|
||||
private static final String TAG = "Catima";
|
||||
|
||||
@@ -98,6 +103,7 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity {
|
||||
private final String STATE_FRONT_IMAGE_REMOVED = "frontImageRemoved";
|
||||
private final String STATE_BACK_IMAGE_REMOVED = "backImageRemoved";
|
||||
private final String STATE_ICON_REMOVED = "iconRemoved";
|
||||
private final String STATE_OPEN_SET_ICON_MENU = "openSetIconMenu";
|
||||
|
||||
private final String TEMP_CAMERA_IMAGE_NAME = LoyaltyCardEditActivity.class.getSimpleName() + "_camera_image.jpg";
|
||||
private final String TEMP_CROP_IMAGE_NAME = LoyaltyCardEditActivity.class.getSimpleName() + "_crop_image.png";
|
||||
@@ -111,10 +117,14 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity {
|
||||
private static final int PERMISSION_REQUEST_CAMERA_IMAGE_FRONT = 100;
|
||||
private static final int PERMISSION_REQUEST_CAMERA_IMAGE_BACK = 101;
|
||||
private static final int PERMISSION_REQUEST_CAMERA_IMAGE_ICON = 102;
|
||||
private static final int PERMISSION_REQUEST_STORAGE_IMAGE_FRONT = 103;
|
||||
private static final int PERMISSION_REQUEST_STORAGE_IMAGE_BACK = 104;
|
||||
private static final int PERMISSION_REQUEST_STORAGE_IMAGE_ICON = 105;
|
||||
|
||||
public static final String BUNDLE_ID = "id";
|
||||
public static final String BUNDLE_DUPLICATE_ID = "duplicateId";
|
||||
public static final String BUNDLE_UPDATE = "update";
|
||||
public static final String BUNDLE_OPEN_SET_ICON_MENU = "openSetIconMenu";
|
||||
public static final String BUNDLE_CARDID = "cardId";
|
||||
public static final String BUNDLE_BARCODEID = "barcodeId";
|
||||
public static final String BUNDLE_BARCODETYPE = "barcodeType";
|
||||
@@ -127,6 +137,7 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity {
|
||||
EditText storeFieldEdit;
|
||||
EditText noteFieldEdit;
|
||||
ChipGroup groupsChips;
|
||||
AutoCompleteTextView validFromField;
|
||||
AutoCompleteTextView expiryField;
|
||||
EditText balanceField;
|
||||
AutoCompleteTextView balanceCurrencyField;
|
||||
@@ -148,6 +159,7 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity {
|
||||
int loyaltyCardId;
|
||||
boolean updateLoyaltyCard;
|
||||
boolean duplicateFromLoyaltyCardId;
|
||||
boolean openSetIconMenu;
|
||||
String cardId;
|
||||
String barcodeId;
|
||||
String barcodeType;
|
||||
@@ -162,12 +174,12 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity {
|
||||
String tempStoredOldBarcodeValue = null;
|
||||
boolean initDone = false;
|
||||
boolean onResuming = false;
|
||||
boolean onRestoring = false;
|
||||
AlertDialog confirmExitDialog = null;
|
||||
|
||||
boolean validBalance = true;
|
||||
Runnable barcodeImageGenerationFinishedCallback;
|
||||
|
||||
HashMap<String, Currency> currencies = new HashMap<>();
|
||||
HashMap<String, String> currencySymbols = new HashMap<>();
|
||||
|
||||
LoyaltyCard tempLoyaltyCard;
|
||||
|
||||
@@ -205,6 +217,7 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity {
|
||||
(int) (fieldName == LoyaltyCardField.id ? value : loyaltyCard.id),
|
||||
(String) (fieldName == LoyaltyCardField.store ? value : loyaltyCard.store),
|
||||
(String) (fieldName == LoyaltyCardField.note ? value : loyaltyCard.note),
|
||||
(Date) (fieldName == LoyaltyCardField.validFrom ? value : loyaltyCard.validFrom),
|
||||
(Date) (fieldName == LoyaltyCardField.expiry ? value : loyaltyCard.expiry),
|
||||
(BigDecimal) (fieldName == LoyaltyCardField.balance ? value : loyaltyCard.balance),
|
||||
(Currency) (fieldName == LoyaltyCardField.balanceType ? value : loyaltyCard.balanceType),
|
||||
@@ -219,7 +232,7 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity {
|
||||
);
|
||||
}
|
||||
|
||||
private void updateTempState(LoyaltyCardField fieldName, Object value) {
|
||||
protected void updateTempState(LoyaltyCardField fieldName, Object value) {
|
||||
tempLoyaltyCard = updateTempState(tempLoyaltyCard, fieldName, value);
|
||||
|
||||
if (initDone && (fieldName == LoyaltyCardField.cardId || fieldName == LoyaltyCardField.barcodeId || fieldName == LoyaltyCardField.barcodeType)) {
|
||||
@@ -235,6 +248,8 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity {
|
||||
updateLoyaltyCard = b != null && b.getBoolean(BUNDLE_UPDATE, false);
|
||||
duplicateFromLoyaltyCardId = b != null && b.getBoolean(BUNDLE_DUPLICATE_ID, false);
|
||||
|
||||
openSetIconMenu = b != null && b.getBoolean(BUNDLE_OPEN_SET_ICON_MENU, false);
|
||||
|
||||
cardId = b != null ? b.getString(BUNDLE_CARDID) : null;
|
||||
barcodeId = b != null ? b.getString(BUNDLE_BARCODEID) : null;
|
||||
barcodeType = b != null ? b.getString(BUNDLE_BARCODETYPE) : null;
|
||||
@@ -246,7 +261,6 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity {
|
||||
+ ", updateLoyaltyCard=" + updateLoyaltyCard);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(@NonNull Bundle savedInstanceState) {
|
||||
super.onSaveInstanceState(savedInstanceState);
|
||||
@@ -281,10 +295,12 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity {
|
||||
savedInstanceState.putInt(STATE_FRONT_IMAGE_REMOVED, mFrontImageRemoved ? 1 : 0);
|
||||
savedInstanceState.putInt(STATE_BACK_IMAGE_REMOVED, mBackImageRemoved ? 1 : 0);
|
||||
savedInstanceState.putInt(STATE_ICON_REMOVED, mIconRemoved ? 1 : 0);
|
||||
savedInstanceState.putInt(STATE_OPEN_SET_ICON_MENU, openSetIconMenu ? 1 : 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRestoreInstanceState(@NonNull Bundle savedInstanceState) {
|
||||
onRestoring = true;
|
||||
tempLoyaltyCard = savedInstanceState.getParcelable(STATE_TEMP_CARD);
|
||||
super.onRestoreInstanceState(savedInstanceState);
|
||||
tabs = binding.tabs;
|
||||
@@ -298,6 +314,7 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity {
|
||||
mFrontImageRemoved = savedInstanceState.getInt(STATE_FRONT_IMAGE_REMOVED) == 1;
|
||||
mBackImageRemoved = savedInstanceState.getInt(STATE_BACK_IMAGE_REMOVED) == 1;
|
||||
mIconRemoved = savedInstanceState.getInt(STATE_ICON_REMOVED) == 1;
|
||||
openSetIconMenu = savedInstanceState.getInt(STATE_OPEN_SET_ICON_MENU) == 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -317,6 +334,7 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity {
|
||||
|
||||
for (Currency currency : Currency.getAvailableCurrencies()) {
|
||||
currencies.put(currency.getSymbol(), currency);
|
||||
currencySymbols.put(currency.getCurrencyCode(), currency.getSymbol());
|
||||
}
|
||||
|
||||
tabs = binding.tabs;
|
||||
@@ -325,6 +343,7 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity {
|
||||
storeFieldEdit = binding.storeNameEdit;
|
||||
noteFieldEdit = binding.noteEdit;
|
||||
groupsChips = binding.groupChips;
|
||||
validFromField = binding.validFromField;
|
||||
expiryField = binding.expiryField;
|
||||
balanceField = binding.balanceField;
|
||||
balanceCurrencyField = binding.balanceCurrencyField;
|
||||
@@ -342,18 +361,18 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity {
|
||||
|
||||
enterButton = binding.enterButton;
|
||||
|
||||
barcodeImageGenerationFinishedCallback = () -> {
|
||||
if (!(boolean) barcodeImage.getTag()) {
|
||||
barcodeImageLayout.setVisibility(View.GONE);
|
||||
Toast.makeText(LoyaltyCardEditActivity.this, getString(R.string.wrongValueForBarcodeType), Toast.LENGTH_LONG).show();
|
||||
}
|
||||
};
|
||||
|
||||
storeFieldEdit.addTextChangedListener(new SimpleTextWatcher() {
|
||||
@Override
|
||||
public void onTextChanged(CharSequence s, int start, int before, int count) {
|
||||
updateTempState(LoyaltyCardField.store, s.toString());
|
||||
generateIcon(s.toString());
|
||||
String storeName = s.toString().trim();
|
||||
updateTempState(LoyaltyCardField.store, storeName);
|
||||
generateIcon(storeName);
|
||||
|
||||
if (storeName.length() == 0) {
|
||||
storeFieldEdit.setError(getString(R.string.field_must_not_be_empty));
|
||||
} else {
|
||||
storeFieldEdit.setError(null);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -364,41 +383,31 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity {
|
||||
}
|
||||
});
|
||||
|
||||
expiryField.addTextChangedListener(new SimpleTextWatcher() {
|
||||
CharSequence lastValue;
|
||||
addDateFieldTextChangedListener(validFromField, R.string.anyDate, R.string.chooseValidFromDate, LoyaltyCardField.validFrom);
|
||||
|
||||
@Override
|
||||
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
|
||||
lastValue = s;
|
||||
}
|
||||
addDateFieldTextChangedListener(expiryField, R.string.never, R.string.chooseExpiryDate, LoyaltyCardField.expiry);
|
||||
|
||||
@Override
|
||||
public void onTextChanged(CharSequence s, int start, int before, int count) {
|
||||
if (s.toString().equals(getString(R.string.never))) {
|
||||
expiryField.setTag(null);
|
||||
} else if (s.toString().equals(getString(R.string.chooseExpiryDate))) {
|
||||
if (!lastValue.toString().equals(getString(R.string.chooseExpiryDate))) {
|
||||
expiryField.setText(lastValue);
|
||||
}
|
||||
DialogFragment datePickerFragment = new DatePickerFragment(LoyaltyCardEditActivity.this, expiryField);
|
||||
datePickerFragment.show(getSupportFragmentManager(), "datePicker");
|
||||
}
|
||||
|
||||
updateTempState(LoyaltyCardField.expiry, expiryField.getTag());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterTextChanged(Editable s) {
|
||||
ArrayList<String> expiryList = new ArrayList<>();
|
||||
expiryList.add(0, getString(R.string.never));
|
||||
expiryList.add(1, getString(R.string.chooseExpiryDate));
|
||||
ArrayAdapter<String> expiryAdapter = new ArrayAdapter<>(LoyaltyCardEditActivity.this, android.R.layout.select_dialog_item, expiryList);
|
||||
expiryField.setAdapter(expiryAdapter);
|
||||
DatePickerFragment.registerDatePickListener(this, (textFieldToEdit, newDate) -> {
|
||||
switch (textFieldToEdit) {
|
||||
case validFrom:
|
||||
formatDateField(this, validFromField, newDate);
|
||||
updateTempState(LoyaltyCardField.validFrom, newDate);
|
||||
break;
|
||||
case expiry:
|
||||
formatDateField(this, expiryField, newDate);
|
||||
updateTempState(LoyaltyCardField.expiry, newDate);
|
||||
break;
|
||||
default:
|
||||
throw new AssertionError("Unexpected field: " + textFieldToEdit);
|
||||
}
|
||||
});
|
||||
|
||||
balanceField.setOnFocusChangeListener((v, hasFocus) -> {
|
||||
if (!hasFocus) {
|
||||
if (!hasFocus && !onResuming && !onRestoring) {
|
||||
if (balanceField.getText().toString().isEmpty()) {
|
||||
updateTempState(LoyaltyCardField.balance, BigDecimal.valueOf(0));
|
||||
}
|
||||
|
||||
balanceField.setText(Utils.formatBalanceWithoutCurrencySymbol(tempLoyaltyCard.balance, tempLoyaltyCard.balanceType));
|
||||
}
|
||||
});
|
||||
@@ -406,13 +415,16 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity {
|
||||
balanceField.addTextChangedListener(new SimpleTextWatcher() {
|
||||
@Override
|
||||
public void onTextChanged(CharSequence s, int start, int before, int count) {
|
||||
if (onResuming || onRestoring) return;
|
||||
try {
|
||||
BigDecimal balance = Utils.parseBalance(s.toString(), tempLoyaltyCard.balanceType);
|
||||
updateTempState(LoyaltyCardField.balance, balance);
|
||||
balanceField.setError(null);
|
||||
validBalance = true;
|
||||
} catch (ParseException e) {
|
||||
validBalance = false;
|
||||
e.printStackTrace();
|
||||
balanceField.setError(getString(R.string.balanceParsingFailed));
|
||||
validBalance = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -430,7 +442,7 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity {
|
||||
|
||||
updateTempState(LoyaltyCardField.balanceType, currency);
|
||||
|
||||
if (tempLoyaltyCard.balance != null) {
|
||||
if (tempLoyaltyCard.balance != null && !onResuming && !onRestoring) {
|
||||
balanceField.setText(Utils.formatBalanceWithoutCurrencySymbol(tempLoyaltyCard.balance, currency));
|
||||
}
|
||||
}
|
||||
@@ -488,6 +500,12 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity {
|
||||
@Override
|
||||
public void onTextChanged(CharSequence s, int start, int before, int count) {
|
||||
updateTempState(LoyaltyCardField.cardId, s.toString());
|
||||
|
||||
if (s.length() == 0) {
|
||||
cardIdFieldView.setError(getString(R.string.field_must_not_be_empty));
|
||||
} else {
|
||||
cardIdFieldView.setError(null);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -516,10 +534,20 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity {
|
||||
builder.setTitle(R.string.setBarcodeId);
|
||||
final EditText input = new EditText(LoyaltyCardEditActivity.this);
|
||||
input.setInputType(InputType.TYPE_CLASS_TEXT);
|
||||
|
||||
FrameLayout container = new FrameLayout(LoyaltyCardEditActivity.this);
|
||||
FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
int contentPadding = getResources().getDimensionPixelSize(R.dimen.alert_dialog_content_padding);
|
||||
params.leftMargin = contentPadding;
|
||||
params.topMargin = contentPadding / 2;
|
||||
params.rightMargin = contentPadding;
|
||||
|
||||
input.setLayoutParams(params);
|
||||
container.addView(input);
|
||||
if (tempLoyaltyCard.barcodeId != null) {
|
||||
input.setText(tempLoyaltyCard.barcodeId);
|
||||
}
|
||||
builder.setView(input);
|
||||
builder.setView(container);
|
||||
|
||||
builder.setPositiveButton(getString(R.string.ok), (dialog, which) -> {
|
||||
// If the user manually changes the barcode again make sure we disable the
|
||||
@@ -693,16 +721,16 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity {
|
||||
}
|
||||
mCropperOptions.setAspectRatioOptions(selectedByDefault,
|
||||
new AspectRatio(null, 1, 1),
|
||||
new AspectRatio(getResources().getString(R.string.ucrop_label_original).toUpperCase(), sourceWidth, sourceHeight),
|
||||
new AspectRatio(getResources().getString(com.yalantis.ucrop.R.string.ucrop_label_original).toUpperCase(), sourceWidth, sourceHeight),
|
||||
new AspectRatio(getResources().getString(R.string.card).toUpperCase(), 85.6f, 53.98f)
|
||||
);
|
||||
|
||||
// Fix theming
|
||||
|
||||
int colorPrimary = MaterialColors.getColor(this, R.attr.colorPrimary, ContextCompat.getColor(this, R.color.md_theme_light_primary));
|
||||
int colorOnPrimary = MaterialColors.getColor(this, R.attr.colorOnPrimary, ContextCompat.getColor(this, R.color.md_theme_light_onPrimary));
|
||||
int colorSurface = MaterialColors.getColor(this, R.attr.colorSurface, ContextCompat.getColor(this, R.color.md_theme_light_surface));
|
||||
int colorOnSurface = MaterialColors.getColor(this, R.attr.colorOnSurface, ContextCompat.getColor(this, R.color.md_theme_light_onSurface));
|
||||
int colorPrimary = MaterialColors.getColor(this, androidx.appcompat.R.attr.colorPrimary, ContextCompat.getColor(this, R.color.md_theme_light_primary));
|
||||
int colorOnPrimary = MaterialColors.getColor(this, com.google.android.material.R.attr.colorOnPrimary, ContextCompat.getColor(this, R.color.md_theme_light_onPrimary));
|
||||
int colorSurface = MaterialColors.getColor(this, com.google.android.material.R.attr.colorSurface, ContextCompat.getColor(this, R.color.md_theme_light_surface));
|
||||
int colorOnSurface = MaterialColors.getColor(this, com.google.android.material.R.attr.colorOnSurface, ContextCompat.getColor(this, R.color.md_theme_light_onSurface));
|
||||
int colorBackground = MaterialColors.getColor(this, android.R.attr.colorBackground, ContextCompat.getColor(this, R.color.md_theme_light_onSurface));
|
||||
mCropperOptions.setToolbarColor(colorSurface);
|
||||
mCropperOptions.setStatusBarColor(colorSurface);
|
||||
@@ -776,7 +804,7 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity {
|
||||
}
|
||||
} else {
|
||||
// New card, use default values
|
||||
tempLoyaltyCard = new LoyaltyCard(-1, "", "", null, new BigDecimal("0"), null, "", null, null, null, 0, Utils.getUnixTime(), 100,0);
|
||||
tempLoyaltyCard = new LoyaltyCard(-1, "", "", null, null, new BigDecimal("0"), null, "", null, null, null, 0, Utils.getUnixTime(), 100,0);
|
||||
|
||||
}
|
||||
}
|
||||
@@ -819,12 +847,21 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity {
|
||||
|
||||
storeFieldEdit.setText(tempLoyaltyCard.store);
|
||||
noteFieldEdit.setText(tempLoyaltyCard.note);
|
||||
formatExpiryField(this, expiryField, tempLoyaltyCard.expiry);
|
||||
formatBalanceCurrencyField(tempLoyaltyCard.balanceType);
|
||||
formatDateField(this, validFromField, tempLoyaltyCard.validFrom);
|
||||
formatDateField(this, expiryField, tempLoyaltyCard.expiry);
|
||||
cardIdFieldView.setText(tempLoyaltyCard.cardId);
|
||||
barcodeIdField.setText(tempLoyaltyCard.barcodeId != null ? tempLoyaltyCard.barcodeId : getString(R.string.sameAsCardId));
|
||||
barcodeTypeField.setText(tempLoyaltyCard.barcodeType != null ? tempLoyaltyCard.barcodeType.prettyName() : getString(R.string.noBarcode));
|
||||
|
||||
// We set the balance here (with onResuming/onRestoring == true) to prevent formatBalanceCurrencyField() from setting it (via onTextChanged),
|
||||
// which can cause issues when switching locale because it parses the balance and e.g. the decimal separator may have changed.
|
||||
formatBalanceCurrencyField(tempLoyaltyCard.balanceType);
|
||||
BigDecimal balance = tempLoyaltyCard.balance == null ? new BigDecimal("0") : tempLoyaltyCard.balance;
|
||||
tempLoyaltyCard = updateTempState(tempLoyaltyCard, LoyaltyCardField.balance, balance);
|
||||
balanceField.setText(Utils.formatBalanceWithoutCurrencySymbol(tempLoyaltyCard.balance, tempLoyaltyCard.balanceType));
|
||||
validBalance = true;
|
||||
Log.d(TAG, "Setting balance to " + balance);
|
||||
|
||||
if (groupsChips.getChildCount() == 0) {
|
||||
List<Group> existingGroups = DBHelper.getGroups(mDatabase);
|
||||
|
||||
@@ -865,10 +902,9 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity {
|
||||
}
|
||||
}
|
||||
|
||||
// Generate random header color
|
||||
if (tempLoyaltyCard.headerColor == null) {
|
||||
// Select a random color to start out with.
|
||||
updateTempState(LoyaltyCardField.headerColor, Utils.getRandomHeaderColor(this));
|
||||
// If name is set, pick colour relevant for name. Otherwise pick randomly
|
||||
updateTempState(LoyaltyCardField.headerColor, tempLoyaltyCard.store.isEmpty() ? Utils.getRandomHeaderColor(this) : Utils.getHeaderColor(this, tempLoyaltyCard));
|
||||
}
|
||||
|
||||
// Update from intent
|
||||
@@ -915,7 +951,7 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity {
|
||||
saveButton.setOnClickListener(v -> doSave());
|
||||
saveButton.bringToFront();
|
||||
|
||||
generateIcon(storeFieldEdit.getText().toString());
|
||||
generateIcon(storeFieldEdit.getText().toString().trim());
|
||||
|
||||
// It can't be null because we set it in updateTempState but SpotBugs insists it can be
|
||||
// NP_NULL_ON_SOME_PATH: Possible null pointer dereference and
|
||||
@@ -928,12 +964,20 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity {
|
||||
}
|
||||
|
||||
onResuming = false;
|
||||
onRestoring = false;
|
||||
|
||||
// Fake click on the edit icon to cause the set icon option to pop up if the icon was
|
||||
// long-pressed in the view activity
|
||||
if (openSetIconMenu) {
|
||||
openSetIconMenu = false;
|
||||
thumbnail.callOnClick();
|
||||
}
|
||||
}
|
||||
|
||||
protected void setColorFromIcon() {
|
||||
Object icon = thumbnail.getTag();
|
||||
if (icon != null && (icon instanceof Bitmap)) {
|
||||
int headerColor = Utils.getHeaderColorFromImage((Bitmap) icon, tempLoyaltyCard.headerColor != null ? tempLoyaltyCard.headerColor : R.attr.colorPrimary);
|
||||
int headerColor = Utils.getHeaderColorFromImage((Bitmap) icon, Utils.getHeaderColor(this, tempLoyaltyCard));
|
||||
|
||||
updateTempState(LoyaltyCardField.headerColor, headerColor);
|
||||
|
||||
@@ -954,13 +998,61 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity {
|
||||
}
|
||||
}
|
||||
|
||||
protected static void formatExpiryField(Context context, EditText expiryField, Date expiry) {
|
||||
expiryField.setTag(expiry);
|
||||
protected void addDateFieldTextChangedListener(AutoCompleteTextView dateField, @StringRes int defaultOptionStringId, @StringRes int chooseDateOptionStringId, LoyaltyCardField loyaltyCardField) {
|
||||
dateField.addTextChangedListener(new SimpleTextWatcher() {
|
||||
CharSequence lastValue;
|
||||
|
||||
if (expiry == null) {
|
||||
expiryField.setText(context.getString(R.string.never));
|
||||
@Override
|
||||
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
|
||||
lastValue = s;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTextChanged(CharSequence s, int start, int before, int count) {
|
||||
if (s.toString().equals(getString(defaultOptionStringId))) {
|
||||
dateField.setTag(null);
|
||||
updateTempState(loyaltyCardField, null);
|
||||
} else if (s.toString().equals(getString(chooseDateOptionStringId))) {
|
||||
if (!lastValue.toString().equals(getString(chooseDateOptionStringId))) {
|
||||
dateField.setText(lastValue);
|
||||
}
|
||||
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
|
||||
loyaltyCardField == LoyaltyCardField.validFrom ? (Date) expiryField.getTag() : null);
|
||||
datePickerFragment.show(getSupportFragmentManager(), "datePicker");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterTextChanged(Editable s) {
|
||||
ArrayList<String> dropdownOptions = new ArrayList<>();
|
||||
dropdownOptions.add(0, getString(defaultOptionStringId));
|
||||
dropdownOptions.add(1, getString(chooseDateOptionStringId));
|
||||
ArrayAdapter<String> dropdownOptionsAdapter = new ArrayAdapter<>(LoyaltyCardEditActivity.this, android.R.layout.select_dialog_item, dropdownOptions);
|
||||
dateField.setAdapter(dropdownOptionsAdapter);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected static void formatDateField(Context context, EditText textField, Date date) {
|
||||
textField.setTag(date);
|
||||
|
||||
if (date == null) {
|
||||
String text;
|
||||
if (textField.getId() == R.id.validFromField) {
|
||||
text = context.getString(R.string.anyDate);
|
||||
} else if (textField.getId() == R.id.expiryField) {
|
||||
text = context.getString(R.string.never);
|
||||
} else {
|
||||
throw new IllegalArgumentException("Unknown textField Id " + textField.getId());
|
||||
}
|
||||
textField.setText(text);
|
||||
} else {
|
||||
expiryField.setText(DateFormat.getDateInstance(DateFormat.LONG).format(expiry));
|
||||
textField.setText(DateFormat.getDateInstance(DateFormat.LONG).format(date));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -968,7 +1060,7 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity {
|
||||
if (balanceType == null) {
|
||||
balanceCurrencyField.setText(getString(R.string.points));
|
||||
} else {
|
||||
balanceCurrencyField.setText(balanceType.getSymbol());
|
||||
balanceCurrencyField.setText(getCurrencySymbol(balanceType));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -981,14 +1073,59 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity {
|
||||
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
||||
|
||||
if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||
if (requestCode == PERMISSION_REQUEST_CAMERA_IMAGE_FRONT) {
|
||||
onMockedRequestPermissionsResult(requestCode, permissions, grantResults);
|
||||
}
|
||||
|
||||
public void onMockedRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
||||
boolean granted = grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED;
|
||||
Integer failureReason = null;
|
||||
|
||||
if (requestCode == PERMISSION_REQUEST_CAMERA_IMAGE_FRONT) {
|
||||
if (granted) {
|
||||
takePhotoForCard(Utils.CARD_IMAGE_FROM_CAMERA_FRONT);
|
||||
} else if (requestCode == PERMISSION_REQUEST_CAMERA_IMAGE_BACK) {
|
||||
takePhotoForCard(Utils.CARD_IMAGE_FROM_CAMERA_BACK);
|
||||
} else if (requestCode == PERMISSION_REQUEST_CAMERA_IMAGE_ICON) {
|
||||
takePhotoForCard(Utils.CARD_IMAGE_FROM_CAMERA_ICON);
|
||||
return;
|
||||
}
|
||||
|
||||
failureReason = R.string.cameraPermissionRequired;
|
||||
} else if (requestCode == PERMISSION_REQUEST_CAMERA_IMAGE_BACK) {
|
||||
if (granted) {
|
||||
takePhotoForCard(Utils.CARD_IMAGE_FROM_CAMERA_BACK);
|
||||
return;
|
||||
}
|
||||
|
||||
failureReason = R.string.cameraPermissionRequired;
|
||||
} else if (requestCode == PERMISSION_REQUEST_CAMERA_IMAGE_ICON) {
|
||||
if (granted) {
|
||||
takePhotoForCard(Utils.CARD_IMAGE_FROM_CAMERA_ICON);
|
||||
return;
|
||||
}
|
||||
|
||||
failureReason = R.string.cameraPermissionRequired;
|
||||
} else if (requestCode == PERMISSION_REQUEST_STORAGE_IMAGE_FRONT) {
|
||||
if (granted) {
|
||||
selectImageFromGallery(Utils.CARD_IMAGE_FROM_FILE_FRONT);
|
||||
return;
|
||||
}
|
||||
|
||||
failureReason = R.string.storageReadPermissionRequired;
|
||||
} else if (requestCode == PERMISSION_REQUEST_STORAGE_IMAGE_BACK) {
|
||||
if (granted) {
|
||||
selectImageFromGallery(Utils.CARD_IMAGE_FROM_FILE_BACK);
|
||||
return;
|
||||
}
|
||||
|
||||
failureReason = R.string.storageReadPermissionRequired;
|
||||
} else if (requestCode == PERMISSION_REQUEST_STORAGE_IMAGE_ICON) {
|
||||
if (granted) {
|
||||
selectImageFromGallery(Utils.CARD_IMAGE_FROM_FILE_ICON);
|
||||
return;
|
||||
}
|
||||
|
||||
failureReason = R.string.storageReadPermissionRequired;
|
||||
}
|
||||
|
||||
if (failureReason != null) {
|
||||
Toast.makeText(this, failureReason, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1057,7 +1194,38 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity {
|
||||
Uri photoURI = FileProvider.getUriForFile(LoyaltyCardEditActivity.this, BuildConfig.APPLICATION_ID, Utils.createTempFile(this, TEMP_CAMERA_IMAGE_NAME));
|
||||
mRequestedImage = type;
|
||||
|
||||
mPhotoTakerLauncher.launch(photoURI);
|
||||
try {
|
||||
mPhotoTakerLauncher.launch(photoURI);
|
||||
} catch (ActivityNotFoundException e) {
|
||||
Toast.makeText(getApplicationContext(), R.string.cameraPermissionDeniedTitle, Toast.LENGTH_LONG).show();
|
||||
Log.e(TAG, "No activity found to handle intent", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void selectImageFromGallery(int type) {
|
||||
mRequestedImage = type;
|
||||
|
||||
Intent photoPickerIntent = new Intent(Intent.ACTION_PICK);
|
||||
photoPickerIntent.setType("image/*");
|
||||
Intent contentIntent = new Intent(Intent.ACTION_GET_CONTENT);
|
||||
contentIntent.setType("image/*");
|
||||
Intent chooserIntent = Intent.createChooser(photoPickerIntent, getString(R.string.addFromImage));
|
||||
chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, new Intent[] { contentIntent });
|
||||
|
||||
try {
|
||||
mPhotoPickerLauncher.launch(chooserIntent);
|
||||
} catch (ActivityNotFoundException e) {
|
||||
Toast.makeText(getApplicationContext(), R.string.failedLaunchingPhotoPicker, Toast.LENGTH_LONG).show();
|
||||
Log.e(TAG, "No activity found to handle intent", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBarcodeImageWriterResult(boolean success) {
|
||||
if (!success) {
|
||||
barcodeImageLayout.setVisibility(View.GONE);
|
||||
Toast.makeText(LoyaltyCardEditActivity.this, getString(R.string.wrongValueForBarcodeType), Toast.LENGTH_LONG).show();
|
||||
}
|
||||
}
|
||||
|
||||
class EditCardIdAndBarcode implements View.OnClickListener {
|
||||
@@ -1111,89 +1279,44 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity {
|
||||
}
|
||||
|
||||
ColorPickerDialog dialog = dialogBuilder.create();
|
||||
dialog.setColorPickerDialogListener(new ColorPickerDialogListener() {
|
||||
@Override
|
||||
public void onColorSelected(int dialogId, int color) {
|
||||
updateTempState(LoyaltyCardField.headerColor, color);
|
||||
|
||||
thumbnailEditIcon.setBackgroundColor(Utils.needsDarkForeground(color) ? Color.BLACK : Color.WHITE);
|
||||
thumbnailEditIcon.setColorFilter(Utils.needsDarkForeground(color) ? Color.WHITE : Color.BLACK);
|
||||
|
||||
// Unset image if set
|
||||
thumbnail.setTag(null);
|
||||
|
||||
generateIcon(storeFieldEdit.getText().toString());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDialogDismissed(int dialogId) {
|
||||
// Nothing to do, no change made
|
||||
}
|
||||
});
|
||||
dialog.show(getSupportFragmentManager(), "color-picker-dialog");
|
||||
|
||||
setCardImage(targetView, null, false);
|
||||
mIconRemoved = true;
|
||||
mIconUnsaved = false;
|
||||
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
cardOptions.put(getString(R.string.takePhoto), () -> {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
int permissionRequestType;
|
||||
int permissionRequestType;
|
||||
|
||||
if (v.getId() == R.id.frontImageHolder) {
|
||||
permissionRequestType = PERMISSION_REQUEST_CAMERA_IMAGE_FRONT;
|
||||
} else if (v.getId() == R.id.backImageHolder) {
|
||||
permissionRequestType = PERMISSION_REQUEST_CAMERA_IMAGE_BACK;
|
||||
} else if (v.getId() == R.id.thumbnail) {
|
||||
permissionRequestType = PERMISSION_REQUEST_CAMERA_IMAGE_ICON;
|
||||
} else {
|
||||
throw new IllegalArgumentException("Unknown ID type " + v.getId());
|
||||
}
|
||||
|
||||
requestPermissions(new String[]{Manifest.permission.CAMERA}, permissionRequestType);
|
||||
} else {
|
||||
int cardImageType;
|
||||
|
||||
if (v.getId() == R.id.frontImageHolder) {
|
||||
cardImageType = Utils.CARD_IMAGE_FROM_CAMERA_FRONT;
|
||||
} else if (v.getId() == R.id.backImageHolder) {
|
||||
cardImageType = Utils.CARD_IMAGE_FROM_CAMERA_BACK;
|
||||
} else if (v.getId() == R.id.thumbnail) {
|
||||
cardImageType = Utils.CARD_IMAGE_FROM_CAMERA_ICON;
|
||||
} else {
|
||||
throw new IllegalArgumentException("Unknown ID type " + v.getId());
|
||||
}
|
||||
|
||||
takePhotoForCard(cardImageType);
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
cardOptions.put(getString(R.string.addFromImage), () -> {
|
||||
if (v.getId() == R.id.frontImageHolder) {
|
||||
mRequestedImage = Utils.CARD_IMAGE_FROM_FILE_FRONT;
|
||||
permissionRequestType = PERMISSION_REQUEST_CAMERA_IMAGE_FRONT;
|
||||
} else if (v.getId() == R.id.backImageHolder) {
|
||||
mRequestedImage = Utils.CARD_IMAGE_FROM_FILE_BACK;
|
||||
permissionRequestType = PERMISSION_REQUEST_CAMERA_IMAGE_BACK;
|
||||
} else if (v.getId() == R.id.thumbnail) {
|
||||
mRequestedImage = Utils.CARD_IMAGE_FROM_FILE_ICON;
|
||||
permissionRequestType = PERMISSION_REQUEST_CAMERA_IMAGE_ICON;
|
||||
} else {
|
||||
throw new IllegalArgumentException("Unknown ID type " + v.getId());
|
||||
}
|
||||
|
||||
Intent i = new Intent(Intent.ACTION_PICK);
|
||||
i.setType("image/*");
|
||||
PermissionUtils.requestCameraPermission(LoyaltyCardEditActivity.this, permissionRequestType);
|
||||
|
||||
try {
|
||||
mPhotoPickerLauncher.launch(i);
|
||||
} catch (ActivityNotFoundException e) {
|
||||
Toast.makeText(getApplicationContext(), R.string.failedLaunchingPhotoPicker, Toast.LENGTH_LONG).show();
|
||||
Log.e(TAG, "No activity found to handle intent", e);
|
||||
return null;
|
||||
});
|
||||
|
||||
cardOptions.put(getString(R.string.addFromImage), () -> {
|
||||
int permissionRequestType;
|
||||
|
||||
if (v.getId() == R.id.frontImageHolder) {
|
||||
permissionRequestType = PERMISSION_REQUEST_STORAGE_IMAGE_FRONT;
|
||||
} else if (v.getId() == R.id.backImageHolder) {
|
||||
permissionRequestType = PERMISSION_REQUEST_STORAGE_IMAGE_BACK;
|
||||
} else if (v.getId() == R.id.thumbnail) {
|
||||
permissionRequestType = PERMISSION_REQUEST_STORAGE_IMAGE_ICON;
|
||||
} else {
|
||||
throw new IllegalArgumentException("Unknown ID type " + v.getId());
|
||||
}
|
||||
|
||||
PermissionUtils.requestStorageReadPermission(LoyaltyCardEditActivity.this, permissionRequestType);
|
||||
|
||||
return null;
|
||||
});
|
||||
|
||||
@@ -1233,24 +1356,80 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity {
|
||||
}
|
||||
}
|
||||
|
||||
// ColorPickerDialogListener callback used by the ColorPickerDialog created in ChooseCardImage to set the thumbnail color
|
||||
// We don't need to set or check the dialogId since it's only used for that single dialog
|
||||
@Override
|
||||
public void onColorSelected(int dialogId, int color) {
|
||||
// Unset image if set
|
||||
setCardImage(thumbnail, null, false);
|
||||
mIconRemoved = true;
|
||||
mIconUnsaved = false;
|
||||
|
||||
updateTempState(LoyaltyCardField.headerColor, color);
|
||||
|
||||
thumbnailEditIcon.setBackgroundColor(Utils.needsDarkForeground(color) ? Color.BLACK : Color.WHITE);
|
||||
thumbnailEditIcon.setColorFilter(Utils.needsDarkForeground(color) ? Color.WHITE : Color.BLACK);
|
||||
|
||||
generateIcon(storeFieldEdit.getText().toString().trim());
|
||||
}
|
||||
|
||||
// ColorPickerDialogListener callback
|
||||
@Override
|
||||
public void onDialogDismissed(int dialogId) {
|
||||
// Nothing to do, no change made
|
||||
}
|
||||
|
||||
public static class DatePickerFragment extends DialogFragment
|
||||
implements DatePickerDialog.OnDateSetListener {
|
||||
|
||||
final Context context;
|
||||
final EditText expiryFieldEdit;
|
||||
public interface OnDatePickListener {
|
||||
void onDatePicked(@NonNull LoyaltyCardField textFieldToEdit, @NonNull Date newDate);
|
||||
}
|
||||
|
||||
DatePickerFragment(Context context, EditText expiryFieldEdit) {
|
||||
this.context = context;
|
||||
this.expiryFieldEdit = expiryFieldEdit;
|
||||
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) expiryFieldEdit.getTag();
|
||||
Date date = (Date) args.getSerializable(CURRENT_DATE_ARGUMENT_KEY);
|
||||
if (date != null) {
|
||||
c.setTime(date);
|
||||
}
|
||||
@@ -1261,17 +1440,23 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity {
|
||||
|
||||
// Create a new instance of DatePickerDialog and return it
|
||||
DatePickerDialog datePickerDialog = new DatePickerDialog(getActivity(), this, year, month, day);
|
||||
datePickerDialog.getDatePicker().setMinDate(getMinDateOfDatePicker());
|
||||
datePickerDialog.getDatePicker().setMinDate(minDate != null ? minDate.getTime() : getDefaultMinDateOfDatePicker());
|
||||
datePickerDialog.getDatePicker().setMaxDate(maxDate != null ? maxDate.getTime() : getDefaultMaxDateOfDatePicker());
|
||||
return datePickerDialog;
|
||||
}
|
||||
|
||||
private long getMinDateOfDatePicker()
|
||||
{
|
||||
private long getDefaultMinDateOfDatePicker() {
|
||||
Calendar minDateCalendar = Calendar.getInstance();
|
||||
minDateCalendar.set(1970, 0, 1);
|
||||
return minDateCalendar.getTimeInMillis();
|
||||
}
|
||||
|
||||
private long getDefaultMaxDateOfDatePicker() {
|
||||
Calendar maxDateCalendar = Calendar.getInstance();
|
||||
maxDateCalendar.set(2100, 11, 31);
|
||||
return maxDateCalendar.getTimeInMillis();
|
||||
}
|
||||
|
||||
public void onDateSet(DatePicker view, int year, int month, int day) {
|
||||
Calendar c = new GregorianCalendar();
|
||||
c.set(Calendar.YEAR, year);
|
||||
@@ -1286,7 +1471,10 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity {
|
||||
|
||||
Date date = new Date(unixTime);
|
||||
|
||||
formatExpiryField(context, expiryFieldEdit, 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1301,18 +1489,41 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity {
|
||||
return;
|
||||
}
|
||||
|
||||
boolean hasError = false;
|
||||
|
||||
if (tempLoyaltyCard.store.isEmpty()) {
|
||||
Snackbar.make(storeFieldEdit, R.string.noStoreError, Snackbar.LENGTH_LONG).show();
|
||||
return;
|
||||
storeFieldEdit.setError(getString(R.string.field_must_not_be_empty));
|
||||
|
||||
// Focus element
|
||||
tabs.selectTab(tabs.getTabAt(0));
|
||||
storeFieldEdit.requestFocus();
|
||||
|
||||
hasError = true;
|
||||
}
|
||||
|
||||
if (tempLoyaltyCard.cardId.isEmpty()) {
|
||||
Snackbar.make(cardIdFieldView, R.string.noCardIdError, Snackbar.LENGTH_LONG).show();
|
||||
return;
|
||||
cardIdFieldView.setError(getString(R.string.field_must_not_be_empty));
|
||||
|
||||
// Focus element if first error element
|
||||
if (!hasError) {
|
||||
tabs.selectTab(tabs.getTabAt(0));
|
||||
cardIdFieldView.requestFocus();
|
||||
hasError = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!validBalance) {
|
||||
Snackbar.make(balanceField, getString(R.string.parsingBalanceFailed, balanceField.getText().toString()), Snackbar.LENGTH_LONG).show();
|
||||
balanceField.setError(getString(R.string.balanceParsingFailed));
|
||||
|
||||
// Focus element if first error element
|
||||
if (!hasError) {
|
||||
tabs.selectTab(tabs.getTabAt(1));
|
||||
balanceField.requestFocus();
|
||||
hasError = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (hasError) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1327,9 +1538,9 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity {
|
||||
// This makes the DBHelper set it to the current date
|
||||
// So that new and edited card are always on top when sorting by recently used
|
||||
if (updateLoyaltyCard) {
|
||||
DBHelper.updateLoyaltyCard(mDatabase, loyaltyCardId, tempLoyaltyCard.store, tempLoyaltyCard.note, tempLoyaltyCard.expiry, tempLoyaltyCard.balance, tempLoyaltyCard.balanceType, tempLoyaltyCard.cardId, tempLoyaltyCard.barcodeId, tempLoyaltyCard.barcodeType, tempLoyaltyCard.headerColor, tempLoyaltyCard.starStatus, null, tempLoyaltyCard.archiveStatus);
|
||||
DBHelper.updateLoyaltyCard(mDatabase, loyaltyCardId, tempLoyaltyCard.store, tempLoyaltyCard.note, tempLoyaltyCard.validFrom, tempLoyaltyCard.expiry, tempLoyaltyCard.balance, tempLoyaltyCard.balanceType, tempLoyaltyCard.cardId, tempLoyaltyCard.barcodeId, tempLoyaltyCard.barcodeType, tempLoyaltyCard.headerColor, tempLoyaltyCard.starStatus, null, tempLoyaltyCard.archiveStatus);
|
||||
} else {
|
||||
loyaltyCardId = (int) DBHelper.insertLoyaltyCard(mDatabase, tempLoyaltyCard.store, tempLoyaltyCard.note, tempLoyaltyCard.expiry, tempLoyaltyCard.balance, tempLoyaltyCard.balanceType, tempLoyaltyCard.cardId, tempLoyaltyCard.barcodeId, tempLoyaltyCard.barcodeType, tempLoyaltyCard.headerColor, 0, null, 0);
|
||||
loyaltyCardId = (int) DBHelper.insertLoyaltyCard(mDatabase, tempLoyaltyCard.store, tempLoyaltyCard.note, tempLoyaltyCard.validFrom, tempLoyaltyCard.expiry, tempLoyaltyCard.balance, tempLoyaltyCard.balanceType, tempLoyaltyCard.cardId, tempLoyaltyCard.barcodeId, tempLoyaltyCard.barcodeType, tempLoyaltyCard.headerColor, 0, null, 0);
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -1448,7 +1659,7 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity {
|
||||
String cardIdString = tempLoyaltyCard.barcodeId != null ? tempLoyaltyCard.barcodeId : tempLoyaltyCard.cardId;
|
||||
CatimaBarcode barcodeFormat = tempLoyaltyCard.barcodeType;
|
||||
|
||||
if (cardIdString == null || barcodeFormat == null) {
|
||||
if (cardIdString == null || cardIdString.isEmpty() || barcodeFormat == null) {
|
||||
barcodeImageLayout.setVisibility(View.GONE);
|
||||
return;
|
||||
}
|
||||
@@ -1466,13 +1677,13 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity {
|
||||
barcodeImage.getViewTreeObserver().removeOnGlobalLayoutListener(this);
|
||||
|
||||
Log.d(TAG, "ImageView size now known");
|
||||
BarcodeImageWriterTask barcodeWriter = new BarcodeImageWriterTask(getApplicationContext(), barcodeImage, cardIdString, barcodeFormat, null, false, barcodeImageGenerationFinishedCallback, true);
|
||||
BarcodeImageWriterTask barcodeWriter = new BarcodeImageWriterTask(getApplicationContext(), barcodeImage, cardIdString, barcodeFormat, null, false, LoyaltyCardEditActivity.this, true);
|
||||
mTasks.executeTask(TaskHandler.TYPE.BARCODE, barcodeWriter);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
Log.d(TAG, "ImageView size known known, creating barcode");
|
||||
BarcodeImageWriterTask barcodeWriter = new BarcodeImageWriterTask(getApplicationContext(), barcodeImage, cardIdString, barcodeFormat, null, false, barcodeImageGenerationFinishedCallback, true);
|
||||
BarcodeImageWriterTask barcodeWriter = new BarcodeImageWriterTask(getApplicationContext(), barcodeImage, cardIdString, barcodeFormat, null, false, this, true);
|
||||
mTasks.executeTask(TaskHandler.TYPE.BARCODE, barcodeWriter);
|
||||
}
|
||||
}
|
||||
@@ -1529,11 +1740,16 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity {
|
||||
|
||||
private void currencyPrioritizeLocaleSymbols(ArrayList<String> currencyList, Locale locale) {
|
||||
try {
|
||||
String currencySymbol = Currency.getInstance(locale).getSymbol();
|
||||
String currencySymbol = getCurrencySymbol(Currency.getInstance(locale));
|
||||
currencyList.remove(currencySymbol);
|
||||
currencyList.add(0, currencySymbol);
|
||||
} catch (IllegalArgumentException e) {
|
||||
Log.d(TAG, "Could not get currency data for locale info: " + e);
|
||||
}
|
||||
}
|
||||
|
||||
private String getCurrencySymbol(final Currency currency) {
|
||||
// Workaround for Android bug where the output of Currency.getSymbol() changes.
|
||||
return currencySymbols.get(currency.getCurrencyCode());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ public enum LoyaltyCardField {
|
||||
id,
|
||||
store,
|
||||
note,
|
||||
validFrom,
|
||||
expiry,
|
||||
balance,
|
||||
balanceType,
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
package protect.card_locker;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
public class LoyaltyCardListDisplayOptionsManager {
|
||||
public static class LoyaltyCardDisplayOption {
|
||||
public String name;
|
||||
public boolean value;
|
||||
public Consumer<Boolean> callback;
|
||||
|
||||
LoyaltyCardDisplayOption(String name, boolean value, Consumer<Boolean> callback) {
|
||||
this.name = name;
|
||||
this.value = value;
|
||||
this.callback = callback;
|
||||
}
|
||||
}
|
||||
|
||||
public final Context mContext;
|
||||
|
||||
private final Runnable mRefreshCardsCallback;
|
||||
private final Runnable mSwapCursorCallback;
|
||||
|
||||
protected SharedPreferences mCardDetailsPref;
|
||||
|
||||
private boolean mShowNameBelowThumbnail;
|
||||
private boolean mShowNote;
|
||||
private boolean mShowBalance;
|
||||
private boolean mShowValidity;
|
||||
private boolean mShowArchivedCards;
|
||||
|
||||
public LoyaltyCardListDisplayOptionsManager(Context context, @NonNull Runnable refreshCardsCallback, @Nullable Runnable swapCursorCallback) {
|
||||
mContext = context;
|
||||
mRefreshCardsCallback = refreshCardsCallback;
|
||||
mSwapCursorCallback = swapCursorCallback;
|
||||
|
||||
// Retrieve user details preference
|
||||
mCardDetailsPref = mContext.getSharedPreferences(
|
||||
mContext.getString(R.string.sharedpreference_card_details),
|
||||
Context.MODE_PRIVATE);
|
||||
mShowNameBelowThumbnail = mCardDetailsPref.getBoolean(mContext.getString(R.string.sharedpreference_card_details_show_name_below_thumbnail), false);
|
||||
mShowNote = mCardDetailsPref.getBoolean(mContext.getString(R.string.sharedpreference_card_details_show_note), true);
|
||||
mShowBalance = mCardDetailsPref.getBoolean(mContext.getString(R.string.sharedpreference_card_details_show_balance), true);
|
||||
mShowValidity = mCardDetailsPref.getBoolean(mContext.getString(R.string.sharedpreference_card_details_show_validity), true);
|
||||
mShowArchivedCards = mCardDetailsPref.getBoolean(mContext.getString(R.string.sharedpreference_card_details_show_archived_cards), true);
|
||||
}
|
||||
|
||||
void saveDetailState(int stateId, boolean value) {
|
||||
SharedPreferences.Editor cardDetailsPrefEditor = mCardDetailsPref.edit();
|
||||
cardDetailsPrefEditor.putBoolean(mContext.getString(stateId), value);
|
||||
cardDetailsPrefEditor.apply();
|
||||
}
|
||||
|
||||
public void showNameBelowThumbnail(boolean show) {
|
||||
mShowNameBelowThumbnail = show;
|
||||
mRefreshCardsCallback.run();
|
||||
|
||||
saveDetailState(R.string.sharedpreference_card_details_show_name_below_thumbnail, show);
|
||||
}
|
||||
|
||||
public boolean showingNameBelowThumbnail() {
|
||||
return mShowNameBelowThumbnail;
|
||||
}
|
||||
|
||||
public void showNote(boolean show) {
|
||||
mShowNote = show;
|
||||
mRefreshCardsCallback.run();
|
||||
|
||||
saveDetailState(R.string.sharedpreference_card_details_show_note, show);
|
||||
}
|
||||
|
||||
public boolean showingNote() {
|
||||
return mShowNote;
|
||||
}
|
||||
|
||||
public void showBalance(boolean show) {
|
||||
mShowBalance = show;
|
||||
mRefreshCardsCallback.run();
|
||||
|
||||
saveDetailState(R.string.sharedpreference_card_details_show_balance, show);
|
||||
}
|
||||
|
||||
public boolean showingBalance() {
|
||||
return mShowBalance;
|
||||
}
|
||||
|
||||
public void showValidity(boolean show) {
|
||||
mShowValidity = show;
|
||||
mRefreshCardsCallback.run();
|
||||
|
||||
saveDetailState(R.string.sharedpreference_card_details_show_validity, show);
|
||||
}
|
||||
|
||||
public boolean showingValidity() {
|
||||
return mShowValidity;
|
||||
}
|
||||
|
||||
public void showArchivedCards(boolean show) {
|
||||
if (mSwapCursorCallback == null) {
|
||||
throw new IllegalStateException("No swap cursor callback is available, can not manage archive state");
|
||||
}
|
||||
|
||||
mShowArchivedCards = show;
|
||||
mSwapCursorCallback.run();
|
||||
|
||||
saveDetailState(R.string.sharedpreference_card_details_show_archived_cards, show);
|
||||
}
|
||||
|
||||
public boolean showingArchivedCards() {
|
||||
if (mSwapCursorCallback == null) {
|
||||
throw new IllegalStateException("No swap cursor callback is available, can not manage archive state");
|
||||
}
|
||||
|
||||
return mShowArchivedCards;
|
||||
}
|
||||
|
||||
public void showDisplayOptionsDialog() {
|
||||
List<LoyaltyCardDisplayOption> displayOptions = new ArrayList<>();
|
||||
|
||||
displayOptions.add(new LoyaltyCardDisplayOption(
|
||||
mContext.getString(R.string.show_name_below_image_thumbnail),
|
||||
showingNameBelowThumbnail(),
|
||||
this::showNameBelowThumbnail
|
||||
));
|
||||
displayOptions.add(new LoyaltyCardDisplayOption(
|
||||
mContext.getString(R.string.show_note),
|
||||
showingNote(),
|
||||
this::showNote
|
||||
));
|
||||
displayOptions.add(new LoyaltyCardDisplayOption(
|
||||
mContext.getString(R.string.show_balance),
|
||||
showingBalance(),
|
||||
this::showBalance
|
||||
));
|
||||
displayOptions.add(new LoyaltyCardDisplayOption(
|
||||
mContext.getString(R.string.show_validity),
|
||||
showingValidity(),
|
||||
this::showValidity
|
||||
));
|
||||
|
||||
// Hide "Show archived cards" option unless the callback exists
|
||||
if (mSwapCursorCallback != null) {
|
||||
displayOptions.add(new LoyaltyCardDisplayOption(
|
||||
mContext.getString(R.string.show_archived_cards),
|
||||
showingArchivedCards(),
|
||||
this::showArchivedCards
|
||||
));
|
||||
}
|
||||
|
||||
// We need to convert Boolean[] to boolean[]
|
||||
boolean[] values = new boolean[displayOptions.size()];
|
||||
for (int i = 0; i < values.length; i++) values[i] = displayOptions.get(i).value;
|
||||
|
||||
AlertDialog.Builder builder = new MaterialAlertDialogBuilder(mContext);
|
||||
builder.setTitle(R.string.action_display_options);
|
||||
builder.setMultiChoiceItems(
|
||||
displayOptions.stream().map(x -> x.name).toArray(String[]::new),
|
||||
values,
|
||||
(dialogInterface, i, b) -> displayOptions.get(i).callback.accept(b)
|
||||
);
|
||||
builder.setPositiveButton(R.string.ok, (dialog, i) -> dialog.dismiss());
|
||||
|
||||
AlertDialog dialog = builder.create();
|
||||
dialog.show();
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -15,18 +15,15 @@ import android.os.Bundle;
|
||||
import android.util.DisplayMetrics;
|
||||
import android.util.Log;
|
||||
import android.util.TypedValue;
|
||||
import android.view.GestureDetector;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.widget.CheckBox;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.activity.result.ActivityResultLauncher;
|
||||
import androidx.activity.result.contract.ActivityResultContracts;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.view.ActionMode;
|
||||
import androidx.appcompat.widget.SearchView;
|
||||
@@ -44,15 +41,13 @@ import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import protect.card_locker.databinding.ArchiveActivityBinding;
|
||||
import protect.card_locker.databinding.ContentMainBinding;
|
||||
import protect.card_locker.databinding.MainActivityBinding;
|
||||
import protect.card_locker.databinding.SortingOptionBinding;
|
||||
import protect.card_locker.preferences.SettingsActivity;
|
||||
|
||||
public class MainActivity extends CatimaAppCompatActivity implements LoyaltyCardCursorAdapter.CardAdapterListener, GestureDetector.OnGestureListener {
|
||||
public class MainActivity extends CatimaAppCompatActivity implements LoyaltyCardCursorAdapter.CardAdapterListener {
|
||||
private MainActivityBinding binding;
|
||||
private ArchiveActivityBinding archiveActivityBinding;
|
||||
private ContentMainBinding contentMainBinding;
|
||||
private static final String TAG = "Catima";
|
||||
public static final String RESTART_ACTIVITY_INTENT = "restart_activity_intent";
|
||||
@@ -63,7 +58,6 @@ public class MainActivity extends CatimaAppCompatActivity implements LoyaltyCard
|
||||
private LoyaltyCardCursorAdapter mAdapter;
|
||||
private ActionMode mCurrentActionMode;
|
||||
private SearchView mSearchView;
|
||||
private GestureDetector mGestureDetector;
|
||||
private int mLoyaltyCardCount = 0;
|
||||
protected String mFilter = "";
|
||||
protected Object mGroup = null;
|
||||
@@ -76,8 +70,7 @@ public class MainActivity extends CatimaAppCompatActivity implements LoyaltyCard
|
||||
private View mNoGroupCardsText;
|
||||
private TabLayout groupsTabLayout;
|
||||
|
||||
private boolean mArchiveMode;
|
||||
public static final String BUNDLE_ARCHIVE_MODE = "archiveMode";
|
||||
private Runnable mSwapLoyaltyCardListCursor;
|
||||
|
||||
private ActivityResultLauncher<Intent> mBarcodeScannerLauncher;
|
||||
private ActivityResultLauncher<Intent> mSettingsLauncher;
|
||||
@@ -233,28 +226,25 @@ public class MainActivity extends CatimaAppCompatActivity implements LoyaltyCard
|
||||
extractIntentFields(getIntent());
|
||||
SplashScreen.installSplashScreen(this);
|
||||
super.onCreate(inputSavedInstanceState);
|
||||
if (!mArchiveMode) {
|
||||
binding = MainActivityBinding.inflate(getLayoutInflater());
|
||||
setTitle(R.string.app_name);
|
||||
setContentView(binding.getRoot());
|
||||
setSupportActionBar(binding.toolbar);
|
||||
groupsTabLayout = binding.groups;
|
||||
contentMainBinding = ContentMainBinding.bind(binding.include.getRoot());
|
||||
} else {
|
||||
archiveActivityBinding = ArchiveActivityBinding.inflate(getLayoutInflater());
|
||||
setTitle(R.string.archiveList);
|
||||
setContentView(archiveActivityBinding.getRoot());
|
||||
setSupportActionBar(archiveActivityBinding.toolbar);
|
||||
groupsTabLayout = archiveActivityBinding.groups;
|
||||
contentMainBinding = ContentMainBinding.bind(archiveActivityBinding.include.getRoot());
|
||||
}
|
||||
|
||||
if(mArchiveMode) {
|
||||
enableToolbarBackButton();
|
||||
}
|
||||
binding = MainActivityBinding.inflate(getLayoutInflater());
|
||||
setTitle(R.string.app_name);
|
||||
setContentView(binding.getRoot());
|
||||
setSupportActionBar(binding.toolbar);
|
||||
groupsTabLayout = binding.groups;
|
||||
contentMainBinding = ContentMainBinding.bind(binding.include.getRoot());
|
||||
|
||||
mDatabase = new DBHelper(this).getWritableDatabase();
|
||||
|
||||
mSwapLoyaltyCardListCursor = () -> {
|
||||
Group group = null;
|
||||
if (mGroup != null) {
|
||||
group = (Group) mGroup;
|
||||
}
|
||||
|
||||
mAdapter.swapCursor(DBHelper.getLoyaltyCardCursor(mDatabase, mFilter, group, mOrder, mOrderDirection, mAdapter.showingArchivedCards() ? DBHelper.LoyaltyCardArchiveFilter.All : DBHelper.LoyaltyCardArchiveFilter.Unarchived));
|
||||
};
|
||||
|
||||
groupsTabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
|
||||
@Override
|
||||
public void onTabSelected(TabLayout.Tab tab) {
|
||||
@@ -282,20 +272,12 @@ public class MainActivity extends CatimaAppCompatActivity implements LoyaltyCard
|
||||
}
|
||||
});
|
||||
|
||||
mGestureDetector = new GestureDetector(this, this);
|
||||
|
||||
View.OnTouchListener gestureTouchListener = (v, event) -> mGestureDetector.onTouchEvent(event);
|
||||
|
||||
mHelpSection = contentMainBinding.helpSection;
|
||||
mNoMatchingCardsText = contentMainBinding.noMatchingCardsText;
|
||||
mNoGroupCardsText = contentMainBinding.noGroupCardsText;
|
||||
mCardList = contentMainBinding.list;
|
||||
|
||||
mNoMatchingCardsText.setOnTouchListener(gestureTouchListener);
|
||||
mCardList.setOnTouchListener(gestureTouchListener);
|
||||
mNoGroupCardsText.setOnTouchListener(gestureTouchListener);
|
||||
|
||||
mAdapter = new LoyaltyCardCursorAdapter(this, null, this);
|
||||
mAdapter = new LoyaltyCardCursorAdapter(this, null, this, mSwapLoyaltyCardListCursor);
|
||||
mCardList.setAdapter(mAdapter);
|
||||
registerForContextMenu(mCardList);
|
||||
|
||||
@@ -361,8 +343,6 @@ public class MainActivity extends CatimaAppCompatActivity implements LoyaltyCard
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
|
||||
mAdapter.refreshState();
|
||||
|
||||
if (mCurrentActionMode != null) {
|
||||
mAdapter.clearSelections();
|
||||
mCurrentActionMode.finish();
|
||||
@@ -375,18 +355,28 @@ public class MainActivity extends CatimaAppCompatActivity implements LoyaltyCard
|
||||
// Start of active tab logic
|
||||
updateTabGroups(groupsTabLayout);
|
||||
|
||||
// Restore settings from Shared Preference
|
||||
// Restore selected tab from Shared Preference
|
||||
SharedPreferences activeTabPref = getApplicationContext().getSharedPreferences(
|
||||
getString(R.string.sharedpreference_active_tab),
|
||||
Context.MODE_PRIVATE);
|
||||
selectedTab = activeTabPref.getInt(getString(R.string.sharedpreference_active_tab), 0);
|
||||
|
||||
// Restore sort preferences from Shared Preferences
|
||||
// If one of the sorting prefererences has never been set or is set to an invalid value,
|
||||
// stick to the defaults.
|
||||
SharedPreferences sortPref = getApplicationContext().getSharedPreferences(
|
||||
getString(R.string.sharedpreference_sort),
|
||||
Context.MODE_PRIVATE);
|
||||
try {
|
||||
mOrder = DBHelper.LoyaltyCardOrder.valueOf(sortPref.getString(getString(R.string.sharedpreference_sort_order), null));
|
||||
mOrderDirection = DBHelper.LoyaltyCardOrderDirection.valueOf(sortPref.getString(getString(R.string.sharedpreference_sort_direction), null));
|
||||
} catch (IllegalArgumentException | NullPointerException ignored) {
|
||||
|
||||
String orderString = sortPref.getString(getString(R.string.sharedpreference_sort_order), null);
|
||||
String orderDirectionString = sortPref.getString(getString(R.string.sharedpreference_sort_direction), null);
|
||||
|
||||
if (orderString != null && orderDirectionString != null) {
|
||||
try {
|
||||
mOrder = DBHelper.LoyaltyCardOrder.valueOf(orderString);
|
||||
mOrderDirection = DBHelper.LoyaltyCardOrderDirection.valueOf(orderDirectionString);
|
||||
} catch (IllegalArgumentException ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
mGroup = null;
|
||||
@@ -400,32 +390,30 @@ public class MainActivity extends CatimaAppCompatActivity implements LoyaltyCard
|
||||
groupsTabLayout.selectTab(tab);
|
||||
assert tab != null;
|
||||
mGroup = tab.getTag();
|
||||
} else if (!mArchiveMode) {
|
||||
} else {
|
||||
scaleScreen();
|
||||
}
|
||||
|
||||
updateLoyaltyCardList(true);
|
||||
// End of active tab logic
|
||||
|
||||
if (!mArchiveMode) {
|
||||
FloatingActionButton addButton = binding.fabAdd;
|
||||
FloatingActionButton addButton = binding.fabAdd;
|
||||
|
||||
addButton.setOnClickListener(v -> {
|
||||
Intent intent = new Intent(getApplicationContext(), ScanActivity.class);
|
||||
Bundle bundle = new Bundle();
|
||||
if (selectedTab != 0) {
|
||||
bundle.putString(LoyaltyCardEditActivity.BUNDLE_ADDGROUP, groupsTabLayout.getTabAt(selectedTab).getText().toString());
|
||||
}
|
||||
intent.putExtras(bundle);
|
||||
mBarcodeScannerLauncher.launch(intent);
|
||||
});
|
||||
addButton.bringToFront();
|
||||
}
|
||||
addButton.setOnClickListener(v -> {
|
||||
Intent intent = new Intent(getApplicationContext(), ScanActivity.class);
|
||||
Bundle bundle = new Bundle();
|
||||
if (selectedTab != 0) {
|
||||
bundle.putString(LoyaltyCardEditActivity.BUNDLE_ADDGROUP, groupsTabLayout.getTabAt(selectedTab).getText().toString());
|
||||
}
|
||||
intent.putExtras(bundle);
|
||||
mBarcodeScannerLauncher.launch(intent);
|
||||
});
|
||||
addButton.bringToFront();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
if (!mSearchView.isIconified()) {
|
||||
if (mSearchView != null && !mSearchView.isIconified()) {
|
||||
mSearchView.setIconified(true);
|
||||
return;
|
||||
}
|
||||
@@ -434,7 +422,7 @@ public class MainActivity extends CatimaAppCompatActivity implements LoyaltyCard
|
||||
}
|
||||
|
||||
private void displayCardSetupOptions(Menu menu, boolean shouldShow) {
|
||||
for (int id : new int[]{R.id.action_search, R.id.action_unfold, R.id.action_sort}) {
|
||||
for (int id : new int[]{R.id.action_search, R.id.action_display_options, R.id.action_sort}) {
|
||||
menu.findItem(id).setVisible(shouldShow);
|
||||
}
|
||||
}
|
||||
@@ -444,12 +432,7 @@ public class MainActivity extends CatimaAppCompatActivity implements LoyaltyCard
|
||||
}
|
||||
|
||||
private void updateLoyaltyCardList(boolean updateCount) {
|
||||
Group group = null;
|
||||
if (mGroup != null) {
|
||||
group = (Group) mGroup;
|
||||
}
|
||||
|
||||
mAdapter.swapCursor(DBHelper.getLoyaltyCardCursor(mDatabase, mFilter, group, mOrder, mOrderDirection, mArchiveMode ? DBHelper.LoyaltyCardArchiveFilter.Archived : DBHelper.LoyaltyCardArchiveFilter.Unarchived));
|
||||
mSwapLoyaltyCardListCursor.run();
|
||||
|
||||
if (updateCount) {
|
||||
updateLoyaltyCardCount();
|
||||
@@ -480,12 +463,6 @@ public class MainActivity extends CatimaAppCompatActivity implements LoyaltyCard
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (mArchiveMode) {
|
||||
// If an user deletes the last card in archive mode, we should close the activity
|
||||
// This will move us back to the main view
|
||||
finish();
|
||||
}
|
||||
|
||||
mCardList.setVisibility(View.GONE);
|
||||
mHelpSection.setVisibility(View.VISIBLE);
|
||||
|
||||
@@ -520,44 +497,45 @@ public class MainActivity extends CatimaAppCompatActivity implements LoyaltyCard
|
||||
|
||||
// Check if an image was shared to us
|
||||
if (Intent.ACTION_SEND.equals(receivedAction)) {
|
||||
if (receivedType.startsWith("image/")) {
|
||||
BarcodeValues barcodeValues;
|
||||
try {
|
||||
Bitmap bitmap;
|
||||
try {
|
||||
Uri data = intent.getParcelableExtra(Intent.EXTRA_STREAM);
|
||||
bitmap = Utils.retrieveImageFromUri(this, data);
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "Error getting data from image file");
|
||||
e.printStackTrace();
|
||||
Toast.makeText(this, R.string.errorReadingImage, Toast.LENGTH_LONG).show();
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
barcodeValues = Utils.getBarcodeFromBitmap(bitmap);
|
||||
|
||||
if (barcodeValues.isEmpty()) {
|
||||
Log.i(TAG, "No barcode found in image file");
|
||||
Toast.makeText(this, R.string.noBarcodeFound, Toast.LENGTH_LONG).show();
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
} catch (NullPointerException e) {
|
||||
Toast.makeText(this, R.string.errorReadingImage, Toast.LENGTH_LONG).show();
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
processBarcodeValues(barcodeValues, null);
|
||||
} else {
|
||||
if (!receivedType.startsWith("image/")) {
|
||||
Log.e(TAG, "Wrong mime-type");
|
||||
return;
|
||||
}
|
||||
|
||||
BarcodeValues barcodeValues;
|
||||
Bitmap bitmap;
|
||||
|
||||
Uri data = intent.getParcelableExtra(Intent.EXTRA_STREAM);
|
||||
if (data == null) {
|
||||
Toast.makeText(this, R.string.errorReadingImage, Toast.LENGTH_LONG).show();
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
bitmap = Utils.retrieveImageFromUri(this, data);
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "Error getting data from image file");
|
||||
e.printStackTrace();
|
||||
Toast.makeText(this, R.string.errorReadingImage, Toast.LENGTH_LONG).show();
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
barcodeValues = Utils.getBarcodeFromBitmap(bitmap);
|
||||
|
||||
if (barcodeValues.isEmpty()) {
|
||||
Log.i(TAG, "No barcode found in image file");
|
||||
Toast.makeText(this, R.string.noBarcodeFound, Toast.LENGTH_LONG).show();
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
processBarcodeValues(barcodeValues, null);
|
||||
}
|
||||
}
|
||||
|
||||
private void extractIntentFields(Intent intent) {
|
||||
final Bundle b = intent.getExtras();
|
||||
mArchiveMode = b != null && b.getBoolean(BUNDLE_ARCHIVE_MODE, false);
|
||||
onSharedIntent(intent);
|
||||
}
|
||||
|
||||
@@ -590,13 +568,8 @@ public class MainActivity extends CatimaAppCompatActivity implements LoyaltyCard
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu inputMenu) {
|
||||
if (!mArchiveMode) {
|
||||
getMenuInflater().inflate(R.menu.main_menu, inputMenu);
|
||||
} else {
|
||||
getMenuInflater().inflate(R.menu.archive_menu, inputMenu);
|
||||
}
|
||||
getMenuInflater().inflate(R.menu.main_menu, inputMenu);
|
||||
|
||||
Utils.updateMenuCardDetailsButtonState(inputMenu.findItem(R.id.action_unfold), mAdapter.showingDetails());
|
||||
displayCardSetupOptions(inputMenu, mLoyaltyCardCount > 0);
|
||||
|
||||
SearchManager searchManager = (SearchManager) getSystemService(Context.SEARCH_SERVICE);
|
||||
@@ -630,14 +603,6 @@ public class MainActivity extends CatimaAppCompatActivity implements LoyaltyCard
|
||||
});
|
||||
}
|
||||
|
||||
if (!mArchiveMode) {
|
||||
if (DBHelper.getArchivedCardsCount(mDatabase) == 0) {
|
||||
inputMenu.findItem(R.id.action_archived).setVisible(false);
|
||||
} else {
|
||||
inputMenu.findItem(R.id.action_archived).setVisible(true);
|
||||
}
|
||||
}
|
||||
|
||||
return super.onCreateOptionsMenu(inputMenu);
|
||||
}
|
||||
|
||||
@@ -649,8 +614,8 @@ public class MainActivity extends CatimaAppCompatActivity implements LoyaltyCard
|
||||
onBackPressed();
|
||||
}
|
||||
|
||||
if (id == R.id.action_unfold) {
|
||||
mAdapter.showDetails(!mAdapter.showingDetails());
|
||||
if (id == R.id.action_display_options) {
|
||||
mAdapter.showDisplayOptionsDialog();
|
||||
invalidateOptionsMenu();
|
||||
|
||||
return true;
|
||||
@@ -706,15 +671,6 @@ public class MainActivity extends CatimaAppCompatActivity implements LoyaltyCard
|
||||
return true;
|
||||
}
|
||||
|
||||
if (id == R.id.action_archived) {
|
||||
Intent i = new Intent(getApplicationContext(), MainActivity.class);
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putBoolean("archiveMode", true);
|
||||
i.putExtras(bundle);
|
||||
startActivity(i);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (id == R.id.action_import_export) {
|
||||
Intent i = new Intent(getApplicationContext(), ImportExportActivity.class);
|
||||
startActivity(i);
|
||||
@@ -755,83 +711,6 @@ public class MainActivity extends CatimaAppCompatActivity implements LoyaltyCard
|
||||
updateLoyaltyCardList(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onDown(MotionEvent e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onShowPress(MotionEvent e) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onSingleTapUp(MotionEvent e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLongPress(MotionEvent e) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean dispatchTouchEvent(MotionEvent ev) {
|
||||
mGestureDetector.onTouchEvent(ev);
|
||||
return super.dispatchTouchEvent(ev);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
|
||||
Log.d(TAG, "On fling");
|
||||
|
||||
// Don't swipe if we have too much vertical movement
|
||||
if (Math.abs(velocityY) > (0.75 * Math.abs(velocityX))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (groupsTabLayout.getTabCount() < 2) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Integer currentTab = groupsTabLayout.getSelectedTabPosition();
|
||||
Log.d("onFling", "Current Tab " + currentTab);
|
||||
// Swipe right
|
||||
if (velocityX < -150) {
|
||||
Log.d("onFling", "Right Swipe detected " + velocityX);
|
||||
Integer nextTab = currentTab + 1;
|
||||
|
||||
if (nextTab == groupsTabLayout.getTabCount()) {
|
||||
groupsTabLayout.selectTab(groupsTabLayout.getTabAt(0));
|
||||
} else {
|
||||
groupsTabLayout.selectTab(groupsTabLayout.getTabAt(nextTab));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Swipe left
|
||||
if (velocityX > 150) {
|
||||
Log.d("onFling", "Left Swipe detected " + velocityX);
|
||||
Integer nextTab = currentTab - 1;
|
||||
|
||||
if (nextTab < 0) {
|
||||
groupsTabLayout.selectTab(groupsTabLayout.getTabAt(groupsTabLayout.getTabCount() - 1));
|
||||
} else {
|
||||
groupsTabLayout.selectTab(groupsTabLayout.getTabAt(nextTab));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRowLongClicked(int inputPosition) {
|
||||
enableActionMode(inputPosition);
|
||||
@@ -871,30 +750,31 @@ public class MainActivity extends CatimaAppCompatActivity implements LoyaltyCard
|
||||
|
||||
boolean hasStarred = false;
|
||||
boolean hasUnstarred = false;
|
||||
|
||||
if (!mArchiveMode) {
|
||||
unarchiveItem.setVisible(false);
|
||||
archiveItem.setVisible(true);
|
||||
} else {
|
||||
unarchiveItem.setVisible(true);
|
||||
archiveItem.setVisible(false);
|
||||
}
|
||||
boolean hasArchived = false;
|
||||
boolean hasUnarchived = false;
|
||||
|
||||
for (LoyaltyCard loyaltyCard : mAdapter.getSelectedItems()) {
|
||||
|
||||
if (loyaltyCard.starStatus == 1) {
|
||||
hasStarred = true;
|
||||
} else {
|
||||
hasUnstarred = true;
|
||||
}
|
||||
|
||||
if (hasStarred && hasUnstarred) {
|
||||
hasStarred = true;
|
||||
hasUnstarred = true;
|
||||
if (loyaltyCard.archiveStatus == 1) {
|
||||
hasArchived = true;
|
||||
} else {
|
||||
hasUnarchived = true;
|
||||
}
|
||||
|
||||
// We have all types, no need to keep checking
|
||||
if (hasStarred && hasUnstarred && hasArchived && hasUnarchived) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
unarchiveItem.setVisible(hasArchived);
|
||||
archiveItem.setVisible(hasUnarchived);
|
||||
|
||||
if (count == 1) {
|
||||
starItem.setVisible(!hasStarred);
|
||||
unstarItem.setVisible(!hasUnstarred);
|
||||
|
||||
@@ -21,7 +21,6 @@ import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
@@ -29,7 +28,6 @@ import androidx.recyclerview.widget.RecyclerView;
|
||||
import protect.card_locker.databinding.ActivityManageGroupBinding;
|
||||
|
||||
public class ManageGroupActivity extends CatimaAppCompatActivity implements ManageGroupCursorAdapter.CardAdapterListener {
|
||||
|
||||
private ActivityManageGroupBinding binding;
|
||||
private SQLiteDatabase mDatabase;
|
||||
private ManageGroupCursorAdapter mAdapter;
|
||||
@@ -101,7 +99,7 @@ public class ManageGroupActivity extends CatimaAppCompatActivity implements Mana
|
||||
}
|
||||
mGroupNameText.setText(mGroup._id);
|
||||
setTitle(getString(R.string.editGroup, mGroup._id));
|
||||
mAdapter = new ManageGroupCursorAdapter(this, null, this, mGroup);
|
||||
mAdapter = new ManageGroupCursorAdapter(this, null, this, mGroup, null);
|
||||
mCardList.setAdapter(mAdapter);
|
||||
registerForContextMenu(mCardList);
|
||||
|
||||
@@ -160,7 +158,7 @@ public class ManageGroupActivity extends CatimaAppCompatActivity implements Mana
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu inputMenu) {
|
||||
getMenuInflater().inflate(R.menu.card_details_menu, inputMenu);
|
||||
Utils.updateMenuCardDetailsButtonState(inputMenu.findItem(R.id.action_unfold), mAdapter.showingDetails());
|
||||
|
||||
return super.onCreateOptionsMenu(inputMenu);
|
||||
}
|
||||
|
||||
@@ -168,8 +166,8 @@ public class ManageGroupActivity extends CatimaAppCompatActivity implements Mana
|
||||
public boolean onOptionsItemSelected(MenuItem inputItem) {
|
||||
int id = inputItem.getItemId();
|
||||
|
||||
if (id == R.id.action_unfold) {
|
||||
mAdapter.showDetails(!mAdapter.showingDetails());
|
||||
if (id == R.id.action_display_options) {
|
||||
mAdapter.showDisplayOptionsDialog();
|
||||
invalidateOptionsMenu();
|
||||
|
||||
return true;
|
||||
|
||||
@@ -16,8 +16,8 @@ public class ManageGroupCursorAdapter extends LoyaltyCardCursorAdapter {
|
||||
final private Group mGroup;
|
||||
final private SQLiteDatabase mDatabase;
|
||||
|
||||
public ManageGroupCursorAdapter(Context inputContext, Cursor inputCursor, CardAdapterListener inputListener, Group group) {
|
||||
super(inputContext, inputCursor, inputListener);
|
||||
public ManageGroupCursorAdapter(Context inputContext, Cursor inputCursor, CardAdapterListener inputListener, Group group, Runnable inputSwapCursorCallback) {
|
||||
super(inputContext, inputCursor, inputListener, inputSwapCursorCallback);
|
||||
mGroup = new Group(group._id, group.order);
|
||||
mInGroupOverlay = new HashMap<>();
|
||||
mDatabase = new DBHelper(inputContext).getWritableDatabase();
|
||||
|
||||
@@ -11,16 +11,14 @@ import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.EditText;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
import com.google.android.material.floatingactionbutton.FloatingActionButton;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.recyclerview.widget.DefaultItemAnimator;
|
||||
@@ -112,65 +110,68 @@ public class ManageGroupsActivity extends CatimaAppCompatActivity implements Gro
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
private void setGroupNameError(EditText input) {
|
||||
String string = sanitizeAddGroupNameField(input.getText());
|
||||
|
||||
if (string.length() == 0) {
|
||||
input.setError(getString(R.string.group_name_is_empty));
|
||||
return;
|
||||
}
|
||||
|
||||
if (DBHelper.getGroup(mDatabase, string) != null) {
|
||||
input.setError(getString(R.string.group_name_already_in_use));
|
||||
return;
|
||||
}
|
||||
|
||||
input.setError(null);
|
||||
}
|
||||
|
||||
private String sanitizeAddGroupNameField(CharSequence s) {
|
||||
return s.toString().trim();
|
||||
}
|
||||
|
||||
private void createGroup() {
|
||||
AlertDialog.Builder builder = new MaterialAlertDialogBuilder(this);
|
||||
builder.setTitle(R.string.enter_group_name);
|
||||
final EditText input = new EditText(this);
|
||||
input.setInputType(InputType.TYPE_CLASS_TEXT);
|
||||
input.addTextChangedListener(new SimpleTextWatcher() {
|
||||
public void onTextChanged(CharSequence s, int start, int before, int count) {
|
||||
setGroupNameError(input);
|
||||
}
|
||||
});
|
||||
setGroupNameError(input);
|
||||
|
||||
// Add spacing to EditText
|
||||
FrameLayout container = new FrameLayout(this);
|
||||
FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(
|
||||
// Header
|
||||
builder.setTitle(R.string.enter_group_name);
|
||||
|
||||
// Layout
|
||||
LinearLayout layout = new LinearLayout(this);
|
||||
layout.setOrientation(LinearLayout.VERTICAL);
|
||||
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
);
|
||||
params.leftMargin = 50;
|
||||
params.rightMargin = 50;
|
||||
int contentPadding = getResources().getDimensionPixelSize(R.dimen.alert_dialog_content_padding);
|
||||
params.leftMargin = contentPadding;
|
||||
params.topMargin = contentPadding / 2;
|
||||
params.rightMargin = contentPadding;
|
||||
|
||||
// EditText with spacing
|
||||
final EditText input = new EditText(this);
|
||||
input.setInputType(InputType.TYPE_CLASS_TEXT);
|
||||
input.setLayoutParams(params);
|
||||
container.addView(input);
|
||||
layout.addView(input);
|
||||
|
||||
builder.setView(container);
|
||||
// Set layout
|
||||
builder.setView(layout);
|
||||
|
||||
// Buttons
|
||||
builder.setPositiveButton(getString(R.string.ok), (dialog, which) -> {
|
||||
CharSequence error = input.getError();
|
||||
|
||||
if (error != null) {
|
||||
Toast.makeText(getApplicationContext(), error.toString(), Toast.LENGTH_SHORT).show();
|
||||
return;
|
||||
}
|
||||
|
||||
DBHelper.insertGroup(mDatabase, sanitizeAddGroupNameField(input.getText()));
|
||||
DBHelper.insertGroup(mDatabase, input.getText().toString().trim());
|
||||
updateGroupList();
|
||||
});
|
||||
builder.setNegativeButton(getString(R.string.cancel), (dialog, which) -> dialog.cancel());
|
||||
AlertDialog dialog = builder.create();
|
||||
|
||||
// Now that the dialog exists, we can bind something that affects the OK button
|
||||
input.addTextChangedListener(new SimpleTextWatcher() {
|
||||
public void onTextChanged(CharSequence s, int start, int before, int count) {
|
||||
String groupName = s.toString().trim();
|
||||
|
||||
if (groupName.length() == 0) {
|
||||
input.setError(getString(R.string.group_name_is_empty));
|
||||
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (DBHelper.getGroup(mDatabase, groupName) != null) {
|
||||
input.setError(getString(R.string.group_name_already_in_use));
|
||||
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false);
|
||||
return;
|
||||
}
|
||||
|
||||
input.setError(null);
|
||||
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(true);
|
||||
}
|
||||
});
|
||||
|
||||
dialog.show();
|
||||
|
||||
// Disable button (must be done **after** dialog is shown to prevent crash
|
||||
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false);
|
||||
// Set focus on input field
|
||||
dialog.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE);
|
||||
input.requestFocus();
|
||||
}
|
||||
|
||||
94
app/src/main/java/protect/card_locker/PermissionUtils.java
Normal file
94
app/src/main/java/protect/card_locker/PermissionUtils.java
Normal file
@@ -0,0 +1,94 @@
|
||||
package protect.card_locker;
|
||||
|
||||
import android.Manifest;
|
||||
import android.app.Activity;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.os.Build;
|
||||
|
||||
import androidx.core.app.ActivityCompat;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
public class PermissionUtils {
|
||||
/**
|
||||
* Check if storage read permission is needed.
|
||||
*
|
||||
* This is only necessary on Android 6.0 (Marshmallow) and below. See
|
||||
* https://github.com/CatimaLoyalty/Android/issues/979 for more info.
|
||||
*
|
||||
* @param activity
|
||||
* @return
|
||||
*/
|
||||
private static boolean needsStorageReadPermission(Activity activity) {
|
||||
// Testing showed this permission wasn't needed for anything Catima did past Marshmallow
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return ContextCompat.checkSelfPermission(activity, android.Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if camera permission is needed.
|
||||
*
|
||||
* @param activity
|
||||
* @return
|
||||
*/
|
||||
public static boolean needsCameraPermission(Activity activity) {
|
||||
// Android only introduced the runtime permission system in Marshmallow (Android 6.0)
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return ContextCompat.checkSelfPermission(activity, android.Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED;
|
||||
}
|
||||
|
||||
/**
|
||||
* Call onRequestPermissionsResult after storage read permission was granted.
|
||||
* Mocks a successful grant if a grant is not necessary.
|
||||
*
|
||||
* @param activity
|
||||
* @param requestCode
|
||||
*/
|
||||
public static void requestStorageReadPermission(CatimaAppCompatActivity activity, int requestCode) {
|
||||
String[] permissions = new String[]{ android.Manifest.permission.READ_EXTERNAL_STORAGE };
|
||||
int[] mockedResults = new int[]{ PackageManager.PERMISSION_GRANTED };
|
||||
|
||||
if (needsStorageReadPermission(activity)) {
|
||||
ActivityCompat.requestPermissions(activity, permissions, requestCode);
|
||||
} else {
|
||||
// FIXME: This points to onMockedRequestPermissionResult instead of to
|
||||
// onRequestPermissionResult because onRequestPermissionResult was only introduced in
|
||||
// Android 6.0 (SDK 23) and we and to support Android 5.0 (SDK 21) too.
|
||||
//
|
||||
// When minSdk becomes 23, this should point to onRequestPermissionResult directly and
|
||||
// the activity input variable should be changed from CatimaAppCompatActivity to
|
||||
// Activity.
|
||||
activity.onMockedRequestPermissionsResult(requestCode, permissions, mockedResults);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Call onRequestPermissionsResult after camera permission was granted.
|
||||
* Mocks a successful grant if a grant is not necessary.
|
||||
*
|
||||
* @param activity
|
||||
* @param requestCode
|
||||
*/
|
||||
public static void requestCameraPermission(CatimaAppCompatActivity activity, int requestCode) {
|
||||
String[] permissions = new String[]{ Manifest.permission.CAMERA };
|
||||
int[] mockedResults = new int[]{ PackageManager.PERMISSION_GRANTED };
|
||||
|
||||
if (needsCameraPermission(activity)) {
|
||||
ActivityCompat.requestPermissions(activity, permissions, requestCode);
|
||||
} else {
|
||||
// FIXME: This points to onMockedRequestPermissionResult instead of to
|
||||
// onRequestPermissionResult because onRequestPermissionResult was only introduced in
|
||||
// Android 6.0 (SDK 23) and we and to support Android 5.0 (SDK 21) too.
|
||||
//
|
||||
// When minSdk becomes 23, this should point to onRequestPermissionResult directly and
|
||||
// the activity input variable should be changed from CatimaAppCompatActivity to
|
||||
// Activity.
|
||||
activity.onMockedRequestPermissionsResult(requestCode, permissions, mockedResults);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,8 @@
|
||||
package protect.card_locker;
|
||||
|
||||
import static protect.card_locker.BarcodeSelectorActivity.BARCODE_CONTENTS;
|
||||
import static protect.card_locker.BarcodeSelectorActivity.BARCODE_FORMAT;
|
||||
|
||||
import android.Manifest;
|
||||
import android.app.Activity;
|
||||
import android.content.ActivityNotFoundException;
|
||||
@@ -9,6 +12,7 @@ import android.graphics.Color;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.provider.Settings;
|
||||
import android.text.InputType;
|
||||
import android.util.DisplayMetrics;
|
||||
import android.util.Log;
|
||||
import android.util.TypedValue;
|
||||
@@ -16,16 +20,22 @@ import android.view.KeyEvent;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.EditText;
|
||||
import android.widget.LinearLayout;
|
||||
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.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
import com.google.zxing.ResultPoint;
|
||||
import com.google.zxing.client.android.Intents;
|
||||
import com.journeyapps.barcodescanner.BarcodeCallback;
|
||||
@@ -45,7 +55,6 @@ import protect.card_locker.databinding.ScanActivityBinding;
|
||||
* originally licensed under Apache 2.0
|
||||
*/
|
||||
public class ScanActivity extends CatimaAppCompatActivity {
|
||||
|
||||
private ScanActivityBinding binding;
|
||||
private CustomBarcodeScannerBinding customBarcodeScannerBinding;
|
||||
private static final String TAG = "Catima";
|
||||
@@ -53,6 +62,8 @@ public class ScanActivity extends CatimaAppCompatActivity {
|
||||
private static final int MEDIUM_SCALE_FACTOR_DIP = 460;
|
||||
private static final int COMPAT_SCALE_FACTOR_DIP = 320;
|
||||
|
||||
private static final int PERMISSION_SCAN_ADD_FROM_IMAGE = 100;
|
||||
|
||||
private CaptureManager capture;
|
||||
private DecoratedBarcodeView barcodeScannerView;
|
||||
|
||||
@@ -64,6 +75,9 @@ public class ScanActivity extends CatimaAppCompatActivity {
|
||||
// can't use the pre-made contract because that launches the file manager for image type instead of gallery
|
||||
private ActivityResultLauncher<Intent> photoPickerLauncher;
|
||||
|
||||
static final String STATE_SCANNER_ACTIVE = "scannerActive";
|
||||
private boolean mScannerActive = true;
|
||||
|
||||
private void extractIntentFields(Intent intent) {
|
||||
final Bundle b = intent.getExtras();
|
||||
cardId = b != null ? b.getString(LoyaltyCardEditActivity.BUNDLE_CARDID) : null;
|
||||
@@ -86,8 +100,36 @@ public class ScanActivity extends CatimaAppCompatActivity {
|
||||
|
||||
manualAddLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> handleActivityResult(Utils.SELECT_BARCODE_REQUEST, result.getResultCode(), result.getData()));
|
||||
photoPickerLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> handleActivityResult(Utils.BARCODE_IMPORT_FROM_IMAGE_FILE, result.getResultCode(), result.getData()));
|
||||
customBarcodeScannerBinding.addFromImage.setOnClickListener(this::addFromImage);
|
||||
customBarcodeScannerBinding.addManually.setOnClickListener(this::addManually);
|
||||
customBarcodeScannerBinding.fabOtherOptions.setOnClickListener(view -> {
|
||||
setScannerActive(false);
|
||||
|
||||
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(ScanActivity.this);
|
||||
builder.setTitle(getString(R.string.add_a_card_in_a_different_way));
|
||||
builder.setItems(
|
||||
new CharSequence[]{
|
||||
getString(R.string.addWithoutBarcode),
|
||||
getString(R.string.addManually),
|
||||
getString(R.string.addFromImage)
|
||||
},
|
||||
(dialogInterface, i) -> {
|
||||
switch (i) {
|
||||
case 0:
|
||||
addWithoutBarcode();
|
||||
break;
|
||||
case 1:
|
||||
addManually();
|
||||
break;
|
||||
case 2:
|
||||
addFromImage();
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("Unknown 'Add a card in a different way' dialog option");
|
||||
}
|
||||
}
|
||||
);
|
||||
builder.setOnCancelListener(dialogInterface -> setScannerActive(true));
|
||||
builder.show();
|
||||
});
|
||||
|
||||
barcodeScannerView = binding.zxingBarcodeScanner;
|
||||
|
||||
@@ -105,8 +147,8 @@ public class ScanActivity extends CatimaAppCompatActivity {
|
||||
public void barcodeResult(BarcodeResult result) {
|
||||
Intent scanResult = new Intent();
|
||||
Bundle scanResultBundle = new Bundle();
|
||||
scanResultBundle.putString(BarcodeSelectorActivity.BARCODE_CONTENTS, result.getText());
|
||||
scanResultBundle.putString(BarcodeSelectorActivity.BARCODE_FORMAT, result.getBarcodeFormat().name());
|
||||
scanResultBundle.putString(BARCODE_CONTENTS, result.getText());
|
||||
scanResultBundle.putString(BARCODE_FORMAT, result.getBarcodeFormat().name());
|
||||
if (addGroup != null) {
|
||||
scanResultBundle.putString(LoyaltyCardEditActivity.BUNDLE_ADDGROUP, addGroup);
|
||||
}
|
||||
@@ -125,7 +167,11 @@ public class ScanActivity extends CatimaAppCompatActivity {
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
capture.onResume();
|
||||
|
||||
if (mScannerActive) {
|
||||
capture.onResume();
|
||||
}
|
||||
|
||||
if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) {
|
||||
showCameraPermissionMissingText(false);
|
||||
}
|
||||
@@ -145,9 +191,18 @@ public class ScanActivity extends CatimaAppCompatActivity {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSaveInstanceState(Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
capture.onSaveInstanceState(outState);
|
||||
protected void onSaveInstanceState(Bundle savedInstanceState) {
|
||||
super.onSaveInstanceState(savedInstanceState);
|
||||
capture.onSaveInstanceState(savedInstanceState);
|
||||
|
||||
savedInstanceState.putBoolean(STATE_SCANNER_ACTIVE, mScannerActive);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRestoreInstanceState(@NonNull Bundle savedInstanceState) {
|
||||
super.onRestoreInstanceState(savedInstanceState);
|
||||
|
||||
mScannerActive = savedInstanceState.getBoolean(STATE_SCANNER_ACTIVE);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -189,33 +244,106 @@ public class ScanActivity extends CatimaAppCompatActivity {
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
private void setScannerActive(boolean isActive) {
|
||||
if (isActive) {
|
||||
barcodeScannerView.resume();
|
||||
} else {
|
||||
barcodeScannerView.pause();
|
||||
}
|
||||
mScannerActive = isActive;
|
||||
}
|
||||
|
||||
private void returnResult(String barcodeContents, String barcodeFormat) {
|
||||
Intent manualResult = new Intent();
|
||||
Bundle manualResultBundle = new Bundle();
|
||||
manualResultBundle.putString(BARCODE_CONTENTS, barcodeContents);
|
||||
manualResultBundle.putString(BARCODE_FORMAT, barcodeFormat);
|
||||
if (addGroup != null) {
|
||||
manualResultBundle.putString(LoyaltyCardEditActivity.BUNDLE_ADDGROUP, addGroup);
|
||||
}
|
||||
manualResult.putExtras(manualResultBundle);
|
||||
ScanActivity.this.setResult(RESULT_OK, manualResult);
|
||||
finish();
|
||||
}
|
||||
|
||||
private void handleActivityResult(int requestCode, int resultCode, Intent intent) {
|
||||
super.onActivityResult(requestCode, resultCode, intent);
|
||||
|
||||
BarcodeValues barcodeValues;
|
||||
BarcodeValues barcodeValues = Utils.parseSetBarcodeActivityResult(requestCode, resultCode, intent, this);
|
||||
|
||||
try {
|
||||
barcodeValues = Utils.parseSetBarcodeActivityResult(requestCode, resultCode, intent, this);
|
||||
} catch (NullPointerException e) {
|
||||
Toast.makeText(this, R.string.errorReadingImage, Toast.LENGTH_LONG).show();
|
||||
if (barcodeValues.isEmpty()) {
|
||||
setScannerActive(true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!barcodeValues.isEmpty()) {
|
||||
Intent manualResult = new Intent();
|
||||
Bundle manualResultBundle = new Bundle();
|
||||
manualResultBundle.putString(BarcodeSelectorActivity.BARCODE_CONTENTS, barcodeValues.content());
|
||||
manualResultBundle.putString(BarcodeSelectorActivity.BARCODE_FORMAT, barcodeValues.format());
|
||||
if (addGroup != null) {
|
||||
manualResultBundle.putString(LoyaltyCardEditActivity.BUNDLE_ADDGROUP, addGroup);
|
||||
}
|
||||
manualResult.putExtras(manualResultBundle);
|
||||
ScanActivity.this.setResult(RESULT_OK, manualResult);
|
||||
finish();
|
||||
}
|
||||
returnResult(barcodeValues.content(), barcodeValues.format());
|
||||
}
|
||||
|
||||
public void addManually(View view) {
|
||||
private void addWithoutBarcode() {
|
||||
AlertDialog.Builder builder = new MaterialAlertDialogBuilder(this);
|
||||
|
||||
builder.setOnCancelListener(dialogInterface -> setScannerActive(true));
|
||||
|
||||
// Header
|
||||
builder.setTitle(R.string.addWithoutBarcode);
|
||||
|
||||
// Layout
|
||||
LinearLayout layout = new LinearLayout(this);
|
||||
layout.setOrientation(LinearLayout.VERTICAL);
|
||||
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
);
|
||||
int contentPadding = getResources().getDimensionPixelSize(R.dimen.alert_dialog_content_padding);
|
||||
params.leftMargin = contentPadding;
|
||||
params.topMargin = contentPadding / 2;
|
||||
params.rightMargin = contentPadding;
|
||||
|
||||
// Description
|
||||
TextView currentTextview = new TextView(this);
|
||||
currentTextview.setText(getString(R.string.enter_card_id));
|
||||
currentTextview.setLayoutParams(params);
|
||||
layout.addView(currentTextview);
|
||||
|
||||
// EditText with spacing
|
||||
final EditText input = new EditText(this);
|
||||
input.setInputType(InputType.TYPE_CLASS_TEXT);
|
||||
input.setLayoutParams(params);
|
||||
layout.addView(input);
|
||||
|
||||
// Set layout
|
||||
builder.setView(layout);
|
||||
|
||||
// Buttons
|
||||
builder.setPositiveButton(getString(R.string.ok), (dialog, which) -> {
|
||||
returnResult(input.getText().toString(), "");
|
||||
});
|
||||
builder.setNegativeButton(getString(R.string.cancel), (dialog, which) -> dialog.cancel());
|
||||
AlertDialog dialog = builder.create();
|
||||
|
||||
// Now that the dialog exists, we can bind something that affects the OK button
|
||||
input.addTextChangedListener(new SimpleTextWatcher() {
|
||||
public void onTextChanged(CharSequence s, int start, int before, int count) {
|
||||
if (s.length() == 0) {
|
||||
input.setError(getString(R.string.card_id_must_not_be_empty));
|
||||
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false);
|
||||
} else {
|
||||
input.setError(null);
|
||||
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(true);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
dialog.show();
|
||||
|
||||
// Disable button (must be done **after** dialog is shown to prevent crash
|
||||
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false);
|
||||
// Set focus on input field
|
||||
dialog.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE);
|
||||
input.requestFocus();
|
||||
}
|
||||
|
||||
public void addManually() {
|
||||
Intent i = new Intent(getApplicationContext(), BarcodeSelectorActivity.class);
|
||||
if (cardId != null) {
|
||||
final Bundle b = new Bundle();
|
||||
@@ -225,12 +353,22 @@ public class ScanActivity extends CatimaAppCompatActivity {
|
||||
manualAddLauncher.launch(i);
|
||||
}
|
||||
|
||||
public void addFromImage(View view) {
|
||||
public void addFromImage() {
|
||||
PermissionUtils.requestStorageReadPermission(this, PERMISSION_SCAN_ADD_FROM_IMAGE);
|
||||
}
|
||||
|
||||
private void addFromImageAfterPermission() {
|
||||
Intent photoPickerIntent = new Intent(Intent.ACTION_PICK);
|
||||
photoPickerIntent.setType("image/*");
|
||||
Intent contentIntent = new Intent(Intent.ACTION_GET_CONTENT);
|
||||
contentIntent.setType("image/*");
|
||||
|
||||
Intent chooserIntent = Intent.createChooser(photoPickerIntent, getString(R.string.addFromImage));
|
||||
chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, new Intent[] { contentIntent });
|
||||
try {
|
||||
photoPickerLauncher.launch(photoPickerIntent);
|
||||
photoPickerLauncher.launch(chooserIntent);
|
||||
} catch (ActivityNotFoundException e) {
|
||||
setScannerActive(true);
|
||||
Toast.makeText(getApplicationContext(), R.string.failedLaunchingPhotoPicker, Toast.LENGTH_LONG).show();
|
||||
Log.e(TAG, "No activity found to handle intent", e);
|
||||
}
|
||||
@@ -240,7 +378,7 @@ public class ScanActivity extends CatimaAppCompatActivity {
|
||||
customBarcodeScannerBinding.cameraPermissionDeniedLayout.cameraPermissionDeniedClickableArea.setOnClickListener(show ? v -> {
|
||||
navigateToSystemPermissionSetting();
|
||||
} : null);
|
||||
customBarcodeScannerBinding.cardInputContainer.setBackgroundColor(show ? obtainThemeAttribute(R.attr.colorSurface) : Color.TRANSPARENT);
|
||||
customBarcodeScannerBinding.cardInputContainer.setBackgroundColor(show ? obtainThemeAttribute(com.google.android.material.R.attr.colorSurface) : Color.TRANSPARENT);
|
||||
customBarcodeScannerBinding.cameraPermissionDeniedLayout.getRoot().setVisibility(show ? View.VISIBLE : View.GONE);
|
||||
}
|
||||
|
||||
@@ -270,9 +408,22 @@ public class ScanActivity extends CatimaAppCompatActivity {
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
||||
if (requestCode == CaptureManager.getCameraPermissionReqCode())
|
||||
showCameraPermissionMissingText(grantResults[0] != PackageManager.PERMISSION_GRANTED);
|
||||
|
||||
onMockedRequestPermissionsResult(requestCode, permissions, grantResults);
|
||||
}
|
||||
|
||||
public void onMockedRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
||||
boolean granted = grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED;
|
||||
|
||||
if (requestCode == CaptureManager.getCameraPermissionReqCode()) {
|
||||
showCameraPermissionMissingText(!granted);
|
||||
} else if (requestCode == PERMISSION_SCAN_ADD_FROM_IMAGE) {
|
||||
if (granted) {
|
||||
addFromImageAfterPermission();
|
||||
} else {
|
||||
setScannerActive(true);
|
||||
Toast.makeText(this, R.string.storageReadPermissionRequired, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,7 +33,6 @@ class ShortcutHelper {
|
||||
private static final int ADAPTIVE_BITMAP_SIZE = 108 * ADAPTIVE_BITMAP_SCALE;
|
||||
private static final int ADAPTIVE_BITMAP_VISIBLE_SIZE = 72 * ADAPTIVE_BITMAP_SCALE;
|
||||
private static final int ADAPTIVE_BITMAP_IMAGE_SIZE = ADAPTIVE_BITMAP_VISIBLE_SIZE + 5 * ADAPTIVE_BITMAP_SCALE;
|
||||
private static final int PADDING_COLOR = Color.argb(255, 255, 255, 255);
|
||||
private static final int PADDING_COLOR_OVERLAY = Color.argb(127, 0, 0, 0);
|
||||
|
||||
/**
|
||||
@@ -71,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++) {
|
||||
@@ -91,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);
|
||||
@@ -145,7 +147,7 @@ class ShortcutHelper {
|
||||
if (iconBitmap == null) {
|
||||
iconBitmap = Utils.generateIcon(context, loyaltyCard, true).getLetterTile();
|
||||
} else {
|
||||
iconBitmap = createAdaptiveBitmap(iconBitmap, loyaltyCard.headerColor == null ? PADDING_COLOR : loyaltyCard.headerColor);
|
||||
iconBitmap = createAdaptiveBitmap(iconBitmap, Utils.getHeaderColor(context, loyaltyCard));
|
||||
}
|
||||
|
||||
IconCompat icon = IconCompat.createWithAdaptiveBitmap(iconBitmap);
|
||||
|
||||
@@ -53,8 +53,8 @@ public class UCropWrapper extends UCropActivity {
|
||||
AppCompatImageView controlsBackgroundImage = (AppCompatImageView) check;
|
||||
// everything gathered and are as expected, now perform color patching
|
||||
Utils.patchColors(this);
|
||||
int colorSurface = MaterialColors.getColor(this, R.attr.colorSurface, ContextCompat.getColor(this, R.color.md_theme_light_surface));
|
||||
int colorOnSurface = MaterialColors.getColor(this, R.attr.colorOnSurface, ContextCompat.getColor(this, R.color.md_theme_light_onSurface));
|
||||
int colorSurface = MaterialColors.getColor(this, com.google.android.material.R.attr.colorSurface, ContextCompat.getColor(this, R.color.md_theme_light_surface));
|
||||
int colorOnSurface = MaterialColors.getColor(this, com.google.android.material.R.attr.colorOnSurface, ContextCompat.getColor(this, R.color.md_theme_light_onSurface));
|
||||
|
||||
Drawable controlsBackgroundImageDrawable = controlsBackgroundImage.getBackground();
|
||||
controlsBackgroundImageDrawable.mutate();
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package protect.card_locker;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
@@ -13,17 +14,24 @@ import android.graphics.ImageDecoder;
|
||||
import android.graphics.Matrix;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.LocaleList;
|
||||
import android.provider.MediaStore;
|
||||
import android.text.Layout;
|
||||
import android.text.Spanned;
|
||||
import android.text.style.ClickableSpan;
|
||||
import android.util.Log;
|
||||
import android.util.TypedValue;
|
||||
import android.view.MenuItem;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.RawRes;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.app.AppCompatDelegate;
|
||||
import androidx.core.graphics.ColorUtils;
|
||||
import androidx.core.os.LocaleListCompat;
|
||||
import androidx.exifinterface.media.ExifInterface;
|
||||
import androidx.palette.graphics.Palette;
|
||||
|
||||
@@ -45,17 +53,22 @@ import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.StringWriter;
|
||||
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.ArrayList;
|
||||
import java.util.Calendar;
|
||||
import java.util.Currency;
|
||||
import java.util.Date;
|
||||
import java.util.GregorianCalendar;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import protect.card_locker.preferences.Settings;
|
||||
|
||||
@@ -74,6 +87,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;
|
||||
@@ -113,6 +128,17 @@ public class Utils {
|
||||
return ColorUtils.calculateLuminance(backgroundColor) > LUMINANCE_MIDPOINT;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Barcode format and content based on the result of an activity.
|
||||
* It shows toasts to notify the end-user as needed itself and will return an empty
|
||||
* BarcodeValues object if the activity was cancelled or nothing could be found.
|
||||
*
|
||||
* @param requestCode
|
||||
* @param resultCode
|
||||
* @param intent
|
||||
* @param context
|
||||
* @return BarcodeValues
|
||||
*/
|
||||
static public BarcodeValues parseSetBarcodeActivityResult(int requestCode, int resultCode, Intent intent, Context context) {
|
||||
String contents;
|
||||
String format;
|
||||
@@ -124,9 +150,15 @@ public class Utils {
|
||||
if (requestCode == Utils.BARCODE_IMPORT_FROM_IMAGE_FILE) {
|
||||
Log.i(TAG, "Received image file with possible barcode");
|
||||
|
||||
Uri data = intent.getData();
|
||||
if (data == null) {
|
||||
Log.e(TAG, "Intent did not contain any data");
|
||||
Toast.makeText(context, R.string.errorReadingImage, Toast.LENGTH_LONG).show();
|
||||
return new BarcodeValues(null, null);
|
||||
}
|
||||
|
||||
Bitmap bitmap;
|
||||
try {
|
||||
Uri data = intent.getData();
|
||||
bitmap = retrieveImageFromUri(context, data);
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "Error getting data from image file");
|
||||
@@ -214,7 +246,21 @@ public class Utils {
|
||||
}
|
||||
}
|
||||
|
||||
static public Boolean isNotYetValid(Date validFromDate) {
|
||||
// The note in `hasExpired` does not apply here, since the bug was fixed before this feature was added.
|
||||
return validFromDate.after(getStartOfToday().getTime());
|
||||
}
|
||||
|
||||
static public Boolean hasExpired(Date expiryDate) {
|
||||
// Note: In #1083 it was discovered that `DatePickerFragment` may sometimes store the expiryDate
|
||||
// at 12:00 PM instead of 12:00 AM in the DB. While this has been fixed and the 12-hour difference
|
||||
// is not a problem for the way the comparison currently works, it's good to keep in mind such
|
||||
// dates may exist in the DB in case the comparison changes in the future and the new one relies
|
||||
// on both dates being set at 12:00 AM.
|
||||
return expiryDate.before(getStartOfToday().getTime());
|
||||
}
|
||||
|
||||
static private Calendar getStartOfToday() {
|
||||
// today
|
||||
Calendar date = new GregorianCalendar();
|
||||
// reset hour, minutes, seconds and millis
|
||||
@@ -222,13 +268,7 @@ public class Utils {
|
||||
date.set(Calendar.MINUTE, 0);
|
||||
date.set(Calendar.SECOND, 0);
|
||||
date.set(Calendar.MILLISECOND, 0);
|
||||
|
||||
// Note: In #1083 it was discovered that `DatePickerFragment` may sometimes store the expiryDate
|
||||
// at 12:00 PM instead of 12:00 AM in the DB. While this has been fixed and the 12-hour difference
|
||||
// is not a problem for the way the comparison currently works, it's good to keep in mind such
|
||||
// dates may exist in the DB in case the comparison changes in the future and the new one relies
|
||||
// on both dates being set at 12:00 AM.
|
||||
return expiryDate.before(date.getTime());
|
||||
return date;
|
||||
}
|
||||
|
||||
static public String formatBalance(Context context, BigDecimal value, Currency currency) {
|
||||
@@ -353,6 +393,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);
|
||||
@@ -416,17 +481,30 @@ public class Utils {
|
||||
|
||||
Locale chosenLocale = settings.getLocale();
|
||||
|
||||
Resources res = context.getResources();
|
||||
Configuration configuration = res.getConfiguration();
|
||||
// New API is broken on Android 6 and lower when selecting locales with both language and country, so still keeping this
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
|
||||
Resources res = context.getResources();
|
||||
Configuration configuration = res.getConfiguration();
|
||||
setLocalesSdkLessThan24(chosenLocale, configuration, res);
|
||||
return context;
|
||||
}
|
||||
|
||||
LocaleList localeList = chosenLocale != null ? new LocaleList(chosenLocale) : LocaleList.getDefault();
|
||||
LocaleList.setDefault(localeList);
|
||||
configuration.setLocales(localeList);
|
||||
return context.createConfigurationContext(configuration);
|
||||
/* Documentation at https://developer.android.com/reference/androidx/appcompat/app/AppCompatDelegate#setApplicationLocales(androidx.core.os.LocaleListCompat)
|
||||
For API levels below that, the developer has two options:
|
||||
- They can opt-in to automatic storage handled through the library...
|
||||
- The second option is that they can choose to handle storage themselves.
|
||||
In order to do so they must use this API to initialize locales during app-start up and provide their stored locales.
|
||||
In this case, API should be called before Activity.onCreate() in the activity lifecycle, e.g. in attachBaseContext().
|
||||
Note: Developers should gate this to API versions <33.
|
||||
|
||||
We are handling storage ourselves (courtesy of the in-app language picker), so we take the second approach.
|
||||
So according to docs, we should have the API < 33 check.
|
||||
*/
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
|
||||
AppCompatDelegate.setApplicationLocales(chosenLocale != null ? LocaleListCompat.create(chosenLocale) : LocaleListCompat.getEmptyLocaleList());
|
||||
}
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
@@ -435,6 +513,55 @@ public class Utils {
|
||||
res.updateConfiguration(configuration, res.getDisplayMetrics());
|
||||
}
|
||||
|
||||
/**
|
||||
* Android 13 settings seems to "force" the user to select country of locale, but many app-supported locales either only have language, not country
|
||||
* or have a country the user doesn't want, which creates a mismatch between the app's supported locales and the system locale.
|
||||
* <br>
|
||||
* Example: The user chooses Espanol (Espana) in system settings, but the app only supports Espanol (Argentina) and the "plain" Espanol.
|
||||
* <br>
|
||||
* This method returns the app-supported locale that is most similar to the system one.
|
||||
* @param appLocales Locales supported by the app
|
||||
* @param sysLocale Per-app locale in system settings
|
||||
* @return The app-supported locale that best matches the system per-app locale
|
||||
*/
|
||||
@NonNull
|
||||
public static Locale getBestMatchLocale(@NonNull List<Locale> appLocales, @NonNull Locale sysLocale) {
|
||||
int highestMatchMagnitude = appLocales.stream()
|
||||
.mapToInt(appLocale -> calculateMatchMagnitudeOfTwoLocales(appLocale, sysLocale))
|
||||
.max()
|
||||
.orElseThrow(() -> new IllegalArgumentException("appLocales is empty"));
|
||||
for (int i = 0; i < appLocales.size(); i++) {
|
||||
Locale appLocale = appLocales.get(i);
|
||||
if (calculateMatchMagnitudeOfTwoLocales(appLocale, sysLocale) == highestMatchMagnitude) {
|
||||
return appLocale;
|
||||
}
|
||||
}
|
||||
throw new AssertionError("This is not possible; there must be a locale whose match magnitude == " + highestMatchMagnitude + " with " + sysLocale.toLanguageTag());
|
||||
}
|
||||
|
||||
private static int calculateMatchMagnitudeOfTwoLocales(@NonNull Locale appLocale, @NonNull Locale sysLocale) {
|
||||
List<String> appLocaleAdjusted = new ArrayList<>();
|
||||
List<String> sysLocaleAdjusted = new ArrayList<>();
|
||||
appLocaleAdjusted.add(appLocale.getLanguage());
|
||||
sysLocaleAdjusted.add(sysLocale.getLanguage());
|
||||
if (!appLocale.getCountry().isEmpty() && !sysLocale.getCountry().isEmpty()) {
|
||||
appLocaleAdjusted.add(appLocale.getCountry());
|
||||
sysLocaleAdjusted.add(sysLocale.getCountry());
|
||||
}
|
||||
if (!appLocale.getVariant().isEmpty() && !sysLocale.getVariant().isEmpty()) {
|
||||
appLocaleAdjusted.add(appLocale.getVariant());
|
||||
sysLocaleAdjusted.add(sysLocale.getVariant());
|
||||
}
|
||||
if (!appLocale.getScript().isEmpty() && !sysLocale.getScript().isEmpty()) {
|
||||
appLocaleAdjusted.add(appLocale.getScript());
|
||||
sysLocaleAdjusted.add(sysLocale.getScript());
|
||||
}
|
||||
if (appLocaleAdjusted.equals(sysLocaleAdjusted)) {
|
||||
return appLocaleAdjusted.size();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static public long getUnixTime() {
|
||||
return System.currentTimeMillis() / 1000;
|
||||
}
|
||||
@@ -454,6 +581,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)) {
|
||||
@@ -533,22 +672,12 @@ public class Utils {
|
||||
activity.findViewById(android.R.id.content).setBackgroundColor(typedValue.data);
|
||||
}
|
||||
|
||||
public static void updateMenuCardDetailsButtonState(MenuItem item, boolean currentlyExpanded) {
|
||||
if (currentlyExpanded) {
|
||||
item.setIcon(R.drawable.ic_baseline_unfold_less_24);
|
||||
item.setTitle(R.string.action_hide_details);
|
||||
} else {
|
||||
item.setIcon(R.drawable.ic_baseline_unfold_more_24);
|
||||
item.setTitle(R.string.action_show_details);
|
||||
}
|
||||
}
|
||||
|
||||
public static int getHeaderColorFromImage(Bitmap image, int fallback) {
|
||||
if (image == null) {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
return new Palette.Builder(image).generate().getDominantColor(R.attr.colorPrimary);
|
||||
return new Palette.Builder(image).generate().getDominantColor(androidx.appcompat.R.attr.colorPrimary);
|
||||
}
|
||||
|
||||
public static int getRandomHeaderColor(Context context) {
|
||||
@@ -564,7 +693,7 @@ public class Utils {
|
||||
while (true) {
|
||||
String nextLine = reader.readLine();
|
||||
|
||||
if (nextLine == null || nextLine.isEmpty()) {
|
||||
if (nextLine == null) {
|
||||
reader.close();
|
||||
break;
|
||||
}
|
||||
@@ -575,4 +704,114 @@ public class Utils {
|
||||
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
// Very crude Markdown to HTML conversion.
|
||||
// Only supports what's currently being used in CHANGELOG.md and PRIVACY.md.
|
||||
// May break easily.
|
||||
public static String basicMDToHTML(final String input) {
|
||||
return input
|
||||
.replaceAll("(?m)^#\\s+(.*)", "<h1>$1</h1>")
|
||||
.replaceAll("(?m)^##\\s+(.*)", "<h2>$1</h2>")
|
||||
.replaceAll("\\[([^]]+)\\]\\((https?://[\\w@#%&+=:?/.-]+)\\)", "<a href=\"$2\">$1</a>")
|
||||
.replaceAll("\\*\\*([^*]+)\\*\\*", "<b>$1</b>")
|
||||
.replaceAll("(?m)^-\\s+(.*)", "<ul><li> $1</li></ul>")
|
||||
.replace("</ul>\n<ul>", "");
|
||||
}
|
||||
|
||||
// Very crude autolinking.
|
||||
// Only supports what's currently being used in CHANGELOG.md and PRIVACY.md.
|
||||
// May break easily.
|
||||
public static String linkify(final String input) {
|
||||
return input
|
||||
.replaceAll("([\\w.-]+@[\\w-]+(\\.[\\w-]+)+)", "<a href=\"mailto:$1\">$1</a>")
|
||||
.replaceAll("(?<!href=\")\\b(https?://[\\w@#%&+=:?/.-]*[\\w@#%&+=:?/-])", "<a href=\"$1\">$1</a>");
|
||||
}
|
||||
|
||||
public static void setIconOrTextWithBackground(Context context, LoyaltyCard loyaltyCard, Bitmap icon, ImageView backgroundOrIcon, TextView textWhenNoImage) {
|
||||
if (icon != null) {
|
||||
Log.d("onResume", "setting icon image");
|
||||
textWhenNoImage.setVisibility(View.GONE);
|
||||
|
||||
backgroundOrIcon.setImageBitmap(icon);
|
||||
backgroundOrIcon.setBackgroundColor(Color.TRANSPARENT);
|
||||
} else {
|
||||
textWhenNoImage.setVisibility(View.VISIBLE);
|
||||
|
||||
int headerColor = getHeaderColor(context, loyaltyCard);
|
||||
|
||||
backgroundOrIcon.setImageBitmap(null);
|
||||
backgroundOrIcon.setBackgroundColor(headerColor);
|
||||
textWhenNoImage.setText(loyaltyCard.store);
|
||||
textWhenNoImage.setTextColor(Utils.needsDarkForeground(headerColor) ? Color.BLACK : Color.WHITE);
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean installedFromGooglePlay(Context context) {
|
||||
try {
|
||||
String packageName = context.getPackageName();
|
||||
String installer;
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
installer = context.getPackageManager().getInstallSourceInfo(packageName).getInstallingPackageName();
|
||||
} else {
|
||||
installer = context.getPackageManager().getInstallerPackageName(packageName);
|
||||
}
|
||||
return installer.equals("com.android.vending");
|
||||
} catch (Throwable ignored) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
public static void makeTextViewLinksClickable(final TextView textView, final Spanned text) {
|
||||
textView.setOnTouchListener((v, event) -> {
|
||||
int action = event.getAction();
|
||||
if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_DOWN) {
|
||||
int x = (int) event.getX() - textView.getTotalPaddingLeft() + textView.getScrollX();
|
||||
int y = (int) event.getY() - textView.getTotalPaddingTop() + textView.getScrollY();
|
||||
Layout layout = textView.getLayout();
|
||||
int line = layout.getLineForVertical(y);
|
||||
int off = layout.getOffsetForHorizontal(line, x);
|
||||
ClickableSpan[] links = text.getSpans(off, off, ClickableSpan.class);
|
||||
if (links.length != 0) {
|
||||
ClickableSpan link = links[0];
|
||||
if (action == MotionEvent.ACTION_UP) {
|
||||
link.onClick(textView);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
package protect.card_locker.contentprovider;
|
||||
|
||||
import static protect.card_locker.DBHelper.LoyaltyCardDbIds;
|
||||
|
||||
import android.content.ContentProvider;
|
||||
import android.content.ContentValues;
|
||||
import android.content.UriMatcher;
|
||||
import android.database.Cursor;
|
||||
import android.database.MatrixCursor;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import protect.card_locker.BuildConfig;
|
||||
import protect.card_locker.DBHelper;
|
||||
import protect.card_locker.preferences.Settings;
|
||||
|
||||
public class CardsContentProvider extends ContentProvider {
|
||||
private static final String TAG = "Catima";
|
||||
|
||||
public static final String AUTHORITY = BuildConfig.APPLICATION_ID + ".contentprovider.cards";
|
||||
|
||||
public static class Version {
|
||||
public static final String MAJOR_COLUMN = "major";
|
||||
public static final String MINOR_COLUMN = "minor";
|
||||
public static final int MAJOR = 1;
|
||||
public static final int MINOR = 0;
|
||||
}
|
||||
|
||||
private static final int URI_VERSION = 0;
|
||||
private static final int URI_CARDS = 1;
|
||||
private static final int URI_GROUPS = 2;
|
||||
private static final int URI_CARD_GROUPS = 3;
|
||||
|
||||
private static final String[] CARDS_DEFAULT_PROJECTION = new String[]{
|
||||
LoyaltyCardDbIds.ID,
|
||||
LoyaltyCardDbIds.STORE,
|
||||
LoyaltyCardDbIds.VALID_FROM,
|
||||
LoyaltyCardDbIds.EXPIRY,
|
||||
LoyaltyCardDbIds.BALANCE,
|
||||
LoyaltyCardDbIds.BALANCE_TYPE,
|
||||
LoyaltyCardDbIds.NOTE,
|
||||
LoyaltyCardDbIds.HEADER_COLOR,
|
||||
LoyaltyCardDbIds.CARD_ID,
|
||||
LoyaltyCardDbIds.BARCODE_ID,
|
||||
LoyaltyCardDbIds.BARCODE_TYPE,
|
||||
LoyaltyCardDbIds.STAR_STATUS,
|
||||
LoyaltyCardDbIds.LAST_USED,
|
||||
LoyaltyCardDbIds.ARCHIVE_STATUS,
|
||||
};
|
||||
|
||||
private static final UriMatcher uriMatcher = new UriMatcher(UriMatcher.NO_MATCH) {{
|
||||
addURI(AUTHORITY, "version", URI_VERSION);
|
||||
addURI(AUTHORITY, "cards", URI_CARDS);
|
||||
addURI(AUTHORITY, "groups", URI_GROUPS);
|
||||
addURI(AUTHORITY, "card_groups", URI_CARD_GROUPS);
|
||||
}};
|
||||
|
||||
@Override
|
||||
public boolean onCreate() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Cursor query(@NonNull final Uri uri,
|
||||
@Nullable final String[] projection,
|
||||
@Nullable final String selection,
|
||||
@Nullable final String[] selectionArgs,
|
||||
@Nullable final String sortOrder) {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
|
||||
// Disable the content provider on SDK < 23 since it grants dangerous
|
||||
// permissions at install-time
|
||||
Log.w(TAG, "Content provider read is only available for SDK >= 23");
|
||||
return null;
|
||||
}
|
||||
|
||||
final Settings settings = new Settings(getContext());
|
||||
if (!settings.getAllowContentProviderRead()) {
|
||||
Log.w(TAG, "Content provider read is disabled");
|
||||
return null;
|
||||
}
|
||||
|
||||
final String table;
|
||||
String[] updatedProjection = projection;
|
||||
|
||||
switch (uriMatcher.match(uri)) {
|
||||
case URI_VERSION:
|
||||
return queryVersion();
|
||||
case URI_CARDS:
|
||||
table = DBHelper.LoyaltyCardDbIds.TABLE;
|
||||
// Restrict columns to the default projection (omit internal columns such as zoom level)
|
||||
if (projection == null) {
|
||||
updatedProjection = CARDS_DEFAULT_PROJECTION;
|
||||
} else {
|
||||
final Set<String> defaultProjection = new HashSet<>(Arrays.asList(CARDS_DEFAULT_PROJECTION));
|
||||
updatedProjection = Arrays.stream(projection).filter(defaultProjection::contains).toArray(String[]::new);
|
||||
}
|
||||
break;
|
||||
case URI_GROUPS:
|
||||
table = DBHelper.LoyaltyCardDbGroups.TABLE;
|
||||
break;
|
||||
case URI_CARD_GROUPS:
|
||||
table = DBHelper.LoyaltyCardDbIdsGroups.TABLE;
|
||||
break;
|
||||
default:
|
||||
Log.w(TAG, "Unrecognized URI " + uri);
|
||||
return null;
|
||||
}
|
||||
|
||||
final DBHelper dbHelper = new DBHelper(getContext());
|
||||
final SQLiteDatabase database = dbHelper.getReadableDatabase();
|
||||
|
||||
return database.query(
|
||||
table,
|
||||
updatedProjection,
|
||||
selection,
|
||||
selectionArgs,
|
||||
null,
|
||||
null,
|
||||
sortOrder
|
||||
);
|
||||
}
|
||||
|
||||
private Cursor queryVersion() {
|
||||
final String[] columns = new String[]{Version.MAJOR_COLUMN, Version.MINOR_COLUMN};
|
||||
final MatrixCursor matrixCursor = new MatrixCursor(columns);
|
||||
matrixCursor.addRow(new Object[]{Version.MAJOR, Version.MINOR});
|
||||
|
||||
return matrixCursor;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public String getType(@NonNull final Uri uri) {
|
||||
// MIME types are not relevant (for now at least)
|
||||
return null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Uri insert(@NonNull final Uri uri,
|
||||
@Nullable final ContentValues values) {
|
||||
// This content provider is read-only for now, so we always return null
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int delete(@NonNull final Uri uri,
|
||||
@Nullable final String selection,
|
||||
@Nullable final String[] selectionArgs) {
|
||||
// This content provider is read-only for now, so we always return 0
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int update(@NonNull final Uri uri,
|
||||
@Nullable final ContentValues values,
|
||||
@Nullable final String selection,
|
||||
@Nullable final String[] selectionArgs) {
|
||||
// This content provider is read-only for now, so we always return 0
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -8,22 +8,14 @@ public class CSVHelpers {
|
||||
/**
|
||||
* Extract a string from the items array. The index into the array
|
||||
* is determined by looking up the index in the fields map using the
|
||||
* "key" as the key. If no such key exists, defaultValue is returned
|
||||
* if it is not null. Otherwise, a FormatException is thrown.
|
||||
* "key" as the key. If no such key exists, defaultValue is returned.
|
||||
*/
|
||||
static String extractString(String key, CSVRecord record, String defaultValue)
|
||||
throws FormatException {
|
||||
String toReturn = defaultValue;
|
||||
|
||||
static String extractString(String key, CSVRecord record, String defaultValue) {
|
||||
if (record.isMapped(key)) {
|
||||
toReturn = record.get(key);
|
||||
} else {
|
||||
if (defaultValue == null) {
|
||||
throw new FormatException("Field not used but expected: " + key);
|
||||
}
|
||||
return record.get(key);
|
||||
}
|
||||
|
||||
return toReturn;
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -32,15 +24,15 @@ public class CSVHelpers {
|
||||
* "key" as the key. If no such key exists, or the data is not a valid
|
||||
* int, a FormatException is thrown.
|
||||
*/
|
||||
static Integer extractInt(String key, CSVRecord record, boolean nullIsOk)
|
||||
static Integer extractInt(String key, CSVRecord record)
|
||||
throws FormatException {
|
||||
if (record.isMapped(key) == false) {
|
||||
if (!record.isMapped(key)) {
|
||||
throw new FormatException("Field not used but expected: " + key);
|
||||
}
|
||||
|
||||
String value = record.get(key);
|
||||
if (value.isEmpty() && nullIsOk) {
|
||||
return null;
|
||||
if (value.isEmpty()) {
|
||||
throw new FormatException("Field is empty: " + key);
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -56,15 +48,15 @@ public class CSVHelpers {
|
||||
* "key" as the key. If no such key exists, or the data is not a valid
|
||||
* int, a FormatException is thrown.
|
||||
*/
|
||||
static Long extractLong(String key, CSVRecord record, boolean nullIsOk)
|
||||
static Long extractLong(String key, CSVRecord record)
|
||||
throws FormatException {
|
||||
if (record.isMapped(key) == false) {
|
||||
if (!record.isMapped(key)) {
|
||||
throw new FormatException("Field not used but expected: " + key);
|
||||
}
|
||||
|
||||
String value = record.get(key);
|
||||
if (value.isEmpty() && nullIsOk) {
|
||||
return null;
|
||||
if (value.isEmpty()) {
|
||||
throw new FormatException("Field is empty: " + key);
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
@@ -127,6 +127,7 @@ public class CatimaExporter implements Exporter {
|
||||
printer.printRecord(DBHelper.LoyaltyCardDbIds.ID,
|
||||
DBHelper.LoyaltyCardDbIds.STORE,
|
||||
DBHelper.LoyaltyCardDbIds.NOTE,
|
||||
DBHelper.LoyaltyCardDbIds.VALID_FROM,
|
||||
DBHelper.LoyaltyCardDbIds.EXPIRY,
|
||||
DBHelper.LoyaltyCardDbIds.BALANCE,
|
||||
DBHelper.LoyaltyCardDbIds.BALANCE_TYPE,
|
||||
@@ -146,6 +147,7 @@ public class CatimaExporter implements Exporter {
|
||||
printer.printRecord(card.id,
|
||||
card.store,
|
||||
card.note,
|
||||
card.validFrom != null ? card.validFrom.getTime() : "",
|
||||
card.expiry != null ? card.expiry.getTime() : "",
|
||||
card.balance,
|
||||
card.balanceType,
|
||||
|
||||
@@ -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(context, 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(Context context, 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(context, 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(context, 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,9 +393,8 @@ public class CatimaImporter implements Importer {
|
||||
* Import a single loyalty card into the database using the given
|
||||
* session.
|
||||
*/
|
||||
private void importLoyaltyCard(Context context, SQLiteDatabase database, CSVRecord record)
|
||||
throws IOException, FormatException {
|
||||
int id = CSVHelpers.extractInt(DBHelper.LoyaltyCardDbIds.ID, record, false);
|
||||
private LoyaltyCard importLoyaltyCard(CSVRecord record) throws FormatException {
|
||||
int id = CSVHelpers.extractInt(DBHelper.LoyaltyCardDbIds.ID, record);
|
||||
|
||||
String store = CSVHelpers.extractString(DBHelper.LoyaltyCardDbIds.STORE, record, "");
|
||||
if (store.isEmpty()) {
|
||||
@@ -286,19 +402,38 @@ public class CatimaImporter implements Importer {
|
||||
}
|
||||
|
||||
String note = CSVHelpers.extractString(DBHelper.LoyaltyCardDbIds.NOTE, record, "");
|
||||
Date expiry = null;
|
||||
|
||||
Date validFrom = null;
|
||||
Long validFromLong;
|
||||
try {
|
||||
expiry = new Date(CSVHelpers.extractLong(DBHelper.LoyaltyCardDbIds.EXPIRY, record, true));
|
||||
} catch (NullPointerException | FormatException e) {
|
||||
validFromLong = CSVHelpers.extractLong(DBHelper.LoyaltyCardDbIds.VALID_FROM, record);
|
||||
} catch (FormatException ignored) {
|
||||
validFromLong = null;
|
||||
}
|
||||
if (validFromLong != null) {
|
||||
validFrom = new Date(validFromLong);
|
||||
}
|
||||
|
||||
BigDecimal balance;
|
||||
Date expiry = null;
|
||||
Long expiryLong;
|
||||
try {
|
||||
balance = new BigDecimal(CSVHelpers.extractString(DBHelper.LoyaltyCardDbIds.BALANCE, record, null));
|
||||
} catch (FormatException _e) {
|
||||
// These fields did not exist in versions 1.8.1 and before
|
||||
// We catch this exception so we can still import old backups
|
||||
balance = new BigDecimal("0");
|
||||
expiryLong = CSVHelpers.extractLong(DBHelper.LoyaltyCardDbIds.EXPIRY, record);
|
||||
} catch (FormatException ignored) {
|
||||
expiryLong = null;
|
||||
}
|
||||
if (expiryLong != null) {
|
||||
expiry = new Date(expiryLong);
|
||||
}
|
||||
|
||||
// These fields did not exist in versions 1.8.1 and before
|
||||
// We default to 0 so we can still import old backups
|
||||
BigDecimal balance = new BigDecimal("0");
|
||||
String balanceString = CSVHelpers.extractString(DBHelper.LoyaltyCardDbIds.BALANCE, record, null);
|
||||
if (balanceString != null) {
|
||||
try {
|
||||
balance = new BigDecimal(CSVHelpers.extractString(DBHelper.LoyaltyCardDbIds.BALANCE, record, null));
|
||||
} catch (NumberFormatException ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
Currency balanceType = null;
|
||||
@@ -324,14 +459,14 @@ public class CatimaImporter implements Importer {
|
||||
}
|
||||
|
||||
Integer headerColor = null;
|
||||
|
||||
if (record.isMapped(DBHelper.LoyaltyCardDbIds.HEADER_COLOR)) {
|
||||
headerColor = CSVHelpers.extractInt(DBHelper.LoyaltyCardDbIds.HEADER_COLOR, record, true);
|
||||
try {
|
||||
headerColor = CSVHelpers.extractInt(DBHelper.LoyaltyCardDbIds.HEADER_COLOR, record);
|
||||
} catch (FormatException ignored) {
|
||||
}
|
||||
|
||||
int starStatus = 0;
|
||||
try {
|
||||
starStatus = CSVHelpers.extractInt(DBHelper.LoyaltyCardDbIds.STAR_STATUS, record, false);
|
||||
starStatus = CSVHelpers.extractInt(DBHelper.LoyaltyCardDbIds.STAR_STATUS, record);
|
||||
} catch (FormatException _e) {
|
||||
// This field did not exist in versions 0.28 and before
|
||||
// We catch this exception so we can still import old backups
|
||||
@@ -340,7 +475,7 @@ public class CatimaImporter implements Importer {
|
||||
|
||||
int archiveStatus = 0;
|
||||
try {
|
||||
archiveStatus = CSVHelpers.extractInt(DBHelper.LoyaltyCardDbIds.ARCHIVE_STATUS, record, false);
|
||||
archiveStatus = CSVHelpers.extractInt(DBHelper.LoyaltyCardDbIds.ARCHIVE_STATUS, record);
|
||||
} catch (FormatException _e) {
|
||||
// This field did not exist in versions 2.16.3 and before
|
||||
// We catch this exception so we can still import old backups
|
||||
@@ -349,35 +484,41 @@ public class CatimaImporter implements Importer {
|
||||
|
||||
Long lastUsed = 0L;
|
||||
try {
|
||||
lastUsed = CSVHelpers.extractLong(DBHelper.LoyaltyCardDbIds.LAST_USED, record, false);
|
||||
lastUsed = CSVHelpers.extractLong(DBHelper.LoyaltyCardDbIds.LAST_USED, record);
|
||||
} catch (FormatException _e) {
|
||||
// This field did not exist in versions 2.5.0 and before
|
||||
// We catch this exception so we can still import old backups
|
||||
}
|
||||
|
||||
DBHelper.insertLoyaltyCard(database, id, store, note, 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);
|
||||
|
||||
DBHelper.insertGroup(database, id);
|
||||
if (id == null) {
|
||||
throw new FormatException("Group has no ID: " + record);
|
||||
}
|
||||
|
||||
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 {
|
||||
Integer cardId = CSVHelpers.extractInt(DBHelper.LoyaltyCardDbIdsGroups.cardID, record, false);
|
||||
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);
|
||||
|
||||
List<Group> cardGroups = DBHelper.getLoyaltyCardGroups(database, cardId);
|
||||
cardGroups.add(DBHelper.getGroup(database, groupId));
|
||||
DBHelper.setLoyaltyCardGroups(database, cardId, cardGroups);
|
||||
if (groupId == null) {
|
||||
throw new FormatException("Group has no ID: " + record);
|
||||
}
|
||||
|
||||
return Map.entry(cardId, groupId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,16 +11,21 @@ import org.apache.commons.csv.CSVParser;
|
||||
import org.apache.commons.csv.CSVRecord;
|
||||
import org.json.JSONException;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.StringReader;
|
||||
import java.math.BigDecimal;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.text.ParseException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import protect.card_locker.CatimaBarcode;
|
||||
import protect.card_locker.DBHelper;
|
||||
import protect.card_locker.FormatException;
|
||||
import protect.card_locker.LoyaltyCard;
|
||||
import protect.card_locker.Utils;
|
||||
|
||||
/**
|
||||
@@ -31,7 +36,16 @@ import protect.card_locker.Utils;
|
||||
* A header is expected for the each table showing the names of the columns.
|
||||
*/
|
||||
public class FidmeImporter implements Importer {
|
||||
public void importData(Context context, SQLiteDatabase database, InputStream input, char[] password) throws IOException, FormatException, JSONException, ParseException {
|
||||
public static class ImportedData {
|
||||
public final List<LoyaltyCard> cards;
|
||||
|
||||
ImportedData(final List<LoyaltyCard> cards) {
|
||||
this.cards = cards;
|
||||
}
|
||||
}
|
||||
|
||||
public void importData(Context context, SQLiteDatabase database, File inputFile, char[] password) throws IOException, FormatException, JSONException, ParseException {
|
||||
InputStream input = new FileInputStream(inputFile);
|
||||
// We actually retrieve a .zip file
|
||||
ZipInputStream zipInputStream = new ZipInputStream(input, password);
|
||||
|
||||
@@ -54,10 +68,14 @@ public class FidmeImporter implements Importer {
|
||||
}
|
||||
|
||||
final CSVParser fidmeParser = new CSVParser(new StringReader(loyaltyCards.toString()), CSVFormat.RFC4180.builder().setDelimiter(';').setHeader().build());
|
||||
ImportedData importedData = new ImportedData(new ArrayList<>());
|
||||
|
||||
try {
|
||||
for (CSVRecord record : fidmeParser) {
|
||||
importLoyaltyCard(context, database, record);
|
||||
LoyaltyCard card = importLoyaltyCard(context, record);
|
||||
if (card != null) {
|
||||
importedData.cards.add(card);
|
||||
}
|
||||
|
||||
if (Thread.currentThread().isInterrupted()) {
|
||||
throw new InterruptedException();
|
||||
@@ -70,14 +88,16 @@ public class FidmeImporter implements Importer {
|
||||
}
|
||||
|
||||
zipInputStream.close();
|
||||
input.close();
|
||||
|
||||
saveAndDeduplicate(database, importedData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Import a single loyalty card into the database using the given
|
||||
* session.
|
||||
*/
|
||||
private void importLoyaltyCard(Context context, SQLiteDatabase database, CSVRecord record)
|
||||
throws FormatException {
|
||||
private LoyaltyCard importLoyaltyCard(Context context, CSVRecord record) throws FormatException {
|
||||
// A loyalty card export from Fidme contains the following fields:
|
||||
// Retailer (store name)
|
||||
// Program (program name)
|
||||
@@ -113,7 +133,7 @@ public class FidmeImporter implements Importer {
|
||||
// Fidme deletes the card id if a card is expired
|
||||
// Because Catima considers the card id a required field, we ignore these expired cards
|
||||
// https://github.com/CatimaLoyalty/Android/issues/1005
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
|
||||
// Sadly, Fidme exports don't contain the card type
|
||||
@@ -128,6 +148,17 @@ public class FidmeImporter implements Importer {
|
||||
|
||||
// TODO: Front and back image
|
||||
|
||||
DBHelper.insertLoyaltyCard(database, store, note, null, BigDecimal.valueOf(0), null, cardId, null, barcodeType, headerColor, starStatus, null,archiveStatus);
|
||||
// use -1 for the ID, it will be ignored when inserting the card into the DB
|
||||
return new LoyaltyCard(-1, store, note, null, null, BigDecimal.valueOf(0), null, cardId, null, barcodeType, headerColor, starStatus, Utils.getUnixTime(), DBHelper.DEFAULT_ZOOM_LEVEL, archiveStatus);
|
||||
}
|
||||
|
||||
public void saveAndDeduplicate(SQLiteDatabase database, final ImportedData data) {
|
||||
// This format does not have IDs that can cause conflicts
|
||||
// Proper deduplication for all formats will be implemented later
|
||||
for (LoyaltyCard card : data.cards) {
|
||||
// Do not use card.id which is set to -1
|
||||
DBHelper.insertLoyaltyCard(database, card.store, card.note, card.validFrom, card.expiry, card.balance, card.balanceType,
|
||||
card.cardId, card.barcodeId, card.barcodeType, card.headerColor, card.starStatus, card.lastUsed, card.archiveStatus);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,8 +5,8 @@ import android.database.sqlite.SQLiteDatabase;
|
||||
|
||||
import org.json.JSONException;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.text.ParseException;
|
||||
|
||||
import protect.card_locker.FormatException;
|
||||
@@ -23,5 +23,5 @@ public interface Importer {
|
||||
* @throws IOException
|
||||
* @throws FormatException
|
||||
*/
|
||||
void importData(Context context, SQLiteDatabase database, InputStream input, char[] password) throws IOException, FormatException, InterruptedException, JSONException, ParseException;
|
||||
void importData(Context context, SQLiteDatabase database, File inputFile, char[] password) throws IOException, FormatException, InterruptedException, JSONException, ParseException;
|
||||
}
|
||||
|
||||
@@ -6,10 +6,15 @@ import android.util.Log;
|
||||
|
||||
import net.lingala.zip4j.exception.ZipException;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
import protect.card_locker.Utils;
|
||||
|
||||
public class MultiFormatImporter {
|
||||
private static final String TAG = "Catima";
|
||||
private static final String TEMP_ZIP_NAME = MultiFormatImporter.class.getSimpleName() + ".zip";
|
||||
|
||||
/**
|
||||
* Attempts to import data from the input stream of the
|
||||
@@ -42,23 +47,33 @@ public class MultiFormatImporter {
|
||||
|
||||
String error = null;
|
||||
if (importer != null) {
|
||||
database.beginTransaction();
|
||||
File inputFile;
|
||||
try {
|
||||
importer.importData(context, database, input, password);
|
||||
database.setTransactionSuccessful();
|
||||
return new ImportExportResult(ImportExportResultType.Success);
|
||||
} catch (ZipException e) {
|
||||
if (e.getType().equals(ZipException.Type.WRONG_PASSWORD)) {
|
||||
return new ImportExportResult(ImportExportResultType.BadPassword);
|
||||
} else {
|
||||
inputFile = Utils.copyToTempFile(context, input, TEMP_ZIP_NAME);
|
||||
database.beginTransaction();
|
||||
try {
|
||||
importer.importData(context, database, inputFile, password);
|
||||
database.setTransactionSuccessful();
|
||||
return new ImportExportResult(ImportExportResultType.Success);
|
||||
} catch (ZipException e) {
|
||||
if (e.getType().equals(ZipException.Type.WRONG_PASSWORD)) {
|
||||
return new ImportExportResult(ImportExportResultType.BadPassword);
|
||||
} else {
|
||||
Log.e(TAG, "Failed to import data", e);
|
||||
error = e.toString();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Failed to import data", e);
|
||||
error = e.toString();
|
||||
} finally {
|
||||
database.endTransaction();
|
||||
if (!inputFile.delete()) {
|
||||
Log.w(TAG, "Failed to delete temporary ZIP file (should not be a problem) " + inputFile);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Failed to import data", e);
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "Failed to copy ZIP file", e);
|
||||
error = e.toString();
|
||||
} finally {
|
||||
database.endTransaction();
|
||||
}
|
||||
} else {
|
||||
error = "Unsupported data format imported: " + format.name();
|
||||
|
||||
@@ -5,6 +5,8 @@ import android.database.sqlite.SQLiteDatabase;
|
||||
import android.graphics.Bitmap;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.google.zxing.BarcodeFormat;
|
||||
|
||||
import net.lingala.zip4j.io.inputstream.ZipInputStream;
|
||||
@@ -13,21 +15,30 @@ 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.Collections;
|
||||
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,21 +51,78 @@ 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 StocardProvider {
|
||||
public String name = null;
|
||||
public String barcodeFormat = null;
|
||||
public Bitmap logo = null;
|
||||
}
|
||||
|
||||
public static class StocardRecord {
|
||||
public String providerId = null;
|
||||
public String store = null;
|
||||
public String label = null;
|
||||
public String note = null;
|
||||
public String cardId = null;
|
||||
public String barcodeType = null;
|
||||
public Long lastUsed = null;
|
||||
public Bitmap frontImage = null;
|
||||
public Bitmap backImage = null;
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format(
|
||||
"StocardRecord{%n providerId=%s,%n store=%s,%n label=%s,%n note=%s,%n cardId=%s,%n"
|
||||
+ " barcodeType=%s,%n lastUsed=%s,%n frontImage=%s,%n backImage=%s%n}",
|
||||
this.providerId,
|
||||
this.store,
|
||||
this.label,
|
||||
this.note,
|
||||
this.cardId,
|
||||
this.barcodeType,
|
||||
this.lastUsed,
|
||||
this.frontImage,
|
||||
this.backImage
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public static class ZIPData {
|
||||
public final Map<String, StocardRecord> cards;
|
||||
public final Map<String, StocardProvider> providers;
|
||||
|
||||
ZIPData(final Map<String, StocardRecord> cards, final Map<String, StocardProvider> providers) {
|
||||
this.cards = cards;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
public static final String PROVIDER_PREFIX = "/loyalty-card-providers/";
|
||||
|
||||
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());
|
||||
|
||||
try {
|
||||
for (CSVRecord record : parser) {
|
||||
HashMap<String, Object> recordData = new HashMap<>();
|
||||
recordData.put("name", record.get("name"));
|
||||
recordData.put("barcodeFormat", record.get("barcodeFormat"));
|
||||
StocardProvider provider = new StocardProvider();
|
||||
provider.name = record.get("name").trim();
|
||||
provider.barcodeFormat = record.get("barcodeFormat").trim();
|
||||
|
||||
providers.put(record.get("_id"), recordData);
|
||||
zipData.providers.put(record.get("_id").trim(), provider);
|
||||
}
|
||||
|
||||
parser.close();
|
||||
@@ -62,160 +130,213 @@ 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.cards.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, FormatException, JSONException {
|
||||
Map<String, StocardRecord> cards = zipData.cards;
|
||||
Map<String, StocardProvider> providers = zipData.providers;
|
||||
|
||||
String[] providersFileName = null;
|
||||
String[] customProvidersBaseName = null;
|
||||
String customProviderId = "";
|
||||
String[] cardBaseName = null;
|
||||
String customProviderId = "";
|
||||
String cardName = "";
|
||||
LocalFileHeader localFileHeader;
|
||||
while ((localFileHeader = zipInputStream.getNextEntry()) != null) {
|
||||
String fileName = localFileHeader.getFileName();
|
||||
String[] nameParts = fileName.split("/");
|
||||
|
||||
if (providersFileName == null) {
|
||||
providersFileName = new String[]{
|
||||
nameParts[0],
|
||||
"sync",
|
||||
"data",
|
||||
"users",
|
||||
nameParts[0],
|
||||
"analytics-properties.json"
|
||||
};
|
||||
if (nameParts.length < 2) {
|
||||
continue;
|
||||
}
|
||||
|
||||
String userId = nameParts[1];
|
||||
|
||||
if (customProvidersBaseName == null) {
|
||||
// FIXME: can we use the points-account/statement/content.json balance info somehow?
|
||||
/*
|
||||
Known files:
|
||||
extracts/<user-UUID>/users/<user-UUID>/
|
||||
analytics-properties/content.json
|
||||
devices/<device-UUID>/
|
||||
analytics-properties/content.json
|
||||
content.json
|
||||
ip-location-wifi/content.json
|
||||
enabled-regions/<UUID>/content.json
|
||||
loyalty-card-custom-providers/<provider-UUID>/content.json - custom providers
|
||||
loyalty-cards/<card-UUID>/
|
||||
card-linked-coupons/accounts/default/
|
||||
content.json
|
||||
user-coupons/<UUID>/content.json
|
||||
content.json - card itself
|
||||
images/back.png - back image (legacy)
|
||||
images/back/back.jpg - back image
|
||||
images/back/content.json
|
||||
images/front.png - front image (legacy)
|
||||
images/front/content.json
|
||||
images/front/front.jpg - front image
|
||||
notes/default/content.json - note
|
||||
points-account/
|
||||
content.json
|
||||
statement/content.json
|
||||
usages/<UUID>/content.json - timestamps
|
||||
usage-statistics/content.json - timestamps
|
||||
reward-program-balances/<UUID>/content.json
|
||||
*/
|
||||
customProvidersBaseName = new String[]{
|
||||
nameParts[0],
|
||||
"sync",
|
||||
"data",
|
||||
"extracts",
|
||||
userId,
|
||||
"users",
|
||||
nameParts[0],
|
||||
userId,
|
||||
"loyalty-card-custom-providers"
|
||||
};
|
||||
cardBaseName = new String[]{
|
||||
nameParts[0],
|
||||
"sync",
|
||||
"data",
|
||||
"extracts",
|
||||
userId,
|
||||
"users",
|
||||
nameParts[0],
|
||||
userId,
|
||||
"loyalty-cards"
|
||||
};
|
||||
}
|
||||
|
||||
if (startsWith(nameParts, customProvidersBaseName, 1)) {
|
||||
// Extract providerId
|
||||
customProviderId = nameParts[customProvidersBaseName.length].split("\\.", 2)[0];
|
||||
customProviderId = nameParts[customProvidersBaseName.length];
|
||||
|
||||
StocardProvider provider = providers.get(customProviderId);
|
||||
if (provider == null) {
|
||||
provider = new StocardProvider();
|
||||
providers.put(customProviderId, provider);
|
||||
}
|
||||
|
||||
// Name file
|
||||
if (nameParts.length == customProvidersBaseName.length + 1) {
|
||||
// Ignore the .txt file
|
||||
if (fileName.endsWith(".json")) {
|
||||
JSONObject jsonObject = ZipUtils.readJSON(zipInputStream);
|
||||
|
||||
providers = appendToHashMap(
|
||||
providers,
|
||||
customProviderId,
|
||||
"name",
|
||||
jsonObject.getString("name")
|
||||
);
|
||||
}
|
||||
if (fileName.endsWith(customProviderId + "/content.json")) {
|
||||
JSONObject jsonObject = ZipUtils.readJSON(zipInputStream);
|
||||
provider.name = jsonObject.getString("name");
|
||||
} else if (fileName.endsWith("logo.png")) {
|
||||
providers = appendToHashMap(
|
||||
providers,
|
||||
customProviderId,
|
||||
"logo",
|
||||
ZipUtils.readImage(zipInputStream)
|
||||
);
|
||||
provider.logo = ZipUtils.readImage(zipInputStream);
|
||||
} else if (!fileName.endsWith("/")) {
|
||||
Log.d(TAG, "Unknown or unused loyalty-card-custom-providers file " + fileName + ", skipping...");
|
||||
}
|
||||
}
|
||||
|
||||
if (startsWith(nameParts, cardBaseName, 1)) {
|
||||
} else if (startsWith(nameParts, cardBaseName, 1)) {
|
||||
// Extract cardName
|
||||
cardName = nameParts[cardBaseName.length].split("\\.", 2)[0];
|
||||
cardName = nameParts[cardBaseName.length];
|
||||
|
||||
StocardRecord record = cards.get(cardName);
|
||||
if (record == null) {
|
||||
record = new StocardRecord();
|
||||
cards.put(cardName, record);
|
||||
}
|
||||
|
||||
// This is the card itself
|
||||
if (nameParts.length == cardBaseName.length + 1) {
|
||||
// Ignore the .txt file
|
||||
if (fileName.endsWith(".json")) {
|
||||
JSONObject jsonObject = ZipUtils.readJSON(zipInputStream);
|
||||
if (fileName.endsWith(cardName + "/content.json")) {
|
||||
JSONObject jsonObject = ZipUtils.readJSON(zipInputStream);
|
||||
record.cardId = jsonObject.getString("input_id");
|
||||
|
||||
loyaltyCardHashMap = appendToHashMap(
|
||||
loyaltyCardHashMap,
|
||||
cardName,
|
||||
"cardId",
|
||||
jsonObject.getString("input_id")
|
||||
);
|
||||
if (jsonObject.has("input_provider_name")) {
|
||||
record.store = jsonObject.getString("input_provider_name");
|
||||
}
|
||||
|
||||
// Provider ID can be either custom or not, extract whatever version is relevant
|
||||
String customProviderPrefix = "/users/" + nameParts[0] + "/loyalty-card-custom-providers/";
|
||||
String providerId = jsonObject
|
||||
.getJSONObject("input_provider_reference")
|
||||
.getString("identifier");
|
||||
if (providerId.startsWith(customProviderPrefix)) {
|
||||
providerId = providerId.substring(customProviderPrefix.length());
|
||||
} else {
|
||||
providerId = providerId.substring("/loyalty-card-providers/".length());
|
||||
}
|
||||
|
||||
loyaltyCardHashMap = appendToHashMap(
|
||||
loyaltyCardHashMap,
|
||||
cardName,
|
||||
"_providerId",
|
||||
providerId
|
||||
);
|
||||
|
||||
if (jsonObject.has("input_barcode_format")) {
|
||||
loyaltyCardHashMap = appendToHashMap(
|
||||
loyaltyCardHashMap,
|
||||
cardName,
|
||||
"barcodeType",
|
||||
jsonObject.getString("input_barcode_format")
|
||||
);
|
||||
if (jsonObject.has("label")) {
|
||||
String label = jsonObject.getString("label");
|
||||
if (!label.isBlank()) {
|
||||
record.label = label;
|
||||
}
|
||||
}
|
||||
} else if (fileName.endsWith("notes/default.json")) {
|
||||
loyaltyCardHashMap = appendToHashMap(
|
||||
loyaltyCardHashMap,
|
||||
cardName,
|
||||
"note",
|
||||
ZipUtils.readJSON(zipInputStream)
|
||||
.getString("content")
|
||||
);
|
||||
} else if (fileName.endsWith("/images/front.png")) {
|
||||
loyaltyCardHashMap = appendToHashMap(
|
||||
loyaltyCardHashMap,
|
||||
cardName,
|
||||
"frontImage",
|
||||
ZipUtils.readImage(zipInputStream)
|
||||
);
|
||||
} else if (fileName.endsWith("/images/back.png")) {
|
||||
loyaltyCardHashMap = appendToHashMap(
|
||||
loyaltyCardHashMap,
|
||||
cardName,
|
||||
"backImage",
|
||||
ZipUtils.readImage(zipInputStream)
|
||||
);
|
||||
|
||||
// Provider ID can be either custom or not, extract whatever version is relevant
|
||||
String customProviderPrefix = "/users/" + userId + "/loyalty-card-custom-providers/";
|
||||
String providerId = jsonObject
|
||||
.getJSONObject("input_provider_reference")
|
||||
.getString("identifier");
|
||||
if (providerId.startsWith(customProviderPrefix)) {
|
||||
providerId = providerId.substring(customProviderPrefix.length());
|
||||
} else if (providerId.startsWith(PROVIDER_PREFIX)) {
|
||||
providerId = providerId.substring(PROVIDER_PREFIX.length());
|
||||
} else {
|
||||
throw new FormatException("Unsupported provider ID format: " + providerId);
|
||||
}
|
||||
|
||||
record.providerId = providerId;
|
||||
|
||||
if (jsonObject.has("input_barcode_format")) {
|
||||
record.barcodeType = jsonObject.getString("input_barcode_format");
|
||||
}
|
||||
} else if (fileName.endsWith("notes/default/content.json")) {
|
||||
record.note = ZipUtils.readJSON(zipInputStream).getString("content");
|
||||
} else if (fileName.endsWith("usage-statistics/content.json")) {
|
||||
JSONArray usages = ZipUtils.readJSON(zipInputStream).getJSONArray("usages");
|
||||
for (int i = 0; i < usages.length(); i++) {
|
||||
JSONObject lastUsedObject = usages.getJSONObject(i);
|
||||
String lastUsedString = lastUsedObject.getJSONObject("time").getString("value");
|
||||
long timeStamp = Instant.parse(lastUsedString).getEpochSecond();
|
||||
if (record.lastUsed == null || timeStamp > record.lastUsed) {
|
||||
record.lastUsed = timeStamp;
|
||||
}
|
||||
}
|
||||
} else if (fileName.matches(".*/usages/[^/]+/content.json")) {
|
||||
JSONObject lastUsedObject = ZipUtils.readJSON(zipInputStream);
|
||||
String lastUsedString = lastUsedObject.getJSONObject("time").getString("value");
|
||||
long timeStamp = Instant.parse(lastUsedString).getEpochSecond();
|
||||
if (record.lastUsed == null || timeStamp > record.lastUsed) {
|
||||
record.lastUsed = timeStamp;
|
||||
}
|
||||
} else if (fileName.endsWith("/images/front.png") || fileName.endsWith("/images/front/front.jpg")) {
|
||||
record.frontImage = ZipUtils.readImage(zipInputStream);
|
||||
} else if (fileName.endsWith("/images/back.png") || fileName.endsWith("/images/back/back.jpg")) {
|
||||
record.backImage = ZipUtils.readImage(zipInputStream);
|
||||
} else if (!fileName.endsWith("/")) {
|
||||
Log.d(TAG, "Unknown or unused loyalty-cards file " + fileName + ", skipping...");
|
||||
}
|
||||
} else if (!fileName.endsWith("/")) {
|
||||
Log.d(TAG, "Unknown or unused file " + fileName + ", skipping...");
|
||||
}
|
||||
}
|
||||
|
||||
if (loyaltyCardHashMap.keySet().size() == 0) {
|
||||
throw new FormatException("Couldn't find any loyalty cards in this Stocard export.");
|
||||
}
|
||||
return new ZIPData(cards, providers);
|
||||
}
|
||||
|
||||
for (HashMap<String, Object> loyaltyCardData : loyaltyCardHashMap.values()) {
|
||||
String providerId = (String) loyaltyCardData.get("_providerId");
|
||||
public ImportedData importLoyaltyCardHashMap(Context context, final ZIPData zipData) throws FormatException {
|
||||
ImportedData importedData = new ImportedData(new ArrayList<>(), new HashMap<>());
|
||||
int tempID = 0;
|
||||
|
||||
if (providerId == null) {
|
||||
Log.d(TAG, "Missing providerId for card " + loyaltyCardData + ", ignoring...");
|
||||
List<String> cardKeys = new ArrayList<>(zipData.cards.keySet());
|
||||
Collections.sort(cardKeys);
|
||||
|
||||
for (String key : cardKeys) {
|
||||
StocardRecord record = zipData.cards.get(key);
|
||||
|
||||
if (record.providerId == null) {
|
||||
Log.d(TAG, "Missing providerId for card " + record + ", ignoring...");
|
||||
continue;
|
||||
}
|
||||
|
||||
HashMap<String, Object> providerData = providers.get(providerId);
|
||||
if (record.cardId == null) {
|
||||
throw new FormatException("No card ID listed, but is required");
|
||||
}
|
||||
|
||||
StocardProvider provider = zipData.providers.get(record.providerId);
|
||||
|
||||
// Read store from card, if not available (old export), fall back to providerData
|
||||
String store = record.store != null ? record.store : provider != null ? provider.name : record.providerId;
|
||||
String note = record.note != null ? record.note : "";
|
||||
String barcodeTypeString = record.barcodeType != null ? record.barcodeType : provider != null ? provider.barcodeFormat : null;
|
||||
|
||||
if (record.label != null && !record.label.equals(store) && !record.label.equals(note)) {
|
||||
note = note.isEmpty() ? record.label : note + "\n" + record.label;
|
||||
}
|
||||
|
||||
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);
|
||||
CatimaBarcode barcodeType = null;
|
||||
if (barcodeTypeString != null && !barcodeTypeString.isEmpty()) {
|
||||
if (barcodeTypeString.equals("RSS_DATABAR_EXPANDED")) {
|
||||
@@ -228,27 +349,45 @@ public class StocardImporter implements Importer {
|
||||
}
|
||||
|
||||
int headerColor = Utils.getRandomHeaderColor(context);
|
||||
Bitmap cardIcon = null;
|
||||
if (providerData != null && providerData.containsKey("logo")) {
|
||||
cardIcon = (Bitmap) providerData.get("logo");
|
||||
headerColor = Utils.getHeaderColorFromImage(cardIcon, headerColor);
|
||||
if (provider != null && provider.logo != null) {
|
||||
headerColor = Utils.getHeaderColorFromImage(provider.logo, headerColor);
|
||||
}
|
||||
|
||||
long loyaltyCardInternalId = DBHelper.insertLoyaltyCard(database, store, note, null, BigDecimal.valueOf(0), null, cardId, null, barcodeType, headerColor, 0, null,0);
|
||||
long lastUsed = record.lastUsed != null ? record.lastUsed : Utils.getUnixTime();
|
||||
|
||||
if (cardIcon != null) {
|
||||
Utils.saveCardImage(context, cardIcon, (int) loyaltyCardInternalId, ImageLocationType.icon);
|
||||
LoyaltyCard card = new LoyaltyCard(tempID, store, note, null, null, BigDecimal.valueOf(0), null, record.cardId, null, barcodeType, headerColor, 0, lastUsed, DBHelper.DEFAULT_ZOOM_LEVEL, 0);
|
||||
importedData.cards.add(card);
|
||||
|
||||
Map<ImageLocationType, Bitmap> images = new HashMap<>();
|
||||
|
||||
if (provider != null && provider.logo != null) {
|
||||
images.put(ImageLocationType.icon, provider.logo);
|
||||
}
|
||||
if (record.frontImage != null) {
|
||||
images.put(ImageLocationType.front, record.frontImage);
|
||||
}
|
||||
if (record.backImage != null) {
|
||||
images.put(ImageLocationType.back, record.backImage);
|
||||
}
|
||||
|
||||
if (loyaltyCardData.containsKey("frontImage")) {
|
||||
Utils.saveCardImage(context, (Bitmap) loyaltyCardData.get("frontImage"), (int) loyaltyCardInternalId, ImageLocationType.front);
|
||||
}
|
||||
if (loyaltyCardData.containsKey("backImage")) {
|
||||
Utils.saveCardImage(context, (Bitmap) loyaltyCardData.get("backImage"), (int) loyaltyCardInternalId, ImageLocationType.back);
|
||||
}
|
||||
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) {
|
||||
@@ -264,16 +403,4 @@ public class StocardImporter implements Importer {
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private HashMap<String, HashMap<String, Object>> appendToHashMap(HashMap<String, HashMap<String, Object>> loyaltyCardHashMap, String cardID, String key, Object value) {
|
||||
HashMap<String, Object> loyaltyCardData = loyaltyCardHashMap.get(cardID);
|
||||
if (loyaltyCardData == null) {
|
||||
loyaltyCardData = new HashMap<>();
|
||||
}
|
||||
|
||||
loyaltyCardData.put(key, value);
|
||||
loyaltyCardHashMap.put(cardID, loyaltyCardData);
|
||||
|
||||
return loyaltyCardHashMap;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,8 @@ import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
@@ -19,13 +21,16 @@ import java.math.BigDecimal;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.text.ParseException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Currency;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.TimeZone;
|
||||
|
||||
import protect.card_locker.CatimaBarcode;
|
||||
import protect.card_locker.DBHelper;
|
||||
import protect.card_locker.FormatException;
|
||||
import protect.card_locker.LoyaltyCard;
|
||||
import protect.card_locker.Utils;
|
||||
|
||||
/**
|
||||
@@ -36,7 +41,16 @@ import protect.card_locker.Utils;
|
||||
* A header is expected for the each table showing the names of the columns.
|
||||
*/
|
||||
public class VoucherVaultImporter implements Importer {
|
||||
public void importData(Context context, SQLiteDatabase database, InputStream input, char[] password) throws IOException, FormatException, JSONException, ParseException {
|
||||
public static class ImportedData {
|
||||
public final List<LoyaltyCard> cards;
|
||||
|
||||
ImportedData(final List<LoyaltyCard> cards) {
|
||||
this.cards = cards;
|
||||
}
|
||||
}
|
||||
|
||||
public void importData(Context context, SQLiteDatabase database, File inputFile, char[] password) throws IOException, FormatException, JSONException, ParseException {
|
||||
InputStream input = new FileInputStream(inputFile);
|
||||
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8));
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
@@ -46,6 +60,16 @@ public class VoucherVaultImporter implements Importer {
|
||||
}
|
||||
JSONArray jsonArray = new JSONArray(sb.toString());
|
||||
|
||||
bufferedReader.close();
|
||||
input.close();
|
||||
|
||||
ImportedData importedData = importJSON(jsonArray);
|
||||
saveAndDeduplicate(database, importedData);
|
||||
}
|
||||
|
||||
public ImportedData importJSON(JSONArray jsonArray) throws FormatException, JSONException, ParseException {
|
||||
ImportedData importedData = new ImportedData(new ArrayList<>());
|
||||
|
||||
// See https://github.com/tim-smart/vouchervault/issues/4#issuecomment-788226503 for more info
|
||||
for (int i = 0; i < jsonArray.length(); i++) {
|
||||
JSONObject jsonCard = jsonArray.getJSONObject(i);
|
||||
@@ -126,9 +150,20 @@ public class VoucherVaultImporter implements Importer {
|
||||
throw new FormatException("Unknown colour type found: " + colorFromJSON);
|
||||
}
|
||||
|
||||
DBHelper.insertLoyaltyCard(database, store, "", 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -63,30 +63,6 @@ public class Settings {
|
||||
return AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM;
|
||||
}
|
||||
|
||||
public double getFontSizeScale() {
|
||||
return getInt(R.string.settings_key_max_font_size_scale, R.integer.settings_max_font_size_scale_pct) / 100.0;
|
||||
}
|
||||
|
||||
public int getSmallFont() {
|
||||
return 14;
|
||||
}
|
||||
|
||||
public int getMediumFont() {
|
||||
return 28;
|
||||
}
|
||||
|
||||
public int getLargeFont() {
|
||||
return 40;
|
||||
}
|
||||
|
||||
public int getFontSizeMin(int fontSize) {
|
||||
return (int) (Math.round(fontSize / 2.0) - 1);
|
||||
}
|
||||
|
||||
public int getFontSizeMax(int fontSize) {
|
||||
return (int) Math.round(fontSize * getFontSizeScale());
|
||||
}
|
||||
|
||||
public boolean useMaxBrightnessDisplayingBarcode() {
|
||||
return getBoolean(R.string.settings_key_display_barcode_max_brightness, true);
|
||||
}
|
||||
@@ -103,6 +79,10 @@ public class Settings {
|
||||
return getBoolean(R.string.settings_key_disable_lockscreen_while_viewing_card, true);
|
||||
}
|
||||
|
||||
public boolean getAllowContentProviderRead() {
|
||||
return getBoolean(R.string.settings_key_allow_content_provider_read, true);
|
||||
}
|
||||
|
||||
public boolean getOledDark() {
|
||||
return getBoolean(R.string.settings_key_oled_dark, false);
|
||||
}
|
||||
|
||||
@@ -3,28 +3,27 @@ package protect.card_locker.preferences;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.view.MenuItem;
|
||||
|
||||
import com.google.android.material.color.DynamicColors;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.app.AppCompatDelegate;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
import androidx.fragment.app.FragmentResultListener;
|
||||
import androidx.core.os.LocaleListCompat;
|
||||
import androidx.preference.ListPreference;
|
||||
import androidx.preference.Preference;
|
||||
import androidx.preference.PreferenceFragmentCompat;
|
||||
|
||||
import kotlin.Suppress;
|
||||
import nl.invissvenska.numberpickerpreference.NumberDialogPreference;
|
||||
import nl.invissvenska.numberpickerpreference.NumberPickerPreferenceDialogFragment;
|
||||
import com.google.android.material.color.DynamicColors;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import protect.card_locker.CatimaAppCompatActivity;
|
||||
import protect.card_locker.MainActivity;
|
||||
import protect.card_locker.R;
|
||||
@@ -103,23 +102,8 @@ public class SettingsActivity extends CatimaAppCompatActivity {
|
||||
// Load the preferences from an XML resource
|
||||
addPreferencesFromResource(R.xml.preferences);
|
||||
|
||||
// Show pretty names
|
||||
ListPreference localePreference = findPreference(getResources().getString(R.string.settings_key_locale));
|
||||
assert localePreference != null;
|
||||
CharSequence[] entryValues = localePreference.getEntryValues();
|
||||
List<CharSequence> entries = new ArrayList<>();
|
||||
for (CharSequence entry : entryValues) {
|
||||
if (entry.length() == 0) {
|
||||
entries.add(getResources().getString(R.string.settings_system_locale));
|
||||
} else {
|
||||
Locale entryLocale = Utils.stringToLocale(entry.toString());
|
||||
entries.add(entryLocale.getDisplayName(entryLocale));
|
||||
}
|
||||
}
|
||||
|
||||
localePreference.setEntries(entries.toArray(new CharSequence[entryValues.length]));
|
||||
|
||||
Preference themePreference = findPreference(getResources().getString(R.string.settings_key_theme));
|
||||
// Show pretty names and summaries
|
||||
ListPreference themePreference = findPreference(getResources().getString(R.string.settings_key_theme));
|
||||
assert themePreference != null;
|
||||
themePreference.setOnPreferenceChangeListener((preference, o) -> {
|
||||
if (o.toString().equals(getResources().getString(R.string.settings_key_light_theme))) {
|
||||
@@ -133,10 +117,16 @@ public class SettingsActivity extends CatimaAppCompatActivity {
|
||||
return true;
|
||||
});
|
||||
|
||||
localePreference.setOnPreferenceChangeListener((preference, newValue) -> {
|
||||
ListPreference themeColorPreference = findPreference(getResources().getString(R.string.setting_key_theme_color));
|
||||
assert themeColorPreference != null;
|
||||
themeColorPreference.setOnPreferenceChangeListener((preference, o) -> {
|
||||
refreshActivity(true);
|
||||
return true;
|
||||
});
|
||||
if (!DynamicColors.isDynamicColorAvailable()) {
|
||||
themeColorPreference.setEntryValues(R.array.color_values_no_dynamic);
|
||||
themeColorPreference.setEntries(R.array.color_value_strings_no_dynamic);
|
||||
}
|
||||
|
||||
Preference oledDarkPreference = findPreference(getResources().getString(R.string.settings_key_oled_dark));
|
||||
assert oledDarkPreference != null;
|
||||
@@ -145,16 +135,61 @@ public class SettingsActivity extends CatimaAppCompatActivity {
|
||||
return true;
|
||||
});
|
||||
|
||||
ListPreference colorPreference = findPreference(getResources().getString(R.string.setting_key_theme_color));
|
||||
assert colorPreference != null;
|
||||
colorPreference.setOnPreferenceChangeListener((preference, o) -> {
|
||||
refreshActivity(true);
|
||||
ListPreference localePreference = findPreference(getResources().getString(R.string.settings_key_locale));
|
||||
assert localePreference != null;
|
||||
CharSequence[] entryValues = localePreference.getEntryValues();
|
||||
List<CharSequence> entries = new ArrayList<>();
|
||||
for (CharSequence entry : entryValues) {
|
||||
if (entry.length() == 0) {
|
||||
entries.add(getResources().getString(R.string.settings_system_locale));
|
||||
} else {
|
||||
Locale entryLocale = Utils.stringToLocale(entry.toString());
|
||||
entries.add(entryLocale.getDisplayName(entryLocale));
|
||||
}
|
||||
}
|
||||
localePreference.setEntries(entries.toArray(new CharSequence[entryValues.length]));
|
||||
// Make locale picker preference in sync with system settings
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
Locale sysLocale = AppCompatDelegate.getApplicationLocales().get(0);
|
||||
if (sysLocale == null) {
|
||||
// Corresponds to "System"
|
||||
localePreference.setValue("");
|
||||
} else {
|
||||
// Need to set preference's value to one of localePreference.getEntryValues() to match the locale.
|
||||
// Locale.toLanguageTag() theoretically should be one of the values in localePreference.getEntryValues()...
|
||||
// But it doesn't work for some locales. so trying something more heavyweight.
|
||||
|
||||
// Obtain all locales supported by the app.
|
||||
List<Locale> appLocales = Arrays.stream(localePreference.getEntryValues())
|
||||
.map(Objects::toString)
|
||||
.map(Utils::stringToLocale)
|
||||
.collect(Collectors.toList());
|
||||
// Get the app locale that best matches the system one
|
||||
Locale bestMatchLocale = Utils.getBestMatchLocale(appLocales, sysLocale);
|
||||
// Get its index in supported locales
|
||||
int index = appLocales.indexOf(bestMatchLocale);
|
||||
// Set preference value to entry value at that index
|
||||
localePreference.setValue(localePreference.getEntryValues()[index].toString());
|
||||
}
|
||||
}
|
||||
|
||||
localePreference.setOnPreferenceChangeListener((preference, newValue) -> {
|
||||
// See corresponding comment in Utils.updateBaseContextLocale for Android 6- notes
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
|
||||
refreshActivity(true);
|
||||
return true;
|
||||
}
|
||||
String newLocale = (String) newValue;
|
||||
// If newLocale is empty, that means "System" was selected
|
||||
AppCompatDelegate.setApplicationLocales(newLocale.isEmpty() ? LocaleListCompat.getEmptyLocaleList() : LocaleListCompat.create(Utils.stringToLocale(newLocale)));
|
||||
return true;
|
||||
});
|
||||
if (!DynamicColors.isDynamicColorAvailable()) {
|
||||
colorPreference.setEntryValues(R.array.color_values_no_dynamic);
|
||||
colorPreference.setEntries(R.array.color_value_strings_no_dynamic);
|
||||
}
|
||||
|
||||
// Disable content provider on SDK < 23 since dangerous permissions
|
||||
// are granted at install-time
|
||||
Preference contentProviderReadPreference = findPreference(getResources().getString(R.string.settings_key_allow_content_provider_read));
|
||||
assert contentProviderReadPreference != null;
|
||||
contentProviderReadPreference.setVisible(Build.VERSION.SDK_INT >= Build.VERSION_CODES.M);
|
||||
}
|
||||
|
||||
private void refreshActivity(boolean reloadMain) {
|
||||
@@ -164,30 +199,5 @@ public class SettingsActivity extends CatimaAppCompatActivity {
|
||||
activity.recreate();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("deprecation") // setTargetFragment
|
||||
// androidx.preference.PreferenceDialogFragmentCompat uses the deprecated method
|
||||
// `getTargetFragment()`, which throws if `setTargetFragment()` isn't used before.
|
||||
// While this isn't fixed on upstream, suppress the deprecation warning
|
||||
// https://issuetracker.google.com/issues/181793702
|
||||
public void onDisplayPreferenceDialog(Preference preference) {
|
||||
if (preference instanceof NumberDialogPreference) {
|
||||
NumberDialogPreference dialogPreference = (NumberDialogPreference) preference;
|
||||
DialogFragment dialogFragment = NumberPickerPreferenceDialogFragment
|
||||
.newInstance(
|
||||
dialogPreference.getKey(),
|
||||
dialogPreference.getMinValue(),
|
||||
dialogPreference.getMaxValue(),
|
||||
dialogPreference.getStepValue(),
|
||||
dialogPreference.getUnitText()
|
||||
);
|
||||
dialogFragment.setTargetFragment(this, 0);
|
||||
dialogFragment.show(getParentFragmentManager(), DIALOG_FRAGMENT_TAG);
|
||||
} else {
|
||||
super.onDisplayPreferenceDialog(preference);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="oval" android:useLevel="true"
|
||||
android:dither="true">
|
||||
|
||||
<size android:height="12dip" android:width="12dip"/>
|
||||
|
||||
<solid android:color="@android:color/white"/>
|
||||
</shape>
|
||||
@@ -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="M18,8h-1L17,6c0,-2.76 -2.24,-5 -5,-5S7,3.24 7,6v2L6,8c-1.1,0 -2,0.9 -2,2v10c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2L20,10c0,-1.1 -0.9,-2 -2,-2zM12,17c-1.1,0 -2,-0.9 -2,-2s0.9,-2 2,-2 2,0.9 2,2 -0.9,2 -2,2zM15.1,8L8.9,8L8.9,6c0,-1.71 1.39,-3.1 3.1,-3.1 1.71,0 3.1,1.39 3.1,3.1v2z"/>
|
||||
<path android:fillColor="@android:color/white" android:pathData="M12,4.5C7,4.5 2.73,7.61 1,12c1.73,4.39 6,7.5 11,7.5s9.27,-3.11 11,-7.5c-1.73,-4.39 -6,-7.5 -11,-7.5zM12,17c-2.76,0 -5,-2.24 -5,-5s2.24,-5 5,-5 5,2.24 5,5 -2.24,5 -5,5zM12,9c-1.66,0 -3,1.34 -3,3s1.34,3 3,3 3,-1.34 3,-3 -1.34,-3 -3,-3z"/>
|
||||
</vector>
|
||||
24
app/src/main/res/drawable/dialog_bg_monet.xml
Normal file
24
app/src/main/res/drawable/dialog_bg_monet.xml
Normal file
@@ -0,0 +1,24 @@
|
||||
<layer-list xmlns:tools="http://schemas.android.com/tools"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item>
|
||||
<shape>
|
||||
<solid android:color="?attr/colorSurface" />
|
||||
<corners
|
||||
android:bottomLeftRadius="28dp"
|
||||
android:bottomRightRadius="28dp"
|
||||
android:topLeftRadius="28dp"
|
||||
android:topRightRadius="28dp" />
|
||||
</shape>
|
||||
</item>
|
||||
<item>
|
||||
<shape>
|
||||
<solid android:color="@color/m3_popupmenu_overlay_color"
|
||||
tools:ignore="PrivateResource" />
|
||||
<corners
|
||||
android:bottomLeftRadius="28dp"
|
||||
android:bottomRightRadius="28dp"
|
||||
android:topLeftRadius="28dp"
|
||||
android:topRightRadius="28dp" />
|
||||
</shape>
|
||||
</item>
|
||||
</layer-list>
|
||||
@@ -1,4 +1,4 @@
|
||||
<vector android:height="24dp" android:tint="#FFFFFF"
|
||||
<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="M15.41,7.41L14,6l-6,6 6,6 1.41,-1.41L10.83,12z"/>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<vector android:height="24dp" android:tint="#FFFFFF"
|
||||
<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="M10,6L8.59,7.41 13.17,12l-4.58,4.59L10,18l6,-6z"/>
|
||||
|
||||
10
app/src/main/res/drawable/ic_baseline_expand_more_24.xml
Normal file
10
app/src/main/res/drawable/ic_baseline_expand_more_24.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M16.59,8.59L12,13.17 7.41,8.59 6,10l6,6 6,-6z"/>
|
||||
</vector>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -1,10 +0,0 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="?attr/colorControlNormal"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FFFFFFFF"
|
||||
android:pathData="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z"/>
|
||||
</vector>
|
||||
@@ -1,5 +0,0 @@
|
||||
<vector android:height="24dp" android:tint="#FFFFFF"
|
||||
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="M18,16.08c-0.76,0 -1.44,0.3 -1.96,0.77L8.91,12.7c0.05,-0.23 0.09,-0.46 0.09,-0.7s-0.04,-0.47 -0.09,-0.7l7.05,-4.11c0.54,0.5 1.25,0.81 2.04,0.81 1.66,0 3,-1.34 3,-3s-1.34,-3 -3,-3 -3,1.34 -3,3c0,0.24 0.04,0.47 0.09,0.7L8.04,9.81C7.5,9.31 6.79,9 6,9c-1.66,0 -3,1.34 -3,3s1.34,3 3,3c0.79,0 1.5,-0.31 2.04,-0.81l7.12,4.16c-0.05,0.21 -0.08,0.43 -0.08,0.65 0,1.61 1.31,2.92 2.92,2.92 1.61,0 2.92,-1.31 2.92,-2.92s-1.31,-2.92 -2.92,-2.92z"/>
|
||||
</vector>
|
||||
@@ -1,5 +1,5 @@
|
||||
<vector android:autoMirrored="true" android:height="24dp" android:tint="?attr/colorControlNormal"
|
||||
<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="M20,11H7.83l5.59,-5.59L12,4l-8,8 8,8 1.41,-1.41L7.83,13H20v-2z"/>
|
||||
<path android:fillColor="@android:color/white" android:pathData="M12,17.27L18.18,21l-1.64,-7.03L22,9.24l-7.19,-0.61L12,2 9.19,8.63 2,9.24l5.46,4.73L5.82,21z"/>
|
||||
</vector>
|
||||
@@ -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="M12,17c1.1,0 2,-0.9 2,-2s-0.9,-2 -2,-2 -2,0.9 -2,2 0.9,2 2,2zM18,8h-1L17,6c0,-2.76 -2.24,-5 -5,-5S7,3.24 7,6h1.9c0,-1.71 1.39,-3.1 3.1,-3.1 1.71,0 3.1,1.39 3.1,3.1v2L6,8c-1.1,0 -2,0.9 -2,2v10c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2L20,10c0,-1.1 -0.9,-2 -2,-2zM18,20L6,20L6,10h12v10z"/>
|
||||
<path android:fillColor="@android:color/white" android:pathData="M22,9.24l-7.19,-0.62L12,2 9.19,8.63 2,9.24l5.46,4.73L5.82,21 12,17.27 18.18,21l-1.63,-7.03L22,9.24zM12,15.4l-3.76,2.27 1,-4.28 -3.32,-2.88 4.38,-0.38L12,6.1l1.71,4.04 4.38,0.38 -3.32,2.88 1,4.28L12,15.4z"/>
|
||||
</vector>
|
||||
16
app/src/main/res/drawable/ic_valid_from_24dp.xml
Normal file
16
app/src/main/res/drawable/ic_valid_from_24dp.xml
Normal file
@@ -0,0 +1,16 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M12.5,7H11v6l5.25,3.15 0.75,-1.23 -4.5,-2.67z" />
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M18,15.782V22l4.886,-3.109z" />
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M11.99,2C6.47,2 2,6.48 2,12C2,17.52 6.47,22 11.99,22C13.417,22 14.772,21.699 16,21.162L16,18.928C14.823,19.608 13.458,20 12,20C7.58,20 4,16.42 4,12C4,7.58 7.58,4 12,4C16.42,4 20,7.58 20,12C20,13.061 19.791,14.073 19.416,15L21.541,15C21.839,14.053 22,13.045 22,12C22,6.48 17.52,2 11.99,2zM11,7L11,13L16,16L16,15L16.951,15L17,14.92L12.5,12.25L12.5,7L11,7z" />
|
||||
</vector>
|
||||
@@ -1,9 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="oval" android:useLevel="true"
|
||||
android:dither="true">
|
||||
|
||||
<size android:height="8dip" android:width="8dip"/>
|
||||
|
||||
<solid android:color="@android:color/white"/>
|
||||
</shape>
|
||||
@@ -128,11 +128,10 @@
|
||||
|
||||
<TextView
|
||||
android:id="@+id/translate_sub"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="2dp"
|
||||
android:text="@string/translate_platform"
|
||||
android:layout_marginEnd="20dp"
|
||||
android:textSize="16sp"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/translate_main"/>
|
||||
@@ -169,11 +168,10 @@
|
||||
|
||||
<TextView
|
||||
android:id="@+id/license_sub"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="2dp"
|
||||
android:text="@string/app_license"
|
||||
android:layout_marginEnd="20dp"
|
||||
android:textSize="16sp"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/license_main"/>
|
||||
@@ -271,6 +269,36 @@
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/donate"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="8dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/donate_main"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:fontFamily="sans-serif-medium"
|
||||
android:padding="2dp"
|
||||
android:text="@string/donate"
|
||||
android:textSize="18sp"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:importantForAccessibility="no"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="8dp"
|
||||
android:fontFamily="sans-serif-medium"
|
||||
android:text="@string/arrow"
|
||||
android:textSize="20sp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/rate"
|
||||
android:layout_width="match_parent"
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
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"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:fitsSystemWindows="true"
|
||||
tools:context="protect.card_locker.MainActivity">
|
||||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
android:id="@+id/toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
style="?attr/toolbarStyle" />
|
||||
|
||||
<com.google.android.material.tabs.TabLayout
|
||||
android:id="@+id/groups"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:tabMode="scrollable"
|
||||
android:visibility="gone"/>
|
||||
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
<include
|
||||
android:id="@+id/include"
|
||||
layout="@layout/content_main" />
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
@@ -34,7 +34,7 @@
|
||||
app:layout_constraintBottom_toTopOf="@+id/barcodeIdLayout"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
android:text="@string/enterBarcodeInstructions" />
|
||||
android:text="@string/manually_enter_barcode_instructions" />
|
||||
<LinearLayout
|
||||
android:id="@+id/barcodeIdLayout"
|
||||
android:orientation="horizontal"
|
||||
@@ -42,7 +42,7 @@
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintTop_toBottomOf="@+id/explanationText"
|
||||
app:layout_constraintBottom_toTopOf="@+id/noBarcode"
|
||||
app:layout_constraintBottom_toTopOf="@+id/barcodes"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent">
|
||||
<TextView android:textSize="16.0sp"
|
||||
@@ -65,21 +65,11 @@
|
||||
android:minHeight="48dp"
|
||||
tools:ignore="ContentDescription" />
|
||||
</LinearLayout>
|
||||
<Button
|
||||
android:id="@+id/noBarcode"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintTop_toBottomOf="@+id/barcodeIdLayout"
|
||||
app:layout_constraintBottom_toTopOf="@+id/barcodes"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
android:text="@string/barcodeNoBarcode"
|
||||
android:enabled="false" />
|
||||
<ListView
|
||||
android:id="@+id/barcodes"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="fill_parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/noBarcode"
|
||||
app:layout_constraintTop_toBottomOf="@+id/barcodeIdLayout"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
|
||||
@@ -2,6 +2,15 @@
|
||||
<merge xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
|
||||
android:id="@+id/fabOtherOptions"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="end|bottom"
|
||||
app:icon="@drawable/ic_baseline_unfold_more_24"
|
||||
android:text="@string/action_more_options"
|
||||
android:layout_margin="16dp" />
|
||||
|
||||
<com.journeyapps.barcodescanner.BarcodeView
|
||||
android:id="@+id/zxing_barcode_surface"
|
||||
android:layout_width="match_parent"
|
||||
@@ -33,25 +42,5 @@
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/add_from_image"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/addFromImage"
|
||||
app:layout_constraintBottom_toTopOf="@+id/add_manually"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/camera_permission_denied_layout"
|
||||
app:layout_constraintVertical_bias="1.0" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/add_manually"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/addManually"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</merge>
|
||||
@@ -37,36 +37,48 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<ImageButton
|
||||
<Button
|
||||
android:id="@+id/moveUp"
|
||||
android:layout_width="@dimen/cardThumbnailSize"
|
||||
android:layout_height="@dimen/cardThumbnailSize"
|
||||
style="?attr/materialIconButtonFilledStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
app:srcCompat="@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"/>
|
||||
|
||||
<ImageButton
|
||||
<Button
|
||||
android:id="@+id/moveDown"
|
||||
android:layout_width="@dimen/cardThumbnailSize"
|
||||
android:layout_height="@dimen/cardThumbnailSize"
|
||||
style="?attr/materialIconButtonFilledStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
app:srcCompat="@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"/>
|
||||
|
||||
<ImageButton
|
||||
<Button
|
||||
android:id="@+id/edit"
|
||||
android:layout_width="@dimen/cardThumbnailSize"
|
||||
android:layout_height="@dimen/cardThumbnailSize"
|
||||
style="?attr/materialIconButtonFilledStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
app:srcCompat="@drawable/ic_mode_edit_white_24dp"
|
||||
app:icon="@drawable/ic_mode_edit_white_24dp"
|
||||
app:iconGravity="textStart"
|
||||
app:tint="?attr/colorOnPrimary"
|
||||
android:contentDescription="@string/edit"/>
|
||||
|
||||
<ImageButton
|
||||
<Button
|
||||
android:id="@+id/delete"
|
||||
android:layout_width="@dimen/cardThumbnailSize"
|
||||
android:layout_height="@dimen/cardThumbnailSize"
|
||||
style="?attr/materialIconButtonFilledStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
app:srcCompat="@drawable/ic_delete_white_24dp"
|
||||
app:icon="@drawable/ic_delete_white_24dp"
|
||||
app:iconGravity="textStart"
|
||||
app:tint="?attr/colorOnPrimary"
|
||||
android:contentDescription="@string/delete"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
@@ -288,7 +288,7 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="numberDecimal"
|
||||
android:digits="0123456789.,$" />
|
||||
android:digits="0123456789,." />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
@@ -311,6 +311,32 @@
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
</LinearLayout>
|
||||
|
||||
<!-- Valid from -->
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingHorizontal="@dimen/inputPadding"
|
||||
android:paddingTop="@dimen/inputPadding"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<!-- Valid from -->
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/validFromView"
|
||||
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.ExposedDropdownMenu"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1.0"
|
||||
android:hint="@string/validFromDate"
|
||||
android:labelFor="@+id/validFromField">
|
||||
|
||||
<AutoCompleteTextView
|
||||
android:id="@+id/validFromField"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="none"/>
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
</LinearLayout>
|
||||
|
||||
<!-- Expiration -->
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
|
||||
@@ -2,15 +2,15 @@
|
||||
<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"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="4dp">
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:id="@+id/icon_layout"
|
||||
@@ -24,7 +24,6 @@
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<ImageView
|
||||
android:importantForAccessibility="no"
|
||||
android:id="@+id/thumbnail"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
@@ -36,6 +35,20 @@
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/thumbnail_text"
|
||||
android:importantForAccessibility="no"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:textStyle="bold"
|
||||
app:autoSizeTextType="uniform"
|
||||
app:autoSizeMinTextSize="12sp"
|
||||
app:autoSizeMaxTextSize="100sp"
|
||||
app:autoSizeStepGranularity="2sp"
|
||||
android:gravity="center"
|
||||
android:maxLines="1"
|
||||
android:layout_margin="20dp" />
|
||||
|
||||
<ImageView
|
||||
android:importantForAccessibility="no"
|
||||
android:id="@+id/selected_thumbnail"
|
||||
@@ -124,7 +137,9 @@
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:layout_marginBottom="4dp"
|
||||
android:textAppearance="?attr/textAppearanceHeadline1"
|
||||
android:textAppearance="?attr/textAppearanceHeadlineSmall"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible"
|
||||
app:layout_constraintTop_toBottomOf="@+id/icon_layout"
|
||||
app:layout_constraintBottom_toTopOf="@+id/note"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
@@ -139,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"
|
||||
@@ -167,34 +181,53 @@
|
||||
android:id="@+id/balance"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_vertical"
|
||||
android:layout_marginStart="8dp"
|
||||
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"
|
||||
app:layout_constraintTop_toBottomOf="@+id/info_divider"
|
||||
app:layout_constraintBottom_toTopOf="@+id/expiry"
|
||||
app:layout_constraintBottom_toTopOf="@+id/validFrom"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
tools:visibility="visible"
|
||||
tools:text="525 points"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/expiry"
|
||||
android:id="@+id/validFrom"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_vertical"
|
||||
android:layout_marginStart="8dp"
|
||||
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"
|
||||
app:drawableLeftCompat="@drawable/ic_valid_from_24dp"
|
||||
android:drawablePadding="4dp"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintTop_toBottomOf="@+id/balance"
|
||||
app:layout_constraintBottom_toTopOf="@+id/expiry"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
tools:visibility="visible"
|
||||
tools:text="Today"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/expiry"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_vertical"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:layout_marginBottom="4dp"
|
||||
android:textAppearance="?attr/textAppearanceBody2"
|
||||
app:drawableLeftCompat="@drawable/ic_baseline_access_time_24"
|
||||
android:drawablePadding="4dp"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintTop_toBottomOf="@+id/validFrom"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
|
||||
@@ -6,215 +6,200 @@
|
||||
android:id="@+id/coordinator_layout"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="fill_parent"
|
||||
android:fitsSystemWindows="true">
|
||||
|
||||
android:fitsSystemWindows="false">
|
||||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:id="@+id/app_bar_layout"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@android:color/transparent"
|
||||
android:clipChildren="false"
|
||||
android:clipToPadding="false"
|
||||
android:fitsSystemWindows="true"
|
||||
android:weightSum="1.0">
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
android:id="@+id/toolbar_landscape"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="?actionBarSize"
|
||||
android:background="@android:color/transparent"
|
||||
android:fitsSystemWindows="false"
|
||||
android:paddingTop="0dp"
|
||||
android:visibility="gone"
|
||||
app:contentInsetStart="72.0dip"
|
||||
app:layout_collapseMode="pin" />
|
||||
android:id="@+id/toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
style="?attr/toolbarStyle" />
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
<com.google.android.material.appbar.CollapsingToolbarLayout
|
||||
android:id="@+id/collapsingToolbarLayout"
|
||||
android:layout_width="fill_parent"
|
||||
<LinearLayout
|
||||
android:id="@+id/container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginTop="?attr/actionBarSize"
|
||||
android:layout_marginBottom="100dp"
|
||||
android:orientation="vertical"
|
||||
android:layout_marginStart="0dp"
|
||||
android:layout_marginEnd="0dp">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/icon_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:clipChildren="false"
|
||||
android:clipToPadding="false"
|
||||
android:minHeight="56.0dip"
|
||||
app:contentScrim="?colorPrimary"
|
||||
app:expandedTitleGravity="top"
|
||||
app:expandedTitleMarginEnd="64dp"
|
||||
app:expandedTitleMarginStart="48dp">
|
||||
android:layout_marginBottom="20dp">
|
||||
|
||||
<androidx.appcompat.widget.AppCompatTextView
|
||||
android:id="@+id/storeName"
|
||||
<Space
|
||||
android:layout_width="50dp"
|
||||
android:layout_height="0dp"/>
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1">
|
||||
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:id="@+id/icon_holder"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
app:cardCornerRadius="8dp"
|
||||
android:layout_margin="10dp"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintDimensionRatio="85.6f:53.98f">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/icon_image"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/icon_text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:textStyle="bold"
|
||||
app:autoSizeTextType="uniform"
|
||||
app:autoSizeMinTextSize="12sp"
|
||||
app:autoSizeMaxTextSize="100sp"
|
||||
app:autoSizeStepGranularity="2sp"
|
||||
android:gravity="center"
|
||||
android:maxLines="1"
|
||||
android:layout_margin="20dp" />
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<Space
|
||||
android:layout_width="50dp"
|
||||
android:layout_height="0dp"/>
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginStart="0dp"
|
||||
android:layout_marginEnd="0dp"
|
||||
android:layoutDirection="ltr">
|
||||
|
||||
<!-- We don't use these buttons for Talkback -->
|
||||
<ImageButton
|
||||
android:importantForAccessibility="no"
|
||||
android:id="@+id/main_left_button"
|
||||
android:layout_width="50dp"
|
||||
android:layout_height="match_parent"
|
||||
app:srcCompat="@drawable/ic_baseline_chevron_left_24"
|
||||
android:background="@android:color/transparent"/>
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/card_holder"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1">
|
||||
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:id="@+id/main_card_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:cardCornerRadius="8dp"
|
||||
android:layout_margin="10dp"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_margin="4dp"
|
||||
android:orientation="vertical">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/main_image"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/card_id_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="@dimen/text_size_large"
|
||||
android:ellipsize="end"
|
||||
android:singleLine="true"
|
||||
android:gravity="center"/>
|
||||
</LinearLayout>
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<!-- We don't use these buttons for Talkback -->
|
||||
<ImageButton
|
||||
android:importantForAccessibility="no"
|
||||
android:id="@+id/main_right_button"
|
||||
android:layout_width="50dp"
|
||||
android:layout_height="match_parent"
|
||||
app:srcCompat="@drawable/ic_baseline_chevron_right_24"
|
||||
android:background="@android:color/transparent"/>
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/fullscreen_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<ImageView
|
||||
android:importantForAccessibility="no"
|
||||
android:id="@+id/fullscreen_image"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toTopOf="@+id/scaler_guideline"/>
|
||||
|
||||
<androidx.constraintlayout.widget.Guideline
|
||||
android:id="@+id/scaler_guideline"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
app:layout_constraintGuide_percent="0.5"/>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintBottom_toTopOf="@+id/fullscreen_button_minimize"
|
||||
android:layout_marginBottom="25dp"
|
||||
android:layout_marginStart="15.0dip"
|
||||
android:layout_marginEnd="15.0dip">
|
||||
|
||||
<TextView
|
||||
android:importantForAccessibility="no"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:text="@string/height"/>
|
||||
|
||||
<SeekBar
|
||||
android:id="@+id/barcode_scaler"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:layout_marginTop="?actionBarSize"
|
||||
android:layout_marginBottom="?actionBarSize"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:textAlignment="center"
|
||||
android:textColor="@android:color/white"
|
||||
android:textSize="40sp"
|
||||
app:layout_collapseMode="parallax" />
|
||||
android:contentDescription="@string/setBarcodeHeight"
|
||||
android:max="100" />
|
||||
</LinearLayout>
|
||||
|
||||
<androidx.appcompat.widget.Toolbar
|
||||
android:id="@id/toolbar"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="?actionBarSize"
|
||||
android:background="@android:color/transparent"
|
||||
app:contentInsetStart="72.0dip"
|
||||
app:layout_collapseMode="pin" />
|
||||
|
||||
</com.google.android.material.appbar.CollapsingToolbarLayout>
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/icon_image"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_alignParentTop="true"
|
||||
android:fitsSystemWindows="true"
|
||||
android:scaleType="centerCrop"
|
||||
app:srcCompat="@drawable/ic_launcher_foreground"
|
||||
tools:ignore="ContentDescription" />
|
||||
|
||||
<FrameLayout
|
||||
android:clipChildren="false"
|
||||
android:clipToPadding="false"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="fill_parent"
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/mainLayout"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<androidx.constraintlayout.widget.Guideline
|
||||
android:id="@+id/centerGuideline"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
app:layout_constraintGuide_percent="0.5"/>
|
||||
|
||||
<androidx.constraintlayout.widget.Guideline
|
||||
android:id="@+id/scalerGuideline"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
app:layout_constraintGuide_percent="0.75"/>
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/maximizeButton"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="40dp"
|
||||
android:layout_marginStart="15.0dip"
|
||||
android:layout_marginTop="10dp"
|
||||
android:layout_marginEnd="15.0dip"
|
||||
android:background="?attr/colorPrimary"
|
||||
android:contentDescription="@string/moveBarcodeToTopOfScreen"
|
||||
android:padding="0dp"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toTopOf="@+id/mainImage"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:srcCompat="@drawable/ic_baseline_arrow_drop_up_24"
|
||||
app:tint="?attr/colorOnPrimary"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/mainImage"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:orientation="horizontal"
|
||||
android:layout_marginTop="10dp"
|
||||
android:layout_marginBottom="10dp"
|
||||
android:layout_marginStart="15.0dip"
|
||||
android:layout_marginEnd="15.0dip"
|
||||
app:layout_constraintBottom_toTopOf="@+id/centerGuideline"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/maximizeButton"/>
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/minimizeButton"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="40dp"
|
||||
android:layout_marginStart="15.0dip"
|
||||
android:layout_marginTop="10dp"
|
||||
android:layout_marginEnd="15.0dip"
|
||||
android:background="?attr/colorPrimary"
|
||||
android:contentDescription="@string/moveBarcodeToCenterOfScreen"
|
||||
android:padding="0dp"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/mainImage"
|
||||
app:srcCompat="@drawable/ic_baseline_arrow_drop_down_24"
|
||||
app:tint="?attr/colorOnPrimary"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/dotIndicator"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_marginStart="15.0dip"
|
||||
android:layout_marginTop="10dp"
|
||||
android:layout_marginEnd="15.0dip"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/minimizeButton"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<SeekBar
|
||||
android:id="@+id/barcodeScaler"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="@dimen/inputPadding"
|
||||
android:layout_marginStart="15.0dip"
|
||||
android:layout_marginEnd="15.0dip"
|
||||
android:contentDescription="@string/set_scale"
|
||||
android:max="100"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/scalerGuideline" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/cardIdView"
|
||||
android:enabled="true"
|
||||
android:textIsSelectable="true"
|
||||
android:focusable="true"
|
||||
android:longClickable="true"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginLeft="10.0dip"
|
||||
android:layout_marginRight="10.0dip"
|
||||
android:paddingBottom="80dp"
|
||||
app:layout_constraintTop_toBottomOf="@+id/dotIndicator"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
android:textAlignment="center"
|
||||
app:autoSizeTextType="uniform"
|
||||
app:autoSizeMinTextSize="@dimen/singleCardCardIdTextSizeMin"
|
||||
app:autoSizeMaxTextSize="@dimen/singleCardCardIdTextSizeMax"
|
||||
android:ellipsize="end"/>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<View
|
||||
android:id="@+id/drop_shadow_actionbar"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="5.0dip"
|
||||
android:layout_gravity="top"/>
|
||||
</FrameLayout>
|
||||
<ImageButton
|
||||
android:id="@+id/fullscreen_button_minimize"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:src="@drawable/ic_baseline_expand_more_24"
|
||||
android:tooltipText="@string/moveDown"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
android:layout_marginBottom="25dp"
|
||||
android:background="@android:color/transparent"/>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<com.google.android.material.bottomappbar.BottomAppBar
|
||||
android:id="@+id/bottom_app_bar"
|
||||
@@ -229,53 +214,70 @@
|
||||
app:contentInsetEnd="0dp"
|
||||
app:fabAlignmentMode="center">
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/button_previous"
|
||||
android:layout_width="wrap_content"
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="left"
|
||||
android:paddingStart="12dp"
|
||||
android:paddingEnd="12dp"
|
||||
android:background="@android:color/transparent"
|
||||
android:src="@drawable/ic_baseline_chevron_left_24"
|
||||
android:tooltipText="@string/previousCard"
|
||||
android:visibility="gone" />
|
||||
android:layoutDirection="ltr">
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/button_show_info"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="start"
|
||||
android:paddingStart="12dp"
|
||||
android:paddingEnd="12dp"
|
||||
android:background="@android:color/transparent"
|
||||
android:src="@drawable/ic_baseline_info_24"
|
||||
android:tooltipText="@string/showMoreInfo"
|
||||
android:visibility="gone" />
|
||||
<ImageButton
|
||||
android:id="@+id/bottom_app_bar_previous_button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:paddingStart="12dp"
|
||||
android:paddingEnd="12dp"
|
||||
android:background="@android:color/transparent"
|
||||
android:src="@drawable/ic_baseline_chevron_left_24"
|
||||
android:tooltipText="@string/previousCard"
|
||||
android:visibility="gone" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/button_next"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="right"
|
||||
android:paddingStart="12dp"
|
||||
android:paddingEnd="12dp"
|
||||
android:background="@android:color/transparent"
|
||||
android:src="@drawable/ic_baseline_chevron_right_24"
|
||||
android:tooltipText="@string/nextCard"
|
||||
android:visibility="gone" />
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"
|
||||
android:orientation="horizontal"
|
||||
android:layoutDirection="locale">
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/button_update_balance"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="end"
|
||||
android:background="@android:color/transparent"
|
||||
android:paddingStart="12dp"
|
||||
android:paddingEnd="12dp"
|
||||
android:src="@drawable/ic_baseline_shopping_cart_24"
|
||||
android:tooltipText="@string/updateBalance"
|
||||
android:visibility="gone" />
|
||||
<ImageButton
|
||||
android:id="@+id/bottom_app_bar_info_button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:paddingStart="12dp"
|
||||
android:paddingEnd="12dp"
|
||||
android:background="@android:color/transparent"
|
||||
android:src="@drawable/ic_baseline_info_24"
|
||||
android:tooltipText="@string/showMoreInfo"
|
||||
android:visibility="gone" />
|
||||
|
||||
<Space
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/bottom_app_bar_update_balance_button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:paddingStart="12dp"
|
||||
android:paddingEnd="12dp"
|
||||
android:background="@android:color/transparent"
|
||||
android:src="@drawable/ic_baseline_shopping_cart_24"
|
||||
android:tooltipText="@string/updateBalance"
|
||||
android:visibility="gone" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/bottom_app_bar_next_button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:paddingStart="12dp"
|
||||
android:paddingEnd="12dp"
|
||||
android:background="@android:color/transparent"
|
||||
android:src="@drawable/ic_baseline_chevron_right_24"
|
||||
android:tooltipText="@string/nextCard"
|
||||
android:visibility="gone" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</com.google.android.material.bottomappbar.BottomAppBar>
|
||||
|
||||
|
||||
8
app/src/main/res/layout/preference_material_switch.xml
Normal file
8
app/src/main/res/layout/preference_material_switch.xml
Normal file
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<com.google.android.material.materialswitch.MaterialSwitch xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/switchWidget"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@null"
|
||||
android:clickable="false"
|
||||
android:focusable="false" />
|
||||
@@ -10,12 +10,13 @@
|
||||
|
||||
<CheckBox
|
||||
android:id="@+id/checkBox_reverse"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginLeft="20dp"
|
||||
android:paddingLeft="20dp"
|
||||
android:layout_marginStart="20dp"
|
||||
android:paddingStart="20dp"
|
||||
android:text="@string/reverse"
|
||||
android:textSize="19sp"/>
|
||||
android:textSize="16sp"
|
||||
android:textColor="?attr/colorOnSurfaceVariant"/>
|
||||
|
||||
</LinearLayout>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
<menu 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"
|
||||
tools:context="protect.card_locker.MainActivity">
|
||||
<item
|
||||
android:id="@+id/action_search"
|
||||
android:title="@string/action_search"
|
||||
android:icon="@drawable/ic_search_white"
|
||||
app:actionViewClass="androidx.appcompat.widget.SearchView"
|
||||
app:showAsAction="always|collapseActionView"
|
||||
android:visible="false"/>
|
||||
<item
|
||||
android:id="@+id/action_unfold"
|
||||
android:title="@string/action_hide_details"
|
||||
android:icon="@drawable/ic_baseline_unfold_less_24"
|
||||
app:showAsAction="always"
|
||||
android:visible="false"/>
|
||||
<item
|
||||
android:id="@+id/action_sort"
|
||||
android:title="@string/sort"
|
||||
android:icon="@drawable/ic_baseline_sort_24"
|
||||
app:showAsAction="always"
|
||||
android:visible="false"/>
|
||||
</menu>
|
||||
@@ -1,9 +1,9 @@
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
<item
|
||||
android:id="@+id/action_unfold"
|
||||
android:title="@string/action_hide_details"
|
||||
android:icon="@drawable/ic_baseline_unfold_less_24"
|
||||
android:id="@+id/action_display_options"
|
||||
android:title="@string/action_display_options"
|
||||
android:icon="@drawable/baseline_visibility_24"
|
||||
app:showAsAction="always"
|
||||
android:visible="true"/>
|
||||
</menu>
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
<item
|
||||
android:id="@+id/action_delete"
|
||||
android:icon="@drawable/ic_delete_white_24dp"
|
||||
android:title="@string/delete"
|
||||
app:showAsAction="always"/>
|
||||
</menu>
|
||||
@@ -4,13 +4,13 @@
|
||||
|
||||
<item
|
||||
android:id="@+id/action_share"
|
||||
android:icon="@drawable/ic_share_white"
|
||||
android:icon="@drawable/ic_share"
|
||||
android:title="@string/share"
|
||||
app:showAsAction="always"/>
|
||||
|
||||
<item
|
||||
android:id="@+id/action_star_unstar"
|
||||
android:icon="@drawable/ic_unstarred_white"
|
||||
android:icon="@drawable/ic_unstarred"
|
||||
android:title="@string/star"
|
||||
app:showAsAction="always" />
|
||||
|
||||
|
||||
@@ -10,9 +10,9 @@
|
||||
app:showAsAction="always|collapseActionView"
|
||||
android:visible="false"/>
|
||||
<item
|
||||
android:id="@+id/action_unfold"
|
||||
android:title="@string/action_hide_details"
|
||||
android:icon="@drawable/ic_baseline_unfold_less_24"
|
||||
android:id="@+id/action_display_options"
|
||||
android:title="@string/action_display_options"
|
||||
android:icon="@drawable/baseline_visibility_24"
|
||||
app:showAsAction="always"
|
||||
android:visible="false"/>
|
||||
<item
|
||||
@@ -26,10 +26,6 @@
|
||||
android:icon="@drawable/ic_folder_white"
|
||||
android:title="@string/groups"
|
||||
app:showAsAction="never"/>
|
||||
<item
|
||||
android:id="@+id/action_archived"
|
||||
android:title="@string/archiveList"
|
||||
app:showAsAction="never"/>
|
||||
<item
|
||||
android:id="@+id/action_import_export"
|
||||
android:icon="@drawable/ic_import_export_white_24dp"
|
||||
|
||||
2
app/src/main/res/raw/.gitignore
vendored
Normal file
2
app/src/main/res/raw/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
changelog.md
|
||||
privacy.md
|
||||
@@ -5,56 +5,73 @@ Allan Nordhøy
|
||||
solokot
|
||||
Heimen Stoffels
|
||||
Oğuz Ersen
|
||||
FC (Fay) Stegerman
|
||||
Katharine Chui
|
||||
mondstern
|
||||
IllusiveMan196
|
||||
StoyanDimitrov
|
||||
Altonss
|
||||
Petr Novák
|
||||
SlavekB
|
||||
Gediminas Murauskas
|
||||
mondstern
|
||||
StoyanDimitrov
|
||||
IllusiveMan196
|
||||
Altonss
|
||||
Michael Moroni
|
||||
Gediminas Murauskas
|
||||
Petr Novák
|
||||
Joel A
|
||||
laralem
|
||||
Taco
|
||||
pfaffenrodt
|
||||
Eric
|
||||
Nyatsuki
|
||||
laralem
|
||||
HudobniVolk
|
||||
Samantaz Fox
|
||||
arno-github
|
||||
Ankit Tiwari
|
||||
Sergio Paredes
|
||||
Clxff H3r4ld0
|
||||
Aayush Gupta
|
||||
Balázs Meskó
|
||||
huuhaa
|
||||
gallegonovato
|
||||
arshbeerSingh
|
||||
Projjal Moitra
|
||||
Quentin PAGÈS
|
||||
Miha Frangež
|
||||
Arnis Jaundzeikars
|
||||
sr093906
|
||||
Giovanni Donisi
|
||||
Alexander Ivanov
|
||||
arshbeerSingh
|
||||
Denis Shilin
|
||||
Freddo espresso
|
||||
Ziad OUALHADJ
|
||||
Silvério Santos
|
||||
Miha Frangež
|
||||
Eryk Michalak
|
||||
Arnis Jaundžeikars
|
||||
Dan
|
||||
sr093906
|
||||
mdvhimself
|
||||
Jiri Grönroos
|
||||
Katarzyna
|
||||
Maciej Błędkowski
|
||||
echo r"0xX4H" | rev
|
||||
Magnitudee
|
||||
Olivia (Zoe)
|
||||
betsythefc
|
||||
Silvério Santos
|
||||
waffshappen
|
||||
Robin
|
||||
Eric
|
||||
Alexander Ivanov
|
||||
ati3
|
||||
Giovanni
|
||||
enolp
|
||||
Evgeniy Khramov
|
||||
Jane Kong
|
||||
Jean Mareilles
|
||||
Jean-Luc Tibaux
|
||||
José Rebelo
|
||||
K. Herbert
|
||||
Lisa A.
|
||||
Mawuena M. KODZO A.
|
||||
Max
|
||||
Still Hsu
|
||||
rudy3
|
||||
Reza
|
||||
Still / Azaka
|
||||
String E. Fighter
|
||||
Tapu
|
||||
Yurical
|
||||
rr-vesp
|
||||
yangyangdaji
|
||||
丛林意志
|
||||
alajemba-vik
|
||||
/usr/local/ΕΨΗΕΛΩΝ
|
||||
Adolfo Jayme-Barrientos
|
||||
@@ -63,111 +80,204 @@ KovalevArtem
|
||||
Artem M.
|
||||
Astrohops1
|
||||
BMN
|
||||
balaraz
|
||||
BootVirtual
|
||||
Bottan Hermawan
|
||||
zChiip
|
||||
Clonewayx
|
||||
D. Domig
|
||||
Danylo Lystopadov
|
||||
Diego
|
||||
Eudes-alencar
|
||||
Fede Pujol
|
||||
Jean-Luc Tibaux
|
||||
FineFindus
|
||||
francescbassas
|
||||
Jason Li
|
||||
Jesse Davids
|
||||
Kamborio
|
||||
Kis Dominik
|
||||
Lukas Grassauer
|
||||
Luna Jernberg
|
||||
Marnick L'Eau
|
||||
Michalis
|
||||
Michał
|
||||
Milo Ivir
|
||||
Mohamed A. Salah
|
||||
Neko Nekowazarashi
|
||||
Yatoku
|
||||
the7thNightmare
|
||||
Quang Trung
|
||||
Rishi Agarwal
|
||||
Rosdyana Kusuma
|
||||
Sabri Ünal
|
||||
umoenks
|
||||
Simon Rusinov
|
||||
Siriusmart
|
||||
Mritunjay
|
||||
Tarik Dzambic
|
||||
Thomas Bertels
|
||||
Thomas Cruveilher
|
||||
Tian Jiale
|
||||
Tong Liu
|
||||
Tymofii Lytvynenko
|
||||
Wanath
|
||||
Younes Bouhouche
|
||||
Runner
|
||||
ce i moa
|
||||
enescan201
|
||||
Frablock
|
||||
inesre
|
||||
lgasp
|
||||
notlin4
|
||||
phlostically
|
||||
pokeghost
|
||||
sal0max
|
||||
Ágata Leuck
|
||||
BmBKun
|
||||
NamHyeonjeong
|
||||
Aditya Das
|
||||
Asier
|
||||
asier123123131
|
||||
Kevin Sicong Jiang
|
||||
Tomer Ben-Rachel
|
||||
Tom Sawyer
|
||||
tfuxu
|
||||
Ahmed Saleh
|
||||
Airat
|
||||
Tapwaterisokey
|
||||
Alexandra-Ioana Moroz
|
||||
sNiXx
|
||||
Angela Enogieru
|
||||
AnimeshChatterjee1
|
||||
Animesh Chatterjee
|
||||
Artūras Kalenda
|
||||
Ashish Yadav
|
||||
Aya Elsaadany
|
||||
Aya
|
||||
Biren
|
||||
Booc Sylvan
|
||||
Brage Nesteby Reitan
|
||||
Cap Amr Karam
|
||||
Carlo Maria Cuoghi Barbagli
|
||||
ChaoticNeutralCzech
|
||||
ChengCheng
|
||||
CherryMonster222
|
||||
Colgrave
|
||||
Csaba
|
||||
djcand
|
||||
Mylou53
|
||||
Daniel Sych
|
||||
danieluhrinyi
|
||||
Daniele Tricoli
|
||||
Kasina Dheeraj
|
||||
Donno
|
||||
Evgeniy Khramov
|
||||
Reihan
|
||||
Erik Spjelkavik
|
||||
Flav
|
||||
Franciszek Stefan
|
||||
Gael Caraballo
|
||||
Giacomo Alessandroni
|
||||
Govind S Nair
|
||||
Grzegorz
|
||||
gneiss15
|
||||
Hamustra Scans
|
||||
helzubair
|
||||
HowITsDone
|
||||
Hubert Maciejewicz
|
||||
Izzy
|
||||
Jacek
|
||||
Jacopo Gennaro Esposito
|
||||
Jean Mareilles
|
||||
Jean-Baptiste
|
||||
人工知能
|
||||
Kung-chih
|
||||
Karvjorm
|
||||
polar
|
||||
krkk
|
||||
Kristoffer Grundström
|
||||
Laura Ferraz
|
||||
Lionel HANNEQUIN
|
||||
Lucas da Costa
|
||||
bittin
|
||||
almir992
|
||||
Manan Jhaveri
|
||||
Marco
|
||||
BRBsoup
|
||||
Mateo Gomez
|
||||
Mattia
|
||||
Md. Al-Amin
|
||||
Michael Gangolf
|
||||
Milan Šalka
|
||||
3DN1M
|
||||
Minecraft boom
|
||||
Mobashir Raihan
|
||||
Moi Toi
|
||||
DiCeYMaYo
|
||||
DivideEtImpera
|
||||
Nicolas
|
||||
Nosnahc
|
||||
osamaqw
|
||||
pa4k
|
||||
pbeckmann
|
||||
Peer Beckmann
|
||||
vandman
|
||||
Piotr Strebski
|
||||
Piotr Zet
|
||||
Poorva Patidar
|
||||
Quang Nguyen
|
||||
Ratnesh
|
||||
Reza
|
||||
Rohan Babbar
|
||||
Ronak Upadhyay
|
||||
Rose Liverman
|
||||
SKULD
|
||||
Sabrina
|
||||
Salem Malus
|
||||
Samarth Asthan
|
||||
tatyhub
|
||||
Shailendra Maurya
|
||||
SilverFS
|
||||
Simone Dotto
|
||||
Subhashish Anand
|
||||
TenTraicion
|
||||
Subhradeep Bera
|
||||
Swayam Khare
|
||||
SziaTomi
|
||||
Mehedi Hasan
|
||||
Tim Trek
|
||||
Titas Pažereckas
|
||||
Tom Sawyer
|
||||
atakujonc
|
||||
tkraljevic
|
||||
Tony C
|
||||
Tymofii Lytvynenko
|
||||
Vancha March
|
||||
tyap-lyap-ivprod
|
||||
Waldemar Stoczkowski
|
||||
Wiktor Kwapisiewicz
|
||||
Yevgeny M
|
||||
Yusril A
|
||||
ahmed-awad26
|
||||
Avik Kundu
|
||||
ayuyydev
|
||||
diksha-2911
|
||||
essys
|
||||
gbonaspetti
|
||||
gittyboy-cell
|
||||
huang ivan
|
||||
lassr8
|
||||
liva
|
||||
lucafont2
|
||||
mtrmirez
|
||||
michaelpratana
|
||||
opsik
|
||||
pesta007
|
||||
polarhun
|
||||
pooyanazari
|
||||
psa-jforestier
|
||||
z369369
|
||||
sergio
|
||||
skauVictor
|
||||
080502
|
||||
Marcus
|
||||
techwebpd
|
||||
tjw123hh
|
||||
Truestorybaby
|
||||
tygyh
|
||||
unstartdev
|
||||
luoyang3
|
||||
wmilan 17
|
||||
MeH762
|
||||
يوسف لطفي
|
||||
しいたけ
|
||||
元气
|
||||
JaeBeom An
|
||||
JungHee Lee
|
||||
|
||||
696
app/src/main/res/raw/license.html
Normal file
696
app/src/main/res/raw/license.html
Normal file
@@ -0,0 +1,696 @@
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
|
||||
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
|
||||
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||
<title>GNU General Public License v3.0 - GNU Project - Free Software Foundation (FSF)</title>
|
||||
<link rel="alternate" type="application/rdf+xml"
|
||||
href="http://www.gnu.org/licenses/gpl-3.0.rdf" />
|
||||
</head>
|
||||
<body>
|
||||
<h3 style="text-align: center;">GNU GENERAL PUBLIC LICENSE</h3>
|
||||
<p style="text-align: center;">Version 3, 29 June 2007</p>
|
||||
|
||||
<p>Copyright © 2007 Free Software Foundation, Inc.
|
||||
<<a href="https://fsf.org/">https://fsf.org/</a>></p><p>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.</p>
|
||||
|
||||
<h4 id="preamble">Preamble</h4>
|
||||
|
||||
<p>The GNU General Public License is a free, copyleft license for
|
||||
software and other kinds of works.</p>
|
||||
|
||||
<p>The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
the GNU General Public License is intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users. We, the Free Software Foundation, use the
|
||||
GNU General Public License for most of our software; it applies also to
|
||||
any other work released this way by its authors. You can apply it to
|
||||
your programs, too.</p>
|
||||
|
||||
<p>When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.</p>
|
||||
|
||||
<p>To protect your rights, we need to prevent others from denying you
|
||||
these rights or asking you to surrender the rights. Therefore, you have
|
||||
certain responsibilities if you distribute copies of the software, or if
|
||||
you modify it: responsibilities to respect the freedom of others.</p>
|
||||
|
||||
<p>For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must pass on to the recipients the same
|
||||
freedoms that you received. You must make sure that they, too, receive
|
||||
or can get the source code. And you must show them these terms so they
|
||||
know their rights.</p>
|
||||
|
||||
<p>Developers that use the GNU GPL protect your rights with two steps:
|
||||
(1) assert copyright on the software, and (2) offer you this License
|
||||
giving you legal permission to copy, distribute and/or modify it.</p>
|
||||
|
||||
<p>For the developers' and authors' protection, the GPL clearly explains
|
||||
that there is no warranty for this free software. For both users' and
|
||||
authors' sake, the GPL requires that modified versions be marked as
|
||||
changed, so that their problems will not be attributed erroneously to
|
||||
authors of previous versions.</p>
|
||||
|
||||
<p>Some devices are designed to deny users access to install or run
|
||||
modified versions of the software inside them, although the manufacturer
|
||||
can do so. This is fundamentally incompatible with the aim of
|
||||
protecting users' freedom to change the software. The systematic
|
||||
pattern of such abuse occurs in the area of products for individuals to
|
||||
use, which is precisely where it is most unacceptable. Therefore, we
|
||||
have designed this version of the GPL to prohibit the practice for those
|
||||
products. If such problems arise substantially in other domains, we
|
||||
stand ready to extend this provision to those domains in future versions
|
||||
of the GPL, as needed to protect the freedom of users.</p>
|
||||
|
||||
<p>Finally, every program is threatened constantly by software patents.
|
||||
States should not allow patents to restrict development and use of
|
||||
software on general-purpose computers, but in those that do, we wish to
|
||||
avoid the special danger that patents applied to a free program could
|
||||
make it effectively proprietary. To prevent this, the GPL assures that
|
||||
patents cannot be used to render the program non-free.</p>
|
||||
|
||||
<p>The precise terms and conditions for copying, distribution and
|
||||
modification follow.</p>
|
||||
|
||||
<h4 id="terms">TERMS AND CONDITIONS</h4>
|
||||
|
||||
<h5 id="section0">0. Definitions.</h5>
|
||||
|
||||
<p>“This License” refers to version 3 of the GNU General Public License.</p>
|
||||
|
||||
<p>“Copyright” also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.</p>
|
||||
|
||||
<p>“The Program” refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as “you”. “Licensees” and
|
||||
“recipients” may be individuals or organizations.</p>
|
||||
|
||||
<p>To “modify” a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a “modified version” of the
|
||||
earlier work or a work “based on” the earlier work.</p>
|
||||
|
||||
<p>A “covered work” means either the unmodified Program or a work based
|
||||
on the Program.</p>
|
||||
|
||||
<p>To “propagate” a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.</p>
|
||||
|
||||
<p>To “convey” a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.</p>
|
||||
|
||||
<p>An interactive user interface displays “Appropriate Legal Notices”
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.</p>
|
||||
|
||||
<h5 id="section1">1. Source Code.</h5>
|
||||
|
||||
<p>The “source code” for a work means the preferred form of the work
|
||||
for making modifications to it. “Object code” means any non-source
|
||||
form of a work.</p>
|
||||
|
||||
<p>A “Standard Interface” means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.</p>
|
||||
|
||||
<p>The “System Libraries” of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
“Major Component”, in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.</p>
|
||||
|
||||
<p>The “Corresponding Source” for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.</p>
|
||||
|
||||
<p>The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.</p>
|
||||
|
||||
<p>The Corresponding Source for a work in source code form is that
|
||||
same work.</p>
|
||||
|
||||
<h5 id="section2">2. Basic Permissions.</h5>
|
||||
|
||||
<p>All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.</p>
|
||||
|
||||
<p>You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.</p>
|
||||
|
||||
<p>Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.</p>
|
||||
|
||||
<h5 id="section3">3. Protecting Users' Legal Rights From Anti-Circumvention Law.</h5>
|
||||
|
||||
<p>No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.</p>
|
||||
|
||||
<p>When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.</p>
|
||||
|
||||
<h5 id="section4">4. Conveying Verbatim Copies.</h5>
|
||||
|
||||
<p>You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.</p>
|
||||
|
||||
<p>You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.</p>
|
||||
|
||||
<h5 id="section5">5. Conveying Modified Source Versions.</h5>
|
||||
|
||||
<p>You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:</p>
|
||||
|
||||
<ul>
|
||||
<li>a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.</li>
|
||||
|
||||
<li>b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
“keep intact all notices”.</li>
|
||||
|
||||
<li>c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.</li>
|
||||
|
||||
<li>d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.</li>
|
||||
</ul>
|
||||
|
||||
<p>A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
“aggregate” if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.</p>
|
||||
|
||||
<h5 id="section6">6. Conveying Non-Source Forms.</h5>
|
||||
|
||||
<p>You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:</p>
|
||||
|
||||
<ul>
|
||||
<li>a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.</li>
|
||||
|
||||
<li>b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.</li>
|
||||
|
||||
<li>c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.</li>
|
||||
|
||||
<li>d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.</li>
|
||||
|
||||
<li>e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.</li>
|
||||
</ul>
|
||||
|
||||
<p>A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.</p>
|
||||
|
||||
<p>A “User Product” is either (1) a “consumer product”, which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, “normally used” refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.</p>
|
||||
|
||||
<p>“Installation Information” for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.</p>
|
||||
|
||||
<p>If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).</p>
|
||||
|
||||
<p>The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.</p>
|
||||
|
||||
<p>Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.</p>
|
||||
|
||||
<h5 id="section7">7. Additional Terms.</h5>
|
||||
|
||||
<p>“Additional permissions” are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.</p>
|
||||
|
||||
<p>When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.</p>
|
||||
|
||||
<p>Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:</p>
|
||||
|
||||
<ul>
|
||||
<li>a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or</li>
|
||||
|
||||
<li>b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or</li>
|
||||
|
||||
<li>c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or</li>
|
||||
|
||||
<li>d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or</li>
|
||||
|
||||
<li>e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or</li>
|
||||
|
||||
<li>f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.</li>
|
||||
</ul>
|
||||
|
||||
<p>All other non-permissive additional terms are considered “further
|
||||
restrictions” within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.</p>
|
||||
|
||||
<p>If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.</p>
|
||||
|
||||
<p>Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.</p>
|
||||
|
||||
<h5 id="section8">8. Termination.</h5>
|
||||
|
||||
<p>You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).</p>
|
||||
|
||||
<p>However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.</p>
|
||||
|
||||
<p>Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.</p>
|
||||
|
||||
<p>Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.</p>
|
||||
|
||||
<h5 id="section9">9. Acceptance Not Required for Having Copies.</h5>
|
||||
|
||||
<p>You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.</p>
|
||||
|
||||
<h5 id="section10">10. Automatic Licensing of Downstream Recipients.</h5>
|
||||
|
||||
<p>Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.</p>
|
||||
|
||||
<p>An “entity transaction” is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.</p>
|
||||
|
||||
<p>You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.</p>
|
||||
|
||||
<h5 id="section11">11. Patents.</h5>
|
||||
|
||||
<p>A “contributor” is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's “contributor version”.</p>
|
||||
|
||||
<p>A contributor's “essential patent claims” are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, “control” includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.</p>
|
||||
|
||||
<p>Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.</p>
|
||||
|
||||
<p>In the following three paragraphs, a “patent license” is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To “grant” such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.</p>
|
||||
|
||||
<p>If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. “Knowingly relying” means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.</p>
|
||||
|
||||
<p>If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.</p>
|
||||
|
||||
<p>A patent license is “discriminatory” if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.</p>
|
||||
|
||||
<p>Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.</p>
|
||||
|
||||
<h5 id="section12">12. No Surrender of Others' Freedom.</h5>
|
||||
|
||||
<p>If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.</p>
|
||||
|
||||
<h5 id="section13">13. Use with the GNU Affero General Public License.</h5>
|
||||
|
||||
<p>Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU Affero General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the special requirements of the GNU Affero General Public License,
|
||||
section 13, concerning interaction through a network will apply to the
|
||||
combination as such.</p>
|
||||
|
||||
<h5 id="section14">14. Revised Versions of this License.</h5>
|
||||
|
||||
<p>The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.</p>
|
||||
|
||||
<p>Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU General
|
||||
Public License “or any later version” applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.</p>
|
||||
|
||||
<p>If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.</p>
|
||||
|
||||
<p>Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.</p>
|
||||
|
||||
<h5 id="section15">15. Disclaimer of Warranty.</h5>
|
||||
|
||||
<p>THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM “AS IS” WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.</p>
|
||||
|
||||
<h5 id="section16">16. Limitation of Liability.</h5>
|
||||
|
||||
<p>IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.</p>
|
||||
|
||||
<h5 id="section17">17. Interpretation of Sections 15 and 16.</h5>
|
||||
|
||||
<p>If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.</p>
|
||||
|
||||
<p>END OF TERMS AND CONDITIONS</p>
|
||||
|
||||
<h4 id="howto">How to Apply These Terms to Your New Programs</h4>
|
||||
|
||||
<p>If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.</p>
|
||||
|
||||
<p>To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the “copyright” line and a pointer to where the full notice is found.</p>
|
||||
|
||||
<pre> <one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
</pre>
|
||||
|
||||
<p>Also add information on how to contact you by electronic and paper mail.</p>
|
||||
|
||||
<p>If the program does terminal interaction, make it output a short
|
||||
notice like this when it starts in an interactive mode:</p>
|
||||
|
||||
<pre> <program> Copyright (C) <year> <name of author>
|
||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
</pre>
|
||||
|
||||
<p>The hypothetical commands `show w' and `show c' should show the appropriate
|
||||
parts of the General Public License. Of course, your program's commands
|
||||
might be different; for a GUI interface, you would use an “about box”.</p>
|
||||
|
||||
<p>You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a “copyright disclaimer” for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU GPL, see
|
||||
<<a href="https://www.gnu.org/licenses/">https://www.gnu.org/licenses/</a>>.</p>
|
||||
|
||||
<p>The GNU General Public License does not permit incorporating your program
|
||||
into proprietary programs. If your program is a subroutine library, you
|
||||
may consider it more useful to permit linking proprietary applications with
|
||||
the library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License. But first, please read
|
||||
<<a href="https://www.gnu.org/licenses/why-not-lgpl.html">https://www.gnu.org/licenses/why-not-lgpl.html</a>>.</p>
|
||||
|
||||
</body></html>
|
||||
@@ -1,7 +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 stores "select _id,name,barcodeFormat from stores" > stocard_stores.csv
|
||||
|
||||
Only used for data portability reasons (ensuring importing works). Do NOT copy this anywhere else or use it for any purpose other than ensuring we can import a GDPR-provided export. We want to make sure this stays under fair use.
|
||||
File diff suppressed because it is too large
Load Diff
1
app/src/main/res/resources.properties
Normal file
1
app/src/main/res/resources.properties
Normal file
@@ -0,0 +1 @@
|
||||
unqualifiedResLocale=en-US
|
||||
@@ -1,45 +1,41 @@
|
||||
<?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_search">بحث</string>
|
||||
<string name="action_add">اضف</string>
|
||||
<string name="noGiftCards">اضغط علي زر + لاضافة بطاقة, او استورد بعض منهم من قائمة ال ⋮.</string>
|
||||
<string name="noMatchingGiftCards">لا يوجد نتائج. حاول تغيير كلمات البحث.</string>
|
||||
<string name="action_add">أضف</string>
|
||||
<string name="noGiftCards">اضغط على + زر الإضافة لاضافة بطاقة، او استورد من ⋮ القائمة.</string>
|
||||
<string name="noMatchingGiftCards">لا نتائج. حاول تغيير كلمات البحث.</string>
|
||||
<string name="storeName">اسم</string>
|
||||
<string name="note">مذكرة</string>
|
||||
<string name="cardId">بطاقة شخصية</string>
|
||||
<string name="cardId">معرّف البطاقة</string>
|
||||
<string name="barcodeType">نوع الباركود</string>
|
||||
<string name="barcodeNoBarcode">لا يوجد باركود</string>
|
||||
<string name="noBarcode">لا يوجد باركود</string>
|
||||
<string name="star">اضف الي المفضلة</string>
|
||||
<string name="star">أضف الي المفضلة</string>
|
||||
<string name="unstar">حذف من المفضلة</string>
|
||||
<string name="cancel">الغاء</string>
|
||||
<string name="cancel">إلغاء</string>
|
||||
<string name="save">حفظ</string>
|
||||
<string name="edit">تعديل</string>
|
||||
<string name="delete">مسح</string>
|
||||
<string name="confirm">تأكيد</string>
|
||||
<string name="deleteConfirmation">مسح هذة البطاقة نهائيا؟</string>
|
||||
<string name="ok">حسنا</string>
|
||||
<string name="copy_to_clipboard">نسخ البطاقة الشخصية الي الحافظة</string>
|
||||
<string name="copy_to_clipboard">نسخ البطاقة الشخصية الى الحافظة</string>
|
||||
<string name="share">شارك</string>
|
||||
<string name="sendLabel">ارسل…</string>
|
||||
<string name="editCardTitle">عدل بطاقة</string>
|
||||
<string name="editCardTitle">عدل البطاقة</string>
|
||||
<string name="addCardTitle">اضف بطاقة</string>
|
||||
<string name="scanCardBarcode">مسح باركود</string>
|
||||
<string name="cardShortcut">اختصار البطاقة</string>
|
||||
<string name="noCardsMessage">اضف بطاقة أولا</string>
|
||||
<string name="card_ids_copied">البطاقة(ات) الشخصية المنسوخة</string>
|
||||
<string name="barcodeImageDescriptionWithType">صورة <xliff:g>%s</xliff:g> باركود</string>
|
||||
<string name="noStoreError">لا يوجد اسم مدخل</string>
|
||||
<string name="noCardIdError">لا يوجد بطاقة شخصية مدخلة</string>
|
||||
<string name="noCardExistsError">لا يمكن العثور علي هذه البطاقة</string>
|
||||
<string name="noCardExistsError">لا يمكن العثور على هذه البطاقة</string>
|
||||
<string name="failedParsingImportUriError">لا يمكن تحليل الرابط المستورد</string>
|
||||
<string name="importExport">استيراد/تصدير</string>
|
||||
<string name="importExportHelp">عمل نسخ احتطياتي لبياناتك يسمح نقلها لجهاز اخر.</string>
|
||||
<string name="importFailed">لا يمكن عمل الاستيراد</string>
|
||||
<string name="importExportHelp">دعم بياناتك يسمح بنقلها إلى جهاز آخر.</string>
|
||||
<string name="importFailed">تعذر إجراء الاستيراد</string>
|
||||
<string name="exportSuccessfulTitle">متصدر</string>
|
||||
<string name="exportFailedTitle">فشل التصدير</string>
|
||||
<string name="exportFailed">لا يمكن عمل التصدير</string>
|
||||
<string name="noExternalStoragePermissionError">امنح التخزين الخارجي اذن لاستيراد وتصدير البيانات</string>
|
||||
<string name="exportOptionExplanation">ستتم كتابة البيانات في الموقع الذي تختاره.</string>
|
||||
<string name="importOptionFilesystemButton">من نظام الملفات</string>
|
||||
<string name="importOptionApplicationTitle">استخدم تطبيقًا آخر</string>
|
||||
@@ -49,22 +45,18 @@
|
||||
<string name="app_copyright_old">بناء على Loyalty Card Keychain
|
||||
\nحقوق النشر © 2016-2020 Branden Archer</string>
|
||||
<string name="app_license">البرمجيات الحرة متروكة الحقوق, ترخيص +GPLv3</string>
|
||||
<string name="app_revision_fmt">معلومات المراجعة: <xliff:g id="app_revision_url">%s</xliff:g></string>
|
||||
<string name="app_libraries">مكتبات الطرف الثالث الحرة: <xliff:g id="app_libraries_list">%s</xliff:g></string>
|
||||
<string name="selectBarcodeTitle">اختار الباركود</string>
|
||||
<string name="enterBarcodeInstructions">أدخل بطاقة هوية ، واختر نوع الباركود أدناه ، أو \"لا يوجد باركود\".</string>
|
||||
<string name="copy_to_clipboard_toast">تم نسخ بطاقة الهوية إلى الحافظة</string>
|
||||
<string name="thumbnailDescription">صورة مصغرة</string>
|
||||
<string name="starImage">نجم مفضل</string>
|
||||
<string name="settings">اعدادات</string>
|
||||
<string name="settings_category_title_ui">واجهة المستخدم</string>
|
||||
<string name="settings_light_theme">فاتح</string>
|
||||
<string name="settings_dark_theme">داكن</string>
|
||||
<string name="settings_card_orientation">اتجاه الباركود</string>
|
||||
<string name="settings_portrait_orientation">الوضع الرأسي</string>
|
||||
<string name="settings_landscape_orientation">الوضع الأفقي</string>
|
||||
<string name="settings_theme">مظهر</string>
|
||||
<string name="settings_max_font_size_scale">اقصي. حجم الخط</string>
|
||||
<string name="settings_display_barcode_max_brightness">عرض مشرق علي الباركود</string>
|
||||
<string name="importSuccessful">تم استيراد البيانات</string>
|
||||
<string name="exportSuccessful">تم تصدير البيانات</string>
|
||||
@@ -83,9 +75,8 @@
|
||||
<string name="balance">الرصيد</string>
|
||||
<string name="currency">العملة</string>
|
||||
<string name="points">نقاط</string>
|
||||
<string name="parsingBalanceFailed"><xliff:g>%s</xliff:g> لا يبدو أنه رصيد صالح.</string>
|
||||
<string name="chooseImportType">استيراد البيانات من</string>
|
||||
<string name="app_loyalty_card_keychain">Loyalty Card Keychain</string>
|
||||
<string name="app_loyalty_card_keychain">سلسلة مفاتيح بطاقة الولاء</string>
|
||||
<string name="privacy_policy">سياسة الخصوصية</string>
|
||||
<string name="accept">قبول</string>
|
||||
<string name="importCatima">الاستيراد من Catima</string>
|
||||
@@ -94,7 +85,7 @@
|
||||
<string name="importFidme">الاستيراد من FidMe</string>
|
||||
<string name="importFidmeMessage">حدد ملفك <i>fidme-export-request-xxxxxx.zip</i> تصدير من FidMe للاستيراد ، ثم حدد أنواع الباركود يدويًا بعد ذلك.
|
||||
\nقم بإنشائه من ملف تعريف FidMe الخاص بك عن طريق اختيار حماية البيانات ثم الضغط على استخراج بياناتي أولاً.</string>
|
||||
<string name="importStocardMessage">حدد ملفك <i>***-sync.zip</i> تصدير من Stocard للاستيراد.
|
||||
<string name="importStocardMessage">حدد ملفك <i>***.zip</i> تصدير من Stocard للاستيراد.
|
||||
\nاحصل عليه عن طريق إرسال بريد إلكتروني إلى support@stocardapp.com لطلب تصدير بياناتك.</string>
|
||||
<string name="importVoucherVault">الاستيراد من Voucher Vault</string>
|
||||
<string name="importVoucherVaultMessage">حدد ملفك <i>vouchervault.json</i> تصدير من Voucher Vault للاستيراد.
|
||||
@@ -124,7 +115,7 @@
|
||||
<string name="turn_flashlight_off">أطفئ المصباح</string>
|
||||
<string name="settings_locale">لغة</string>
|
||||
<string name="settings_system_locale">النظام</string>
|
||||
<string name="setIcon">تعيين رمز</string>
|
||||
<string name="setIcon">تعيين الصورة المصغرة</string>
|
||||
<string name="settings_catima_theme">Catima</string>
|
||||
<string name="settings_pink_theme">زهري</string>
|
||||
<string name="settings_magenta_theme">أرجواني</string>
|
||||
@@ -137,9 +128,6 @@
|
||||
<string name="app_contributors">أصبح ممكنًا بواسطة: <xliff:g id="app_contributors">%s</xliff:g></string>
|
||||
<string name="sort">فرز</string>
|
||||
<string name="showMoreInfo">اظهر المعلومات</string>
|
||||
<string name="hideMoreInfo">إخفاء المعلومات</string>
|
||||
<string name="swipeToSwitchImages">اسحب أو اضغط لفترة طويلة لتبديل الصور</string>
|
||||
<string name="sort_by_balance">الرصيد</string>
|
||||
<string name="reverse">... بترتيب معكوس</string>
|
||||
<string name="sort_by">صنف حسب</string>
|
||||
<string name="settings_oled_dark">خلفية سوداء نقية لمظهر داكن</string>
|
||||
@@ -151,16 +139,14 @@
|
||||
<string name="shortcutSelectCard">اختر بطاقة</string>
|
||||
<string name="options">خيارات</string>
|
||||
<string name="starred">مميز بنجمة</string>
|
||||
<string name="set_scale">ضبط النطاق</string>
|
||||
<string name="include_if_asking_support">إذا كنت ترغب في طلب الدعم ، فقم بتضمين المعلومات التالية:</string>
|
||||
<string name="duplicateCard">كرر</string>
|
||||
<string name="archive">ضع الي الأرشيف</string>
|
||||
<string name="unarchive">أخرج من الأرشيف</string>
|
||||
<string name="archived">تمت أرشفة البطاقة</string>
|
||||
<string name="unarchived">البطاقة غير مؤرشفة</string>
|
||||
<string name="archiveList">أرشيف</string>
|
||||
<string name="failedLaunchingPhotoPicker">تعذر العثور على تطبيق معرض مدعوم</string>
|
||||
<string name="noGiftCardsGroup">انشئ بعض من البطاقات, و عيينهم لهذة المجموعة.</string>
|
||||
<string name="noGiftCardsGroup">انشئ بعضا من البطاقات, ثم عيينهم لهذه المجموعة.</string>
|
||||
<string name="deleteTitle">مسح بطاقة</string>
|
||||
<plurals name="selectedCardCount">
|
||||
<item quantity="zero"><xliff:g>%d</xliff:g> محدد</item>
|
||||
@@ -174,12 +160,11 @@
|
||||
<string name="exportName">تصدير</string>
|
||||
<string name="exporting">جار التصدير…</string>
|
||||
<string name="importSuccessfulTitle">مستورد</string>
|
||||
<string name="importFailedTitle">فشل الاستيراد</string>
|
||||
<string name="importFailedTitle">فشل بالاستيراد</string>
|
||||
<string name="sort_by_most_recently_used">الأكثر أستعمالا مؤخرا</string>
|
||||
<string name="license">رخصة</string>
|
||||
<string name="sort_by_name">اسم</string>
|
||||
<string name="version_history">تاريخ النسخة</string>
|
||||
<string name="action_hide_details">أخف التفاصيل</string>
|
||||
<plurals name="groupCardCountWithArchived">
|
||||
<item quantity="zero"><xliff:g>%1$d</xliff:g> بطاقة ( <xliff:g id="archivedCount">%2$d</xliff:g> مؤرشفة)</item>
|
||||
<item quantity="one"><xliff:g>%1$d</xliff:g> بطاقة ( <xliff:g id="archivedCount">%2$d</xliff:g> مؤرشفة)</item>
|
||||
@@ -191,18 +176,18 @@
|
||||
<plurals name="deleteCardsTitle">
|
||||
<item quantity="zero">مسح <xliff:g>%d</xliff:g> بطاقة</item>
|
||||
<item quantity="one">مسح <xliff:g>%d</xliff:g> بطاقة</item>
|
||||
<item quantity="two">مسح <xliff:g>%d</xliff:g> بطاقة</item>
|
||||
<item quantity="two">مسح <xliff:g>%d</xliff:g> بطاقتين</item>
|
||||
<item quantity="few">مسح <xliff:g>%d</xliff:g> بطاقات</item>
|
||||
<item quantity="many">مسح <xliff:g>%d</xliff:g> بطاقات</item>
|
||||
<item quantity="other">مسح <xliff:g>%d</xliff:g> بطاقات</item>
|
||||
</plurals>
|
||||
<plurals name="deleteCardsConfirmation">
|
||||
<item quantity="zero">مسح هذه <xliff:g>%d</xliff:g> بطاقة نهائيا؟</item>
|
||||
<item quantity="one">مسح هذه <xliff:g>%d</xliff:g> بطاقة نهائيا؟</item>
|
||||
<item quantity="two">مسح هذه <xliff:g>%d</xliff:g> بطاقة نهائيا؟</item>
|
||||
<item quantity="few">مسح هذه <xliff:g>%d</xliff:g> بطاقات نهائيا؟</item>
|
||||
<item quantity="many">مسح هذه <xliff:g>%d</xliff:g> بطاقات نهائيا؟</item>
|
||||
<item quantity="other">مسح هذه <xliff:g>%d</xliff:g> بطاقات نهائيا؟</item>
|
||||
<item quantity="zero">مسح هذه <xliff:g>%d</xliff:g> البطاقة نهائيا؟</item>
|
||||
<item quantity="one">مسح هذه <xliff:g>%d</xliff:g> البطاقة نهائيا؟</item>
|
||||
<item quantity="two">مسح هذه <xliff:g>%d</xliff:g> البطاقتين نهائيا؟</item>
|
||||
<item quantity="few">مسح هذه <xliff:g>%d</xliff:g> البطاقات نهائيا؟</item>
|
||||
<item quantity="many">مسح هذه <xliff:g>%d</xliff:g> البطاقات نهائيا؟</item>
|
||||
<item quantity="other">مسح هذه <xliff:g>%d</xliff:g> البطاقات نهائيا؟</item>
|
||||
</plurals>
|
||||
<string name="importOptionFilesystemTitle">الاستيراد من نظام الملفات</string>
|
||||
<string name="importOptionFilesystemExplanation">اختر ملفًا محددًا من نظام الملفات.</string>
|
||||
@@ -213,7 +198,6 @@
|
||||
<string name="app_resources">موارد الطرف الثالث الحرة: <xliff:g id="app_resources_list">%s</xliff:g></string>
|
||||
<string name="settings_follow_system_orientation">نظام المتابعة</string>
|
||||
<string name="groups">مجموعات</string>
|
||||
<string name="barcode">باركود</string>
|
||||
<string name="settings_keep_screen_on">حافظ على الشاشة قيد التشغيل</string>
|
||||
<string name="intent_import_card_from_url_share_text">اريد مشاركة بطاقة معك</string>
|
||||
<string name="groupsList">مجموعات: <xliff:g>%s</xliff:g></string>
|
||||
@@ -235,7 +219,6 @@
|
||||
<string name="balanceSentence">الرصيد: <xliff:g>%s</xliff:g></string>
|
||||
<string name="expiryStateSentence">تنتهي: <xliff:g>%s</xliff:g></string>
|
||||
<string name="expiryStateSentenceExpired">منتهي الصلاحية: <xliff:g>%s</xliff:g></string>
|
||||
<string name="moveBarcodeToCenterOfScreen">قم بتوسيط الرمز الشريطي على الشاشة</string>
|
||||
<plurals name="balancePoints">
|
||||
<item quantity="zero"><xliff:g>%s</xliff:g> نقطة</item>
|
||||
<item quantity="one"><xliff:g>%s</xliff:g> نقطة</item>
|
||||
@@ -255,9 +238,7 @@
|
||||
<string name="on_github">على GitHub</string>
|
||||
<string name="report_error">الإبلاغ عن خطأ</string>
|
||||
<string name="sort_by_expiry">انقضاء</string>
|
||||
<string name="noUnarchivedCardsMessage">لا توجد بطاقات غير مؤرشفة موجودة</string>
|
||||
<string name="importLoyaltyCardKeychain">الاستيراد من Loyalty Card Keychain</string>
|
||||
<string name="app_copyright_fmt" tools:ignore="PluralsCandidate">Copyright © 2019–<xliff:g>%d</xliff:g> Sylvia van Os</string>
|
||||
<string name="importLoyaltyCardKeychainMessage">حدد ملفك <i>LoyaltyCardKeychain.csv</i> التصدير من Loyalty Card Keychain للاستيراد.
|
||||
\nقم بإنشائه من قائمة الاستيراد / التصدير في Loyalty Card Keychain بالضغط على تصدير هناك أولاً.</string>
|
||||
<string name="importStocard">الاستيراد من Stocard</string>
|
||||
@@ -266,7 +247,6 @@
|
||||
\nلا يتم جمع أي بيانات على الإطلاق ، والتي يمكن لأي شخص تأكيدها لأن تطبيقنا هو برنامج حر.</string>
|
||||
<string name="failedGeneratingShareURL">تعذر إنشاء عنوان URL قابل للمشاركة. الرجاء الإبلاغ عن هذا.</string>
|
||||
<string name="help_translate_this_app">ساعد في ترجمة هذا التطبيق</string>
|
||||
<string name="action_show_details">اظهر التفاصيل</string>
|
||||
<string name="on_google_play">على Google Play</string>
|
||||
<string name="settings_theme_color">لون المظهر</string>
|
||||
<string name="previousCard">السابق</string>
|
||||
@@ -275,4 +255,55 @@
|
||||
<string name="barcodeLongPressMessage">يمكن فتح صور فقط في تطبيق معرض الصور</string>
|
||||
<string name="failedToOpenUrl">ثبت متصفح ويب أولاً</string>
|
||||
<string name="welcome">مرحبا بك في كاتيما</string>
|
||||
<string name="updateBalanceTitle">كم أنفقت؟</string>
|
||||
<string name="currentBalanceSentence">الرصيد الحالي: <xliff:g> %s </xliff:g></string>
|
||||
<plurals name="viewArchivedCardsWithCount">
|
||||
<item quantity="zero">عرض الأرشيف (<xliff:g>%1$d</xliff:g> بطاقة)</item>
|
||||
<item quantity="one">عرض الأرشيف (<xliff:g>%1$d</xliff:g> بطاقة)</item>
|
||||
<item quantity="two">عرض الأرشيف (<xliff:g>%1$d</xliff:g> بطاقتين)</item>
|
||||
<item quantity="few">عرض الأرشيف (<xliff:g>%1$d</xliff:g> بطاقات)</item>
|
||||
<item quantity="many">عرض الأرشيف (<xliff:g>%1$d</xliff:g> بطاقات)</item>
|
||||
<item quantity="other">عرض الأرشيف (<xliff:g>%1$d</xliff:g> بطاقات)</item>
|
||||
</plurals>
|
||||
<string name="importCards">استيراد البطاقات</string>
|
||||
<string name="newBalanceSentence">الرصيد الجديد: <xliff:g>%s</xliff:g></string>
|
||||
<string name="cameraPermissionDeniedTitle">تعذر الوصول إلى الكاميرا</string>
|
||||
<string name="noCameraPermissionDirectToSystemSetting">لمسح الباركود، ستحتاج Catima إلى الوصول إلى الكاميرا. اضغط هنا لتغيير إعدادات الأذونات.</string>
|
||||
<string name="updateBalance">تحديث الرصيد</string>
|
||||
<string name="updateBalanceHint">أدخل المبلغ</string>
|
||||
<string name="storageReadPermissionRequired">الصلاحيه للوصل للتخزين مطلوبة لهذا الاجراء</string>
|
||||
<string name="validFromDate">عربيه</string>
|
||||
<string name="cameraPermissionRequired">إذن للوصول إلى الكاميرا اللازمة لهذا الإجراء…</string>
|
||||
<string name="anyDate">أي تاريخ</string>
|
||||
<string name="chooseValidFromDate">اختر صالح من التاريخ</string>
|
||||
<string name="validFromSentence">صالح من:<xliff:g>%s</xliff:g></string>
|
||||
<string name="height">الطول:</string>
|
||||
<string name="permissionReadCardsDescription">اقرأ بطاقتك مع جميع التفاصيل، بما فيه الملاحضات والصور</string>
|
||||
<string name="settings_display_barcode_max_brightness_summary">ظروري لعمل بعض الماسحات الضوئية</string>
|
||||
<string name="permissionReadCardsLabel">اقرأ بطاقات كاتيما</string>
|
||||
<string name="app_copyright_fmt" tools:ignore="PluralsCandidate">حقوق النشر © 2019–<xliff:g>%d</xliff:g> Sylvia van Os والمساهمون</string>
|
||||
<string name="app_copyright_short">حقوق النشر © Sylvia van Os والمساهمون</string>
|
||||
<string name="openFrontImageInGalleryApp">افتح الصورة الأمامية في تطبيق المعرض</string>
|
||||
<string name="show_name_below_image_thumbnail">إظهار الاسم أسفل الصورة المصغرة</string>
|
||||
<string name="show_validity">إظهار الصلاحية</string>
|
||||
<string name="view_online">مشاهدة حية</string>
|
||||
<string name="settings_category_title_general">عام</string>
|
||||
<string name="switchToBarcode">التبديل إلى الباركود</string>
|
||||
<string name="settings_disable_lockscreen_while_viewing_card_summary">تعطيل قفل الشاشة أثناء عرض البطاقة</string>
|
||||
<string name="action_display_options">خيارات العرض</string>
|
||||
<string name="settings_oled_dark_summary">يقلل من استخدام البطارية على شاشات OLED</string>
|
||||
<string name="icon_header_click_text">اضغط لفترة طويلة لتحرير الصورة المصغرة</string>
|
||||
<string name="settings_category_title_cards">البطاقات</string>
|
||||
<string name="show_note">إظهار الملاحظة</string>
|
||||
<string name="switchToBackImage">التبديل إلى الصورة الخلفية</string>
|
||||
<string name="switchToFrontImage">التبديل إلى الصورة الأمامية</string>
|
||||
<string name="settings_allow_content_provider_read_summary">سيظل يتعين على التطبيقات طلب الإذن لمنحها حق الوصول</string>
|
||||
<string name="setBarcodeHeight">ضبط ارتفاع الباركود</string>
|
||||
<string name="openBackImageInGalleryApp">فتح الصورة الخلفية في تطبيق المعرض</string>
|
||||
<string name="settings_allow_content_provider_read_title">السماح للتطبيقات الأخرى بالوصول إلى بياناتي</string>
|
||||
<string name="donate">يتبرع</string>
|
||||
<string name="show_archived_cards">عرض البطاقات المؤرشفة</string>
|
||||
<string name="settings_category_title_privacy">خصوصية</string>
|
||||
<string name="show_balance">إظهار التوازن</string>
|
||||
<string name="settings_keep_screen_on_summary">تعطيل مهلة الشاشة أثناء عرض البطاقة</string>
|
||||
</resources>
|
||||
@@ -1,2 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources></resources>
|
||||
52
app/src/main/res/values-ast/strings.xml
Normal file
52
app/src/main/res/values-ast/strings.xml
Normal file
@@ -0,0 +1,52 @@
|
||||
<?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="storeName">Nome</string>
|
||||
<string name="note">Nota</string>
|
||||
<string name="noMatchingGiftCards">Nun hai nengún resultáu. Prueba a camudar la busca.</string>
|
||||
<string name="barcodeType">Tipu de códigu de barres</string>
|
||||
<string name="noBarcode">Ensin códigu de barres</string>
|
||||
<string name="star">Meter en Favoritos</string>
|
||||
<string name="confirm">Confirmar</string>
|
||||
<string name="deleteConfirmation">¿Quies desaniciar esta tarxeta permanentemente\?</string>
|
||||
<string name="importExport">Importar/Esportar</string>
|
||||
<string name="exportFailed">Num se pudo facer la esportación</string>
|
||||
<string name="importFailedTitle">La importación falló</string>
|
||||
<string name="exportOptionExplanation">Los datos escríbense nel llugar qu\'escueyas.</string>
|
||||
<string name="app_license">Software llibre llicenciao cola GPLv3+</string>
|
||||
<string name="action_search">Buscar</string>
|
||||
<string name="action_add">Amestar</string>
|
||||
<plurals name="selectedCardCount">
|
||||
<item quantity="one"><xliff:g>%d</xliff:g> na seleición</item>
|
||||
<item quantity="other"><xliff:g>%d</xliff:g> na seleición</item>
|
||||
</plurals>
|
||||
<string name="noGiftCards">Calca\'l botón «+» p\'amestar una tarxeta o impórtales dende\'l menú «⋮».</string>
|
||||
<string name="cardId">ID</string>
|
||||
<string name="save">Guardar</string>
|
||||
<string name="edit">Editar</string>
|
||||
<string name="delete">Desaniciar</string>
|
||||
<plurals name="deleteCardsTitle">
|
||||
<item quantity="one"></item>
|
||||
<item quantity="other"></item>
|
||||
</plurals>
|
||||
<string name="unstar">Quitar de Favoritos</string>
|
||||
<string name="cancel">Encaboxar</string>
|
||||
<string name="importFailed">Nun se pudo facer la importación</string>
|
||||
<string name="noCardExistsError">Nun se pudo atopar esa tarxeta</string>
|
||||
<string name="all">Too</string>
|
||||
<string name="sendLabel">Unviar…</string>
|
||||
<string name="editCardTitle">Editar la tarxeta</string>
|
||||
<string name="ok">D\'acuerdu</string>
|
||||
<string name="exportFailedTitle">La esportación falló</string>
|
||||
<string name="noCameraPermissionDirectToSystemSetting">Pa escaniar códigos de barres, Catima tien d\'acceder a la cámara. Toca equí pa camudar la configuración de los permisos.</string>
|
||||
<string name="debug_version_fmt">Versión: <xliff:g id="version">%s</xliff:g></string>
|
||||
<string name="importing">Importando…</string>
|
||||
<string name="exporting">Esportando…</string>
|
||||
<string name="storageReadPermissionRequired">Esta aición precisa\'l permisu pa lleer l\'almacenamientu…</string>
|
||||
<string name="cameraPermissionRequired">Esta aición precisa l\'accesu a la cámara…</string>
|
||||
<string name="cameraPermissionDeniedTitle">Nun se pudo acceder a la cámara</string>
|
||||
<string name="app_copyright_old">Básase en Loyalty Card Keychain
|
||||
\ncopyright © 2016–2020 Branden Archer</string>
|
||||
<string name="thumbnailDescription">Miniatura</string>
|
||||
<string name="settings">Configuración</string>
|
||||
<string name="settings_theme">Estilu</string>
|
||||
</resources>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user