mirror of
https://github.com/CatimaLoyalty/Android.git
synced 2026-09-08 12:21:49 -04:00
Compare commits
1174
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
909034d4f3 | ||
|
|
c261d33980 | ||
|
|
e9654c9b04 | ||
|
|
4742a290c8 | ||
|
|
bbae3351a5 | ||
|
|
db149cd3aa | ||
|
|
1a94748958 | ||
|
|
a20ad88532 | ||
|
|
93b4ba1167 | ||
|
|
693d6d6663 | ||
|
|
67bb1a2627 | ||
|
|
d20f91018d | ||
|
|
9414627772 | ||
|
|
6134fb26c6 | ||
|
|
6445df4d2f | ||
|
|
1069803681 | ||
|
|
60f9d19c6c | ||
|
|
b2d001d238 | ||
|
|
ea26cdbb9f | ||
|
|
f3154081f4 | ||
|
|
5388bbfc16 | ||
|
|
48284acf06 | ||
|
|
852830c894 | ||
|
|
97b2c8f080 | ||
|
|
888869d5fc | ||
|
|
9452c65438 | ||
|
|
08db580076 | ||
|
|
649135e6d4 | ||
|
|
fb255fd46a | ||
|
|
ba01e9f0fe | ||
|
|
c6b6a4012e | ||
|
|
964c1a82db | ||
|
|
6eb2a8db45 | ||
|
|
abf5466883 | ||
|
|
c40fcdee9e | ||
|
|
ca66205493 | ||
|
|
8bdea4a3cd | ||
|
|
d11797be41 | ||
|
|
332a5df506 | ||
|
|
024125cf52 | ||
|
|
5b747d6fea | ||
|
|
f4b2597d95 | ||
|
|
4a24b98868 | ||
|
|
932f82a08b | ||
|
|
c20709b377 | ||
|
|
f1fd813ddc | ||
|
|
dfb8089db3 | ||
|
|
cf6cd74e8f | ||
|
|
5b93bd79a9 | ||
|
|
5831adbcc6 | ||
|
|
133b6df382 | ||
|
|
0e9abac8a2 | ||
|
|
b6b172ebfd | ||
|
|
561f508e02 | ||
|
|
b36a29a5c7 | ||
|
|
938f17b559 | ||
|
|
9feec7f6e3 | ||
|
|
b1f0e7bbfd | ||
|
|
24335cb2fc | ||
|
|
4d41ae9448 | ||
|
|
d380560ff0 | ||
|
|
aa649ca9a9 | ||
|
|
754f9c51ea | ||
|
|
c6a2f79946 | ||
|
|
3e6a735217 | ||
|
|
d0d9d70445 | ||
|
|
9db715b916 | ||
|
|
7c88c004b6 | ||
|
|
6215d77b72 | ||
|
|
c947f53d2c | ||
|
|
d9029284c3 | ||
|
|
48b099e182 | ||
|
|
cbac258a6a | ||
|
|
256a39643b | ||
|
|
9dc21315b8 | ||
|
|
d27ada63b1 | ||
|
|
97a2874218 | ||
|
|
1c3471f7b9 | ||
|
|
b8b677e7a8 | ||
|
|
986873e945 | ||
|
|
7e90a439a6 | ||
|
|
ba2dab02a6 | ||
|
|
5aad361ebd | ||
|
|
418443300c | ||
|
|
3958a48137 | ||
|
|
c829b2d9bf | ||
|
|
12d78786f0 | ||
|
|
87655c9e28 | ||
|
|
eb54d7c250 | ||
|
|
78a7b93c21 | ||
|
|
4da8378aef | ||
|
|
2f6687ec90 | ||
|
|
38b2584153 | ||
|
|
a25d36da29 | ||
|
|
6f7c3ec39c | ||
|
|
d9e94d52f5 | ||
|
|
035f651b9f | ||
|
|
98d2c69640 | ||
|
|
0dfeb2dcc8 | ||
|
|
645e7780f3 | ||
|
|
d635e31afc | ||
|
|
91d0882ad8 | ||
|
|
4ab4ac7668 | ||
|
|
48115b3f2d | ||
|
|
f55559d6d6 | ||
|
|
0bf1afd7af | ||
|
|
1ff4f47102 | ||
|
|
70dbd7eac1 | ||
|
|
ff7ebe0629 | ||
|
|
0d847607a2 | ||
|
|
dd53862d37 | ||
|
|
9df5366fc6 | ||
|
|
69f888fe29 | ||
|
|
bdc08ef95c | ||
|
|
7b098bb6dd | ||
|
|
43085d0c13 | ||
|
|
89885f5e9d | ||
|
|
3d48bdeee6 | ||
|
|
e331f2e293 | ||
|
|
0a605d518b | ||
|
|
72f02bd16d | ||
|
|
ff9a6884a9 | ||
|
|
9fd6c89c5b | ||
|
|
73d2644e96 | ||
|
|
e8de07bdb8 | ||
|
|
3003acdc81 | ||
|
|
350241f517 | ||
|
|
ed6289341a | ||
|
|
0811eb75df | ||
|
|
619408e272 | ||
|
|
c43ff9dc95 | ||
|
|
7fd461ef61 | ||
|
|
3b17a24f29 | ||
|
|
95358bf16b | ||
|
|
d8de262111 | ||
|
|
5f78ee7f95 | ||
|
|
c3cbdf5a24 | ||
|
|
ac1f7d158a | ||
|
|
bdf8823855 | ||
|
|
17693ec3f2 | ||
|
|
1afea8f39c | ||
|
|
f0068144dc | ||
|
|
90a4de1343 | ||
|
|
e83a8ef7e7 | ||
|
|
2526cb65ba | ||
|
|
4f419b86e2 | ||
|
|
6c1baa9ec1 | ||
|
|
b499d540dc | ||
|
|
522bd5d4b0 | ||
|
|
d3d6d1a565 | ||
|
|
e6b639040b | ||
|
|
64259502db | ||
|
|
a5a9c1330b | ||
|
|
c9fb6ad2ac | ||
|
|
3ecc752c6c | ||
|
|
db0f97542f | ||
|
|
c9d372223e | ||
|
|
d5be79198c | ||
|
|
b75a5b56d2 | ||
|
|
a1da000157 | ||
|
|
965d1fd2b7 | ||
|
|
bc34cd9b5e | ||
|
|
d3d2368be0 | ||
|
|
afb78ac8b8 | ||
|
|
b675ad785d | ||
|
|
649bd85fd5 | ||
|
|
f302fb7da5 | ||
|
|
a9ad35a379 | ||
|
|
ed412c221a | ||
|
|
e46da24e03 | ||
|
|
c9ef64b81f | ||
|
|
18a6235844 | ||
|
|
357566805b | ||
|
|
f89a2b8447 | ||
|
|
2528b9ff2a | ||
|
|
3a8b3689fb | ||
|
|
74d2a17867 | ||
|
|
e77c90debe | ||
|
|
8364e86980 | ||
|
|
fc5206456e | ||
|
|
216e3afec7 | ||
|
|
70b86277bd | ||
|
|
488163c945 | ||
|
|
5c8916d32e | ||
|
|
9f1fa7601b | ||
|
|
a3f59ed7a2 | ||
|
|
20ad4003b7 | ||
|
|
c059dd916e | ||
|
|
61b582f676 | ||
|
|
4bf6ae897f | ||
|
|
74b15ce766 | ||
|
|
cea4a5431e | ||
|
|
12e0105b5a | ||
|
|
17be43d0d2 | ||
|
|
dd071c912a | ||
|
|
32cdacb4be | ||
|
|
c1cbebd782 | ||
|
|
2a334445dc | ||
|
|
34bba5a636 | ||
|
|
3c18ac0d96 | ||
|
|
9b9005087e | ||
|
|
88f0bc4c28 | ||
|
|
c8d368e86f | ||
|
|
318d6956fa | ||
|
|
5443214033 | ||
|
|
2bdabbcd4c | ||
|
|
38eaa59518 | ||
|
|
ed6fa92f51 | ||
|
|
7f0b4bccb3 | ||
|
|
f4edfb870b | ||
|
|
9a07256a70 | ||
|
|
b744754949 | ||
|
|
f08859b377 | ||
|
|
fb5475b724 | ||
|
|
798d26ddae | ||
|
|
cd2eb2e0d9 | ||
|
|
dfa1c29fa0 | ||
|
|
090e85ea1c | ||
|
|
d1fd7c1eb0 | ||
|
|
775cba5667 | ||
|
|
6f40bc5b1b | ||
|
|
e14fb974b4 | ||
|
|
dbf29c6b83 | ||
|
|
ad44fd3382 | ||
|
|
61df41d5fe | ||
|
|
942a795c2c | ||
|
|
5f948badd1 | ||
|
|
f07f32b169 | ||
|
|
905257d039 | ||
|
|
4af7500e42 | ||
|
|
a9a3a0037b | ||
|
|
111d16319b | ||
|
|
269a74da52 | ||
|
|
51a094a502 | ||
|
|
b048c017bf | ||
|
|
d725756498 | ||
|
|
65649bbb72 | ||
|
|
7e246e9d78 | ||
|
|
cdf965f4da | ||
|
|
b2e19bb89a | ||
|
|
ba6cfb0a55 | ||
|
|
8a24dbfdaf | ||
|
|
d84e13e2ef | ||
|
|
c4727aa3d8 | ||
|
|
110b5d4c87 | ||
|
|
b8c834794b | ||
|
|
bcc1cc412e | ||
|
|
ff96880cc2 | ||
|
|
6f4e9662db | ||
|
|
5da99fc5f0 | ||
|
|
ec3fc8737a | ||
|
|
5fd3dcda36 | ||
|
|
938aaba898 | ||
|
|
cb7e6c96b0 | ||
|
|
49b128a20c | ||
|
|
7f683eb920 | ||
|
|
489c89205d | ||
|
|
4b0b91de98 | ||
|
|
c54b1ab4a0 | ||
|
|
8055a2b651 | ||
|
|
71ababc4d1 | ||
|
|
498b885398 | ||
|
|
774fcfd497 | ||
|
|
0967e475d5 | ||
|
|
2bd6fc444e | ||
|
|
dfa025952b | ||
|
|
30918675bf | ||
|
|
91f6c11821 | ||
|
|
bf16fbe84b | ||
|
|
ffb8003245 | ||
|
|
1645b5f362 | ||
|
|
aa03cb5f89 | ||
|
|
1d8025b0cf | ||
|
|
3ac8998377 | ||
|
|
5160f61523 | ||
|
|
68c5fa10f4 | ||
|
|
620f564ae5 | ||
|
|
b840d481a8 | ||
|
|
7872a59a89 | ||
|
|
62c7f92563 | ||
|
|
283f237180 | ||
|
|
6dcd1c43fe | ||
|
|
805bad437c | ||
|
|
f476fb1cc0 | ||
|
|
f35f8b1b29 | ||
|
|
061ed7e765 | ||
|
|
fbbfe78828 | ||
|
|
557132b4c5 | ||
|
|
5bc7a15c15 | ||
|
|
0fa34e3eeb | ||
|
|
eeef8bd468 | ||
|
|
6b5eeea3b7 | ||
|
|
6e9fd540c5 | ||
|
|
c4da8ce9df | ||
|
|
5cc50e6e06 | ||
|
|
5088ea7b80 | ||
|
|
d7ae175a00 | ||
|
|
60e47f2ffc | ||
|
|
850c2d3b8a | ||
|
|
cdddd5eacf | ||
|
|
fd4793db99 | ||
|
|
9006dbee87 | ||
|
|
073923d072 | ||
|
|
dfb006584e | ||
|
|
c00383ce81 | ||
|
|
ca9d61d955 | ||
|
|
37a8549d21 | ||
|
|
6c753b5058 | ||
|
|
7395ed0c2b | ||
|
|
2b55c2b5e0 | ||
|
|
41d9aa488f | ||
|
|
3e46d2ad84 | ||
|
|
dac990716d | ||
|
|
ed56e14574 | ||
|
|
acf72397b6 | ||
|
|
02d97e440f | ||
|
|
f2e67d3727 | ||
|
|
ae11b24c7a | ||
|
|
6f2b77fbdc | ||
|
|
11d4df8c36 | ||
|
|
f221b01811 | ||
|
|
236e9e60da | ||
|
|
40cfb3d111 | ||
|
|
05190e6384 | ||
|
|
77ab8ed85b | ||
|
|
059cf5e631 | ||
|
|
b304c7ebe8 | ||
|
|
9b097d3a42 | ||
|
|
4dc2e1f262 | ||
|
|
6a10cdd8cd | ||
|
|
7febdf13b7 | ||
|
|
47b0b678e4 | ||
|
|
4e21c606ef | ||
|
|
aef143256d | ||
|
|
7c76bd91b4 | ||
|
|
2ddce7b69b | ||
|
|
73df916b86 | ||
|
|
85285a2005 | ||
|
|
5a3b90ec12 | ||
|
|
13bc0b7d46 | ||
|
|
814e11a153 | ||
|
|
262988a0f8 | ||
|
|
4376bf069c | ||
|
|
995d75c95a | ||
|
|
f9773de1d8 | ||
|
|
c46cc41bc4 | ||
|
|
bcef06a1f1 | ||
|
|
45941784a8 | ||
|
|
d646853c2e | ||
|
|
eedc3ccc69 | ||
|
|
036a9a12cc | ||
|
|
59d8746cb3 | ||
|
|
cf8e266c4c | ||
|
|
363c53ede2 | ||
|
|
f9175ffd03 | ||
|
|
041b2538d3 | ||
|
|
58daf0993b | ||
|
|
2be75b28d9 | ||
|
|
3e9549088a | ||
|
|
0428a87c71 | ||
|
|
9121df192d | ||
|
|
b63d85d341 | ||
|
|
423a762cad | ||
|
|
2c68d395f7 | ||
|
|
b1132c901a | ||
|
|
db1fd83c8b | ||
|
|
4592ba8d38 | ||
|
|
05951c0776 | ||
|
|
070c010156 | ||
|
|
3e0741b6d8 | ||
|
|
8ec0e3348b | ||
|
|
e81a63c695 | ||
|
|
d41976829a | ||
|
|
09589ec899 | ||
|
|
9553f1ca74 | ||
|
|
2d91e87ca0 | ||
|
|
1cd09bace7 | ||
|
|
a896464e4c | ||
|
|
052d492f59 | ||
|
|
d0fc8c3f73 | ||
|
|
02322e50ab | ||
|
|
029aa6d466 | ||
|
|
9fbbb43ec1 | ||
|
|
8429539527 | ||
|
|
935f878688 | ||
|
|
5596d1f8a6 | ||
|
|
00028248c0 | ||
|
|
75feb7b409 | ||
|
|
5fefe96963 | ||
|
|
7572cdb25d | ||
|
|
77834809f6 | ||
|
|
b72edac261 | ||
|
|
4c4623d92d | ||
|
|
162b51f1fa | ||
|
|
f667c4f3ad | ||
|
|
b291f6b26c | ||
|
|
a679acfb6a | ||
|
|
c546294cd9 | ||
|
|
474c6e2112 | ||
|
|
dbce80b4fa | ||
|
|
4399cdadb0 | ||
|
|
14e7f5bbb8 | ||
|
|
3440017db8 | ||
|
|
1223b2b4eb | ||
|
|
731fbc3083 | ||
|
|
bb9487b35e | ||
|
|
43ea2a0f84 | ||
|
|
f5606aea09 | ||
|
|
2d10d080a4 | ||
|
|
e36c007176 | ||
|
|
60c2784643 | ||
|
|
49d91caafd | ||
|
|
6c4e9fc29b | ||
|
|
e49fa835a8 | ||
|
|
ab1e7c623f | ||
|
|
725ce14ed6 | ||
|
|
a154445723 | ||
|
|
b09e49ddae | ||
|
|
f94f39a31c | ||
|
|
a4172acdf2 | ||
|
|
8823762b16 | ||
|
|
5b9f44e73d | ||
|
|
af1b5f135b | ||
|
|
14399f46cf | ||
|
|
eccd154046 | ||
|
|
e7e789aadb | ||
|
|
1f171427fc | ||
|
|
1cf054f50c | ||
|
|
e824c3830e | ||
|
|
b93b0eaef7 | ||
|
|
be4c4e7455 | ||
|
|
5dbb53f053 | ||
|
|
550946be5e | ||
|
|
8819e2dddc | ||
|
|
ae14412385 | ||
|
|
276a731f53 | ||
|
|
6873c8bc72 | ||
|
|
13a3a85916 | ||
|
|
79c25e5dd4 | ||
|
|
05e1710953 | ||
|
|
756698bda6 | ||
|
|
8965acb17c | ||
|
|
87e6fc3d03 | ||
|
|
438959e42b | ||
|
|
c5713c6079 | ||
|
|
2251df5a6f | ||
|
|
4474aee7fa | ||
|
|
4dcc637065 | ||
|
|
d793eec2e0 | ||
|
|
3ece1c3636 | ||
|
|
c135cad669 | ||
|
|
3d9e636cc3 | ||
|
|
b64c4966e0 | ||
|
|
7695fe8659 | ||
|
|
ad7e021928 | ||
|
|
73233ad6de | ||
|
|
3e96c793b1 | ||
|
|
6d10a87efc | ||
|
|
0228b7d0a4 | ||
|
|
798377416d | ||
|
|
9b64c65816 | ||
|
|
2030fe4461 | ||
|
|
c8f9fb8bac | ||
|
|
cce698578e | ||
|
|
2bf8146489 | ||
|
|
7148477209 | ||
|
|
fe0e68ab37 | ||
|
|
24420db4c4 | ||
|
|
2a0e067472 | ||
|
|
8126ffc980 | ||
|
|
9d530e095a | ||
|
|
8dd8ed45b4 | ||
|
|
c825d91779 | ||
|
|
38a2c3f26b | ||
|
|
3b8aba29d5 | ||
|
|
3aa77e5022 | ||
|
|
543bacf448 | ||
|
|
4518dadcf6 | ||
|
|
f07b7412f5 | ||
|
|
474dc0e5d6 | ||
|
|
202cb91ac3 | ||
|
|
4e2672d9ae | ||
|
|
b281bdf392 | ||
|
|
168245c3c1 | ||
|
|
6d1856ceba | ||
|
|
dc58e89a0d | ||
|
|
16f4866c38 | ||
|
|
93492828dc | ||
|
|
e6fb6fd95b | ||
|
|
57b2f308da | ||
|
|
93d259b1d8 | ||
|
|
daf447cbdd | ||
|
|
9e438e1498 | ||
|
|
ce213356b9 | ||
|
|
f5fa11dcc3 | ||
|
|
841902bcaf | ||
|
|
3de83b6745 | ||
|
|
18ae00f5a4 | ||
|
|
8639bf98f4 | ||
|
|
543c5f9b23 | ||
|
|
102ba18a05 | ||
|
|
54e0ed3f82 | ||
|
|
d86b0043ff | ||
|
|
1a214e6526 | ||
|
|
af7cf16786 | ||
|
|
5ac2405834 | ||
|
|
08325b8528 | ||
|
|
6573c5ea80 | ||
|
|
77ca8e7069 | ||
|
|
3dbd99f052 | ||
|
|
4d34794957 | ||
|
|
8c99189d79 | ||
|
|
4dae3cb826 | ||
|
|
52d4f63418 | ||
|
|
76e3ba2e4a | ||
|
|
1bea439fa0 | ||
|
|
36459d62b6 | ||
|
|
3699a24809 | ||
|
|
fc8691470c | ||
|
|
a03be85474 | ||
|
|
701964bc46 | ||
|
|
18604ba8bd | ||
|
|
b6b9b7c90e | ||
|
|
5aa7217c1b | ||
|
|
7fb5c96322 | ||
|
|
3d9729f992 | ||
|
|
8c61e32798 | ||
|
|
0bb38e4fce | ||
|
|
9eb9e9df25 | ||
|
|
e8d7857044 | ||
|
|
2c782fba62 | ||
|
|
31cd7f009c | ||
|
|
068c0307af | ||
|
|
54c885d1cf | ||
|
|
bbb434e2b5 | ||
|
|
bc879f847d | ||
|
|
1a474e9d22 | ||
|
|
cbb2453f6b | ||
|
|
e372d23b6e | ||
|
|
a3507d5975 | ||
|
|
c4f28c12f8 | ||
|
|
69939d5957 | ||
|
|
c2b0fda80a | ||
|
|
b62a0b0f4f | ||
|
|
5f9376ca63 | ||
|
|
f1087573dc | ||
|
|
62f188eb72 | ||
|
|
cdfa3e6779 | ||
|
|
f64406f70f | ||
|
|
cabae400e6 | ||
|
|
d5b3796ae9 | ||
|
|
442c7c28d4 | ||
|
|
e7fd16dd45 | ||
|
|
868d065d5d | ||
|
|
185b0ff2df | ||
|
|
e60b1cc26a | ||
|
|
5cecad9088 | ||
|
|
f26799c65c | ||
|
|
6046b48285 | ||
|
|
4bf925f1b1 | ||
|
|
fa55a8b354 | ||
|
|
38a8f76b88 | ||
|
|
dc574c0569 | ||
|
|
c52791e029 | ||
|
|
37c31d8a0f | ||
|
|
4e99654d1d | ||
|
|
a9a84ffdfc | ||
|
|
9bb51da3c1 | ||
|
|
3f73224d97 | ||
|
|
b79c62341e | ||
|
|
33a47f2ed1 | ||
|
|
3177e91e58 | ||
|
|
3ec4ffcab2 | ||
|
|
2f8449bb04 | ||
|
|
011f3edcbc | ||
|
|
abb6762a4f | ||
|
|
382a28c983 | ||
|
|
c7a8d343b5 | ||
|
|
bc21a53d44 | ||
|
|
d3bdad56e4 | ||
|
|
c9a2a4acbb | ||
|
|
eaf30e6c2d | ||
|
|
5979160a6a | ||
|
|
0181fde243 | ||
|
|
a5599dc673 | ||
|
|
25290ae202 | ||
|
|
4d36812cd6 | ||
|
|
a9f402cd6d | ||
|
|
2798ba7648 | ||
|
|
349558e62e | ||
|
|
91ca2a220e | ||
|
|
93104e4b40 | ||
|
|
d8adcdd1e5 | ||
|
|
c4b210a8db | ||
|
|
d54323fde0 | ||
|
|
c02be50242 | ||
|
|
a883ce0f43 | ||
|
|
ace353d71d | ||
|
|
18dbb24375 | ||
|
|
78daf54716 | ||
|
|
ee6541ba54 | ||
|
|
e4e9fe05e1 | ||
|
|
9d4035c94e | ||
|
|
293d38bd09 | ||
|
|
28be05600b | ||
|
|
03649820ce | ||
|
|
93515d2f88 | ||
|
|
d7dc70c0df | ||
|
|
ce0738782a | ||
|
|
df738b9c1d | ||
|
|
8bf0033d22 | ||
|
|
01e08e4928 | ||
|
|
90aee54a3c | ||
|
|
b7c42b5c8c | ||
|
|
008b5254c6 | ||
|
|
1b1163a100 | ||
|
|
70c14514e6 | ||
|
|
5830813170 | ||
|
|
a07d4d1b8a | ||
|
|
29c4ccf4d9 | ||
|
|
417006cfa3 | ||
|
|
907a8578ca | ||
|
|
7e63babd47 | ||
|
|
c289bb0af3 | ||
|
|
80767e7cd6 | ||
|
|
90e6dd8738 | ||
|
|
d3de9c65e6 | ||
|
|
48993f0486 | ||
|
|
918c6fb41b | ||
|
|
d121841fdf | ||
|
|
95ace2aa39 | ||
|
|
b8cd52563d | ||
|
|
70754af167 | ||
|
|
80bad7ad56 | ||
|
|
e214d6d69e | ||
|
|
0e7509d966 | ||
|
|
58a5b16484 | ||
|
|
a8df588b33 | ||
|
|
ef948f16f1 | ||
|
|
11f942771f | ||
|
|
f507e15bfc | ||
|
|
64ebc345b5 | ||
|
|
f94dbcd66d | ||
|
|
881303b73d | ||
|
|
97a2ff7125 | ||
|
|
22e968796d | ||
|
|
0c5f90d4ad | ||
|
|
0f51e19d44 | ||
|
|
5c720aa58a | ||
|
|
19d7ef0669 | ||
|
|
04a22be542 | ||
|
|
6ff2a9b48c | ||
|
|
19d3b736cb | ||
|
|
2bcdf170ad | ||
|
|
23d79ba536 | ||
|
|
fe6de6f309 | ||
|
|
29251978c4 | ||
|
|
a10e8d5aab | ||
|
|
5973761e2e | ||
|
|
00f3fb47d5 | ||
|
|
37e6782032 | ||
|
|
f58bdba273 | ||
|
|
e02f74032b | ||
|
|
6213bd4500 | ||
|
|
7739249364 | ||
|
|
2308deba33 | ||
|
|
8252f52862 | ||
|
|
9b05d46c77 | ||
|
|
a8e9a932cb | ||
|
|
d1858f22cf | ||
|
|
307f0e12bc | ||
|
|
3caf3ee0a8 | ||
|
|
a0fea79359 | ||
|
|
b01e6b5928 | ||
|
|
23457854b7 | ||
|
|
68d5dcd5f3 | ||
|
|
d9905665b2 | ||
|
|
58af4bae55 | ||
|
|
9c5aefd8d1 | ||
|
|
bca0e07c23 | ||
|
|
4676c2c2b3 | ||
|
|
196542ce5e | ||
|
|
a550bcca07 | ||
|
|
17d3d86127 | ||
|
|
2c0efcb2c3 | ||
|
|
53f17731b3 | ||
|
|
5fb1ffebc1 | ||
|
|
9093eb9693 | ||
|
|
60ba0bb63e | ||
|
|
2215aa84c0 | ||
|
|
6d2ac18859 | ||
|
|
056f346f1f | ||
|
|
50c0babb24 | ||
|
|
deb47c032b | ||
|
|
2d3b0b9001 | ||
|
|
26b076a981 | ||
|
|
6b8d58c230 | ||
|
|
54aca0323a | ||
|
|
5546e904f4 | ||
|
|
cd48cd0635 | ||
|
|
f8ec8e1dbb | ||
|
|
13db4f6dab | ||
|
|
cdafc60bf0 | ||
|
|
d505f7f27b | ||
|
|
6b82952f2a | ||
|
|
b0c1fbf769 | ||
|
|
0f55b478c2 | ||
|
|
d8cd526545 | ||
|
|
0fe4a1a64b | ||
|
|
b19183b802 | ||
|
|
25f6d3752b | ||
|
|
cd96d11769 | ||
|
|
2558210d10 | ||
|
|
fb584a7c89 | ||
|
|
112beb7edd | ||
|
|
fa8891ca34 | ||
|
|
d9beca97d7 | ||
|
|
143221825d | ||
|
|
d2d3a84b07 | ||
|
|
70f18f8aeb | ||
|
|
169c3cefc1 | ||
|
|
a6b8d2c133 | ||
|
|
42620cbf50 | ||
|
|
7897e1cb76 | ||
|
|
ccfcc1103c | ||
|
|
c202c86ce4 | ||
|
|
c4132d2b6b | ||
|
|
ecbf970ae3 | ||
|
|
aca497ba2d | ||
|
|
b06f240755 | ||
|
|
61e26262b0 | ||
|
|
6ca0f0e176 | ||
|
|
7f9ae158ec | ||
|
|
2b1c7f739d | ||
|
|
5a3f7c17ed | ||
|
|
329c2049c3 | ||
|
|
2e51bd1ffa | ||
|
|
1a8c6d6e90 | ||
|
|
64a31b5729 | ||
|
|
2dcc94e534 | ||
|
|
81cbc9f5e9 | ||
|
|
0d68735b60 | ||
|
|
bb8591b0ef | ||
|
|
e513ab4a09 | ||
|
|
e162fac30a | ||
|
|
3ae93cc1ad | ||
|
|
59001d466a | ||
|
|
c8dcdedae0 | ||
|
|
0c61abf4f0 | ||
|
|
8e5e875fe0 | ||
|
|
e6d7065461 | ||
|
|
8b87a4612c | ||
|
|
e8d7293e2a | ||
|
|
c283feb699 | ||
|
|
b895a9bc48 | ||
|
|
0d33319ad4 | ||
|
|
2af9d58f06 | ||
|
|
427e9d6482 | ||
|
|
e21fd954d3 | ||
|
|
e7f47c7a5a | ||
|
|
54c6dac322 | ||
|
|
cf64958562 | ||
|
|
ea31a3b42a | ||
|
|
353dbedbb9 | ||
|
|
a87c446c31 | ||
|
|
85f1d06b02 | ||
|
|
17d3e9b3d0 | ||
|
|
de47b9e774 | ||
|
|
acfa8d9fe2 | ||
|
|
930246e6c5 | ||
|
|
c6b8272448 | ||
|
|
24b832a217 | ||
|
|
3acf002f95 | ||
|
|
16f9b3f6b1 | ||
|
|
3c38c7cc25 | ||
|
|
efbc930125 | ||
|
|
90b326e6b9 | ||
|
|
d3e7fe212d | ||
|
|
803d83f8e1 | ||
|
|
59b060fbc0 | ||
|
|
6d64bd4cdf | ||
|
|
ada4850f65 | ||
|
|
479fce68d5 | ||
|
|
2c0b49d7f8 | ||
|
|
e534eebc4d | ||
|
|
db16676cc4 | ||
|
|
0f1e5b858b | ||
|
|
a39d2e46e1 | ||
|
|
4370cd2383 | ||
|
|
0c4ef730e0 | ||
|
|
ee909e0047 | ||
|
|
6eee4a25f3 | ||
|
|
ffa99231c6 | ||
|
|
cbcd74f735 | ||
|
|
4f46a3c8ab | ||
|
|
5cec75c4c7 | ||
|
|
38d3731027 | ||
|
|
2a9f911a39 | ||
|
|
c762fcf6cc | ||
|
|
59db6642c3 | ||
|
|
eb5168ef83 | ||
|
|
7221ea64c8 | ||
|
|
af17bde7c5 | ||
|
|
67e2abde8b | ||
|
|
f28a9e7ba3 | ||
|
|
96f01b6a2c | ||
|
|
56bc429c4b | ||
|
|
163da4b021 | ||
|
|
6d9c168125 | ||
|
|
ccaef1adc5 | ||
|
|
f73222597c | ||
|
|
1714606744 | ||
|
|
ac3ef7fb36 | ||
|
|
9417814268 | ||
|
|
72737074fb | ||
|
|
e05386620b | ||
|
|
7cfe1ad833 | ||
|
|
00b975a140 | ||
|
|
424e57e41a | ||
|
|
cdb169c4e0 | ||
|
|
17aa18397e | ||
|
|
bb72eefc7f | ||
|
|
a007ed6a8f | ||
|
|
2219e86576 | ||
|
|
7f90e06ac5 | ||
|
|
5dd089c976 | ||
|
|
ef9aacd609 | ||
|
|
fa19960c5e | ||
|
|
a5428b80ff | ||
|
|
56512101c2 | ||
|
|
2bc8312511 | ||
|
|
43ccf9b48e | ||
|
|
e1a4ed6634 | ||
|
|
272e249d5e | ||
|
|
5636653c16 | ||
|
|
06b1b32ce7 | ||
|
|
202936b21f | ||
|
|
122f7f64b7 | ||
|
|
fbb913862a | ||
|
|
61892f9f22 | ||
|
|
166056fedd | ||
|
|
e03c883a9c | ||
|
|
1b4559fa3c | ||
|
|
ae8487f8d9 | ||
|
|
922b517d37 | ||
|
|
5a0d99fc80 | ||
|
|
c89a759c8b | ||
|
|
4c81fdcefb | ||
|
|
7a35b7f598 | ||
|
|
e1108c08ac | ||
|
|
6ab943f776 | ||
|
|
aac3570431 | ||
|
|
3abf287c67 | ||
|
|
cb26d23b02 | ||
|
|
9afcb0d7c6 | ||
|
|
acc50c4fda | ||
|
|
91fab12da6 | ||
|
|
e094b969ee | ||
|
|
3103d3a9cf | ||
|
|
c82b255eaa | ||
|
|
9ee4f0da9b | ||
|
|
3b5e6ac450 | ||
|
|
7ab270f323 | ||
|
|
6f9b4739c8 | ||
|
|
e654a657a0 | ||
|
|
ae6567a784 | ||
|
|
be12707ab1 | ||
|
|
e8d40ec679 | ||
|
|
e9c40c88a9 | ||
|
|
82bb9b2817 | ||
|
|
976dd6f80b | ||
|
|
2ac36fa415 | ||
|
|
19399dd17f | ||
|
|
acae8c1a0d | ||
|
|
64073e210e | ||
|
|
3adbf61be7 | ||
|
|
f0df4622eb | ||
|
|
81c0f284f6 | ||
|
|
fce60ca712 | ||
|
|
2be4d1cd2b | ||
|
|
72b19a8272 | ||
|
|
9f9d404632 | ||
|
|
157d1ecc49 | ||
|
|
54b21167ec | ||
|
|
34a125008f | ||
|
|
1776f8fd90 | ||
|
|
55269f748f | ||
|
|
6e42d0dcd9 | ||
|
|
48113eba18 | ||
|
|
41c8bb1815 | ||
|
|
eab4f9e123 | ||
|
|
05f4cfc07b | ||
|
|
b07b09e703 | ||
|
|
42e69916f0 | ||
|
|
254c9fee14 | ||
|
|
66e568aa06 | ||
|
|
1f38110e94 | ||
|
|
e92a98a956 | ||
|
|
875e90e940 | ||
|
|
5ebd23a88a | ||
|
|
6559857ba2 | ||
|
|
684b3e9836 | ||
|
|
d4b4599496 | ||
|
|
decd0a104d | ||
|
|
177f8e43e2 | ||
|
|
75234a6cd4 | ||
|
|
86b5c2998e | ||
|
|
0fee650cee | ||
|
|
61ee8b5910 | ||
|
|
dae66a63f1 | ||
|
|
934d7ba041 | ||
|
|
32b4dd73aa | ||
|
|
e558e7f6e4 | ||
|
|
20cc714b52 | ||
|
|
abf26c92e4 | ||
|
|
89bdbe1544 | ||
|
|
afe930a29f | ||
|
|
fc28ad0088 | ||
|
|
58b1c67abf | ||
|
|
ba563023c1 | ||
|
|
efe9487a44 | ||
|
|
2140066c4d | ||
|
|
32a70f10c0 | ||
|
|
c5ef9f4b1d | ||
|
|
91a3548613 | ||
|
|
1f370b86dd | ||
|
|
a93ddcb76e | ||
|
|
69aa985318 | ||
|
|
2855cf4569 | ||
|
|
01878a5ced | ||
|
|
bfb68e4118 | ||
|
|
709fffcfe5 | ||
|
|
5c0474b38e | ||
|
|
986ae4f0cb | ||
|
|
04937e8839 | ||
|
|
071062a939 | ||
|
|
058dc6c79b | ||
|
|
13a78dadb4 | ||
|
|
bf12bc4f9d | ||
|
|
7986428149 | ||
|
|
e17cbd5c9a | ||
|
|
ebfb105fe4 | ||
|
|
05a06eea27 | ||
|
|
663d7f3354 | ||
|
|
dbe5b88b52 | ||
|
|
c839fffadb | ||
|
|
8edfe53b45 | ||
|
|
0153fc54f1 | ||
|
|
f6b0af153f | ||
|
|
cfefce1baf | ||
|
|
ff1683d5b4 | ||
|
|
e181a866f7 | ||
|
|
1571d5766c | ||
|
|
1a4582adae | ||
|
|
88335b970f | ||
|
|
cac2dffb6c | ||
|
|
8a868e17bc | ||
|
|
1d05e96690 | ||
|
|
1d315d530f | ||
|
|
597fefa9c9 | ||
|
|
764834bbae | ||
|
|
582cfb4cf0 | ||
|
|
998fb16a03 | ||
|
|
40dd95f9c2 | ||
|
|
a27a6733e8 | ||
|
|
c76de152fc | ||
|
|
d0d75a4f50 | ||
|
|
df42111f83 | ||
|
|
da52a3685f | ||
|
|
bd85711e7f | ||
|
|
a02bf3e05c | ||
|
|
a397199834 | ||
|
|
27c16c2faf | ||
|
|
131004494b | ||
|
|
777bde7b5e | ||
|
|
3ba8f36108 | ||
|
|
773a0fa6d4 | ||
|
|
e88a537aec | ||
|
|
6ac60f9546 | ||
|
|
6fd6379ef3 | ||
|
|
2a4949a505 | ||
|
|
386a24305d | ||
|
|
45c4b89a4d | ||
|
|
73ea525d8b | ||
|
|
2ab267f601 | ||
|
|
ae54e91382 | ||
|
|
45c082fba9 | ||
|
|
aeedd9c3ac | ||
|
|
e76e4f42f2 | ||
|
|
f68a1f1c86 | ||
|
|
45a07d361c | ||
|
|
7f4937552d | ||
|
|
b786fd60b4 | ||
|
|
66646758a8 | ||
|
|
ece309fbde | ||
|
|
99c472330f | ||
|
|
246d5b5e4c | ||
|
|
8a8b243012 | ||
|
|
4612473e62 | ||
|
|
ab94e05e91 | ||
|
|
d2ecad5c3f | ||
|
|
a4c9d5a345 | ||
|
|
5b30a11da3 | ||
|
|
bda159a343 | ||
|
|
f473d31f13 | ||
|
|
1afe181085 | ||
|
|
647b7185df | ||
|
|
f301726a02 | ||
|
|
819be647b5 | ||
|
|
6215972732 | ||
|
|
90e406c30e | ||
|
|
d7a5a47393 | ||
|
|
8bacd4d1f5 | ||
|
|
a4d9ef0cb1 | ||
|
|
8bed9c753b | ||
|
|
47e598ede1 | ||
|
|
7edd41b08f | ||
|
|
a00dd69005 | ||
|
|
201c2b5964 | ||
|
|
5329a69e4d | ||
|
|
7ab0ffa0a3 | ||
|
|
33471e91be | ||
|
|
129bffe4b7 | ||
|
|
3f3a9ac807 | ||
|
|
c702efbd1e | ||
|
|
088098edad | ||
|
|
1e3e3c0e2e | ||
|
|
d9781e207c | ||
|
|
4a83c21d0d | ||
|
|
5d592e253b | ||
|
|
820091b8fa | ||
|
|
40c5eab3c5 | ||
|
|
c133fcf08a | ||
|
|
8094b7cc47 | ||
|
|
abd8716b56 | ||
|
|
cecad8351e | ||
|
|
4d1af69ed8 | ||
|
|
f468c06801 | ||
|
|
a0ef9b8d1b | ||
|
|
27f1f6f179 | ||
|
|
6ea1120517 | ||
|
|
2f7c44cbbe | ||
|
|
dd866a0f2b | ||
|
|
889d1beab4 | ||
|
|
357052ee42 | ||
|
|
19eda065ba | ||
|
|
5279c5c3b2 | ||
|
|
17be4e739f | ||
|
|
f2dd2e4d7e | ||
|
|
7c6ce077c1 | ||
|
|
45bf552eff | ||
|
|
633d412b52 | ||
|
|
54b8fb2d78 | ||
|
|
443e9f110b | ||
|
|
ac80bed084 | ||
|
|
802717c7a4 | ||
|
|
68b931f3b5 | ||
|
|
d4a4067754 | ||
|
|
ca18cfd6d1 | ||
|
|
18d80d2a4a | ||
|
|
ba4b9e4234 | ||
|
|
b87d531069 | ||
|
|
5cbb2505e3 | ||
|
|
e500a13c7e | ||
|
|
a4e9333c6e | ||
|
|
9dbe39e1a4 | ||
|
|
13c78eaee5 | ||
|
|
ef0e36b8be | ||
|
|
a1351563c1 | ||
|
|
303b40e572 | ||
|
|
622ea37554 | ||
|
|
8a80d16f11 | ||
|
|
4ea515c342 | ||
|
|
ce3dbaf902 | ||
|
|
a429b858e2 | ||
|
|
d8d228aa67 | ||
|
|
b31785a705 | ||
|
|
48b5e9f775 | ||
|
|
150ef5982a | ||
|
|
f91b94d100 | ||
|
|
6f25cc416f | ||
|
|
8358e982f9 | ||
|
|
637fdeebe6 | ||
|
|
96cf5274b1 | ||
|
|
1df5772857 | ||
|
|
44690dae55 | ||
|
|
ff46db7ac2 | ||
|
|
8f03595683 | ||
|
|
ac7494d08d | ||
|
|
e6ae0dab30 | ||
|
|
bc7da41da4 | ||
|
|
2fc5216cf1 | ||
|
|
8a792481b6 | ||
|
|
53e4e6b675 | ||
|
|
f06d338c5a | ||
|
|
fef65bd5d2 | ||
|
|
b830040639 | ||
|
|
2662178bef | ||
|
|
2a15ba9fe4 | ||
|
|
f777491dcd | ||
|
|
81445a21ff | ||
|
|
ff410542fb | ||
|
|
343e10f433 | ||
|
|
8023372a03 | ||
|
|
cd999f2346 | ||
|
|
4272d48fbf | ||
|
|
ae40737b75 | ||
|
|
4fe55be866 | ||
|
|
c5c4cf615f | ||
|
|
44e542ed5a | ||
|
|
bc2be8d33c | ||
|
|
73ed0edab7 | ||
|
|
a34a091cdb | ||
|
|
4f3d162d7a | ||
|
|
99605d7d18 | ||
|
|
fa152510a6 | ||
|
|
ddc868894e | ||
|
|
0a65fb607a | ||
|
|
921c76459c | ||
|
|
abf1ad61d6 | ||
|
|
fbcc2ef4fe | ||
|
|
699e7ce489 | ||
|
|
a45588abee | ||
|
|
44d5095101 | ||
|
|
b0b6de9a7d | ||
|
|
6b13e83146 | ||
|
|
cbac67728e | ||
|
|
400f4d20c1 | ||
|
|
3288b4602a | ||
|
|
74dec728ad | ||
|
|
aa72663440 | ||
|
|
f2fa6ed96d | ||
|
|
9b8e78a264 | ||
|
|
0e442beed5 | ||
|
|
ff1d38d159 | ||
|
|
5f8c8048e6 | ||
|
|
dc6d951241 | ||
|
|
9037ae0d53 | ||
|
|
83e7aa61fa | ||
|
|
11030b1e6a | ||
|
|
2f37e2a9c7 | ||
|
|
fcf891647c | ||
|
|
8465131d41 | ||
|
|
a33656d43b | ||
|
|
52397ab340 | ||
|
|
930a730252 | ||
|
|
37a707ba1d | ||
|
|
d9e4f58687 | ||
|
|
98bbca85b2 | ||
|
|
0f9aac76e2 | ||
|
|
7c933f888c | ||
|
|
9ae02ddb15 | ||
|
|
8333dd0d0c | ||
|
|
c21159c571 | ||
|
|
81db39d4e1 | ||
|
|
3e77ab6845 | ||
|
|
562ae9cd56 | ||
|
|
cb321ffdb8 | ||
|
|
a8d654b8d5 | ||
|
|
b2806cd000 | ||
|
|
d4166f681d | ||
|
|
19829be16a | ||
|
|
666ee288c3 | ||
|
|
2a8b5f983f | ||
|
|
adf8ae9878 | ||
|
|
7a6bee4a13 | ||
|
|
4a05031e42 | ||
|
|
8c86cc3c1a | ||
|
|
5205011610 | ||
|
|
f689cb6a8e | ||
|
|
412215603e | ||
|
|
97c34f8ae6 | ||
|
|
6563bc1b70 | ||
|
|
205bda34ae |
No files matched your search
@@ -9,10 +9,15 @@ updates:
|
||||
- mavenCentral
|
||||
schedule:
|
||||
interval: "daily"
|
||||
cooldown:
|
||||
default-days: 7
|
||||
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
cooldown:
|
||||
default-days: 7
|
||||
|
||||
# Workaround for https://github.com/dependabot/dependabot-core/issues/6888
|
||||
registries:
|
||||
|
||||
@@ -24,7 +24,7 @@ permissions:
|
||||
security-events: none
|
||||
statuses: none
|
||||
env:
|
||||
JAVA_HOME: /usr/lib/jvm/java-21-openjdk-amd64
|
||||
JAVA_HOME: /usr/lib/jvm/java-25-openjdk-amd64
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -32,41 +32,43 @@ jobs:
|
||||
matrix:
|
||||
flavor: [Foss, Gplay]
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- 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/actions/wrapper-validation@v4
|
||||
- name: set up OpenJDK 21
|
||||
- uses: actions/checkout@v7.0.0
|
||||
- uses: gradle/actions/wrapper-validation@v6.2.0
|
||||
- name: set up OpenJDK 25
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y openjdk-21-jdk-headless
|
||||
sudo apt-get install -y openjdk-25-jdk-headless
|
||||
sudo update-alternatives --auto java
|
||||
- name: Build
|
||||
run: ./gradlew assemble${{ matrix.flavor }}Release
|
||||
- name: Check for non-free libraries
|
||||
run: |
|
||||
wget -q https://codeberg.org/Katastima/apkscanner/releases/download/v0.0.7/apk-scanner_v0.0.7.jar
|
||||
java -jar apk-scanner_v0.0.7.jar scan-apk "$(echo 'app/build/outputs/apk/${{ matrix.flavor }}/release/app-${{ matrix.flavor }}-release-unsigned.apk' | tr '[:upper:]' '[:lower:]')"
|
||||
- name: Check lint
|
||||
run: ./gradlew lint${{ matrix.flavor }}Release
|
||||
- name: Run unit tests
|
||||
run: timeout 5m ./gradlew test${{ matrix.flavor }}ReleaseUnitTest || { ./gradlew --stop && timeout 5m ./gradlew test${{ matrix.flavor }}ReleaseUnitTest; }
|
||||
run: timeout 5m ./gradlew test${{ matrix.flavor }}DebugUnitTest || { ./gradlew --stop && timeout 5m ./gradlew test${{ matrix.flavor }}DebugUnitTest; }
|
||||
- name: Enable KVM
|
||||
run: |
|
||||
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
|
||||
sudo udevadm control --reload-rules
|
||||
sudo udevadm trigger --name-match=kvm
|
||||
- name: Run instrumented tests (API 21)
|
||||
uses: ReactiveCircus/android-emulator-runner@v2
|
||||
- name: Run instrumented tests (API 23)
|
||||
uses: ReactiveCircus/android-emulator-runner@v2.37.0
|
||||
with:
|
||||
api-level: 21
|
||||
api-level: 23
|
||||
arch: x86_64
|
||||
script: ./gradlew connected${{ matrix.flavor }}DebugAndroidTest
|
||||
- name: Run instrumented tests (API 35)
|
||||
uses: ReactiveCircus/android-emulator-runner@v2
|
||||
uses: ReactiveCircus/android-emulator-runner@v2.37.0
|
||||
with:
|
||||
api-level: 35
|
||||
arch: x86_64
|
||||
script: ./gradlew connected${{ matrix.flavor }}DebugAndroidTest
|
||||
- name: Archive test results
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4.6.2
|
||||
uses: actions/upload-artifact@v7.0.1
|
||||
with:
|
||||
name: test-results-flavor${{ matrix.flavor }}
|
||||
path: app/build/reports
|
||||
@@ -19,15 +19,15 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
id: checkout
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v7.0.0
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v6.0.0
|
||||
uses: actions/setup-python@v6.3.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@v7.0.8
|
||||
uses: peter-evans/create-pull-request@v8.1.1
|
||||
with:
|
||||
title: "Update Fastlane changelogs"
|
||||
commit-message: "Update Fastlane changelogs"
|
||||
|
||||
@@ -17,15 +17,17 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
id: checkout
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v7.0.0
|
||||
- name: Update contributors
|
||||
id: update_contributors
|
||||
uses: TheLastProject/contributors-to-file-action@v3.2.0
|
||||
with:
|
||||
file_in_repo: app/src/main/res/raw/contributors.txt
|
||||
min_commit_count: 5
|
||||
- name: Remove gradle-update-robot from contributors (not a person)
|
||||
run: "sed -i '/^gradle-update-robot$/d' app/src/main/res/raw/contributors.txt"
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v7.0.8
|
||||
uses: peter-evans/create-pull-request@v8.1.1
|
||||
with:
|
||||
title: "Update contributors"
|
||||
commit-message: "Update contributors"
|
||||
|
||||
@@ -17,7 +17,7 @@ jobs:
|
||||
generate-feature-graphic:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v7.0.0
|
||||
- name: Install requirements
|
||||
run: |
|
||||
sudo apt-get update
|
||||
@@ -31,7 +31,7 @@ jobs:
|
||||
- name: Generate featureGraphic.png for each language
|
||||
run: .scripts/generate_feature_graphic/generate_feature_graphic.sh
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v7.0.8
|
||||
uses: peter-evans/create-pull-request@v8.1.1
|
||||
with:
|
||||
title: "Update feature graphic"
|
||||
commit-message: "Update feature graphic"
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
name: i18n check
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- staging
|
||||
- trying
|
||||
pull_request:
|
||||
branches:
|
||||
- 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
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7.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
|
||||
- name: Check app_name consistency
|
||||
run: bash .scripts/check_app_name.sh
|
||||
@@ -1,19 +0,0 @@
|
||||
name: Update Gradle Wrapper
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 0 * * *"
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
update-gradle-wrapper:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- name: Update Gradle Wrapper
|
||||
uses: gradle-update/update-gradle-wrapper-action@v2
|
||||
@@ -17,13 +17,13 @@ jobs:
|
||||
update-locales:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v7.0.0
|
||||
- name: Add new locales
|
||||
run: .scripts/new-locales.py
|
||||
- name: Update locales
|
||||
run: .scripts/locales.py
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v7.0.8
|
||||
uses: peter-evans/create-pull-request@v8.1.1
|
||||
with:
|
||||
title: "Update locales"
|
||||
commit-message: "Update locales"
|
||||
|
||||
+2
-2
@@ -19,8 +19,8 @@
|
||||
/app/*.log
|
||||
/app/build
|
||||
/app/release
|
||||
/.idea
|
||||
|
||||
/.idea/*
|
||||
!/.idea/icon.svg
|
||||
# Bundle
|
||||
/.bundle/
|
||||
/vendor/bundle
|
||||
|
||||
+1
@@ -0,0 +1 @@
|
||||
../.design/ic_launcher_foreground.svg
|
||||
@@ -0,0 +1 @@
|
||||
3.3.11
|
||||
@@ -0,0 +1,72 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
shopt -s lastpipe # Run last command in a pipeline in the current shell.
|
||||
|
||||
# Colors
|
||||
LIGHTCYAN='\033[1;36m'
|
||||
GREEN='\033[0;32m'
|
||||
RED='\033[0;31m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Vars
|
||||
SUCCESS=1
|
||||
CANONICAL_TITLE="Catima"
|
||||
# peo is only temporarily allowed due to https://github.com/WeblateOrg/weblate/issues/17455
|
||||
ALLOWLIST=("ar" "bn" "fa" "fa-IR" "he-IL" "hi" "hi-IN" "kn" "kn-IN" "ml" "mr" "peo", "ta" "ta-IN" "zh-rTW" "zh-TW") # TODO: Link values and fastlane with different codes together
|
||||
|
||||
function get_lang() {
|
||||
LANG_DIRNAME=$(dirname "$FILE" | xargs basename)
|
||||
LANG=${LANG_DIRNAME#values-} # Fetch lang name
|
||||
LANG=${LANG#values} # Handle "app/src/main/res/values"
|
||||
LANG=${LANG:-en} # Default to en
|
||||
}
|
||||
|
||||
# FIXME: This function should use its own variables and return a success/fail status, instead of working on global variables
|
||||
function check() {
|
||||
# FIXME: This allows inconsistency between values and fastlane if the app name is not Catima
|
||||
# When the app name is not Catima, it should still check if title.txt and strings.xml use the same app name (or start)
|
||||
if echo "${ALLOWLIST[*]}" | grep -w -q "${LANG}" || [[ -z ${APP_NAME} ]]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [[ ${FILE} == *"title.txt" ]]; then
|
||||
if [[ ! ${APP_NAME} =~ ^${CANONICAL_TITLE} ]]; then
|
||||
echo -e "${RED}Error: ${LIGHTCYAN}title in $FILE ($LANG) is ${RED}'$APP_NAME'${LIGHTCYAN}, expected to start with ${GREEN}'$CANONICAL_TITLE'. ${NC}"
|
||||
SUCCESS=0
|
||||
fi
|
||||
else
|
||||
if [[ ${APP_NAME} != "${CANONICAL_TITLE}" ]]; then
|
||||
echo -e "${RED}Error: ${LIGHTCYAN}app_name in $FILE ($LANG) is ${RED}'$APP_NAME'${LIGHTCYAN}, expected ${GREEN}'$CANONICAL_TITLE'. ${NC}"
|
||||
SUCCESS=0
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# FIXME: This checks all title.txt and strings.xml files separately, but it needs to check if the title.txt and strings.xml match for a language as well
|
||||
echo -e "${LIGHTCYAN}Checking title.txt's. ${NC}"
|
||||
|
||||
find fastlane/metadata/android/* -maxdepth 1 -type f -name "title.txt" | while read -r FILE; do
|
||||
APP_NAME=$(head -n 1 "$FILE")
|
||||
|
||||
get_lang
|
||||
check
|
||||
done
|
||||
|
||||
echo -e "${LIGHTCYAN}Checking string.xml's. ${NC}"
|
||||
|
||||
find app/src/main/res/values* -maxdepth 1 -type f -name "strings.xml" | while read -r FILE; do
|
||||
# FIXME: This only checks app_name, but there are more strings with Catima inside it
|
||||
# It should check the original English text for all strings that contain Catima and ensure they use the correct app_name for consistency
|
||||
APP_NAME=$(grep -oP '<string name="app_name">\K[^<]+' "$FILE" | head -n1)
|
||||
|
||||
get_lang
|
||||
check
|
||||
done
|
||||
|
||||
if [[ $SUCCESS -eq 1 ]]; then
|
||||
echo -e "\n${GREEN}Success!! All app_name values match the canonical title. ${NC}"
|
||||
else
|
||||
echo -e "\n${RED}Unsuccessful!! Some app_name values did not match the canonical titles. ${NC}"
|
||||
exit 1
|
||||
fi
|
||||
@@ -42,6 +42,7 @@ for lang in "$script_location/../../fastlane/metadata/android/"*; do
|
||||
ja-JP) sed -i "s/Lexend/Noto Sans CJK JP/" featureGraphic.svg ;;
|
||||
kn-IN) sed -i -e 's/font-size="150"/font-size="125"/' -e 's/\(<tspan x="469" \)y="270"/\1y="240"/' -e "s/Lobster/Noto Sans Kannada/" -e "s/Lexend/Noto Sans Kannada/" featureGraphic.svg ;;
|
||||
ko) sed -i "s/Lexend/Noto Sans CJK KR/" featureGraphic.svg ;;
|
||||
ta-IN) sed -i -e 's/font-size="150"/font-size="125"/' -e 's/\(<tspan x="469" \)y="270"/\1y="240"/' featureGraphic.svg ;;
|
||||
zh-CN) sed -i "s/Lexend/Noto Sans CJK SC/" featureGraphic.svg ;;
|
||||
zh-TW) sed -i -e "s/Lobster/Noto Sans CJK TC/" -e "s/Lexend/Noto Sans CJK TC/" featureGraphic.svg ;;
|
||||
*) ;;
|
||||
|
||||
+78
-1
@@ -1,9 +1,86 @@
|
||||
# Changelog
|
||||
|
||||
## Unreleased - 153
|
||||
## v2.43.0 - 167 (2026-07-12)
|
||||
|
||||
- Add support for showing the card ID in the card list
|
||||
- Add support to sort by most recently added
|
||||
- Add internal image viewer for zooming card front/back/icon images
|
||||
|
||||
## v2.42.1 - 166 (2026-05-16)
|
||||
|
||||
- Prevent crash on devices with misbehaving NFC
|
||||
|
||||
## v2.42.0 - 165 (2026-05-06)
|
||||
|
||||
- Disable NFC while showing a card by default
|
||||
- Ask keyboard to capitalize each word in store name in edit screen
|
||||
- Put date in export file name
|
||||
|
||||
## v2.41.7 - 164 (2026-03-06)
|
||||
|
||||
Android 5.0 and 5.1 are no longer supported starting with this release. If you want to use Catima on these versions, please use version 2.41.6.
|
||||
|
||||
- Fix crash when switching cards using volume buttons if only one card is available
|
||||
- Fix shortcut returning to previous card after rotation
|
||||
|
||||
## v2.41.6 - 163 (2026-01-23)
|
||||
|
||||
- Auto-detect URLs in card ID and make the clickable in card ID details pop-up
|
||||
- Fix barcode selector activity showing white squares instead of barcodes
|
||||
|
||||
## v2.41.5 - 162 (2026-01-15)
|
||||
|
||||
- Fix list widget sometimes opening wrong card
|
||||
- Fix several bugs with shortcut handling
|
||||
- Fix About activity not using pure black title bar in OLED mode
|
||||
- Fully remove automatic barcode encoding detection
|
||||
- Expose barcode encoding through ContentProvider (used by Gadgetbridge)
|
||||
|
||||
## v2.41.4 - 161 (2026-01-04)
|
||||
|
||||
- Disable automatic barcode encoding detection for now (breaks too many cards)
|
||||
|
||||
## v2.41.3 - 160 (2026-01-04)
|
||||
|
||||
- Follow-up for fix in 2.41.2 for cards explicitly set to ISO-8859-1
|
||||
- Migrate old cards to ISO-8859-1 to fix sudden behaviour differences for existing cards
|
||||
|
||||
## v2.41.2 - 159 (2026-01-03)
|
||||
|
||||
- Fix change introduced in 2.41.0 that broke support for some scanners for non-UTF-8 barcodes
|
||||
|
||||
## v2.41.1 - 158 (2025-12-31)
|
||||
|
||||
- Fix status and navigation bar colour in new About activity
|
||||
|
||||
## v2.41.0 - 157 (2025-12-30)
|
||||
|
||||
- Add support for UTF-8 barcodes
|
||||
- Add duplicate option to main screen and reorder options slightly
|
||||
- Fix column count setting not being applied to group card list
|
||||
- Remove theme colour support
|
||||
- Reduce max photo size to reduce storage use (only for newly added photos)
|
||||
- Migrate About activity from Android XML to Jetpack Compose
|
||||
|
||||
## v2.40.0 - 156 (2025-12-08)
|
||||
|
||||
- Copy card ID to clipboard from view dialog or long press
|
||||
- Swap balance and currency fields to hopefully reduce unintended rounding
|
||||
|
||||
## v2.39.2 - 155 (2025-11-04)
|
||||
|
||||
- Preparations for future improvements (rewrote many classes to Kotlin)
|
||||
|
||||
## v2.39.1 - 154 (2025-10-01)
|
||||
|
||||
- Fix possible crash that could occur for cards missing colour information in the database
|
||||
|
||||
## v2.39.0 - 153 (2025-09-30)
|
||||
|
||||
- Target Android 16
|
||||
- Fix possible crash after removing image from card
|
||||
- Remove "Screen orientation" feature (Google removed the ability for apps to control screen rotation when targeting Android 16)
|
||||
- Add crash reporter to FOSS build (not used in Google Play version, only in other app stores)
|
||||
|
||||
## v2.38.0 - 152 (2025-09-12)
|
||||
|
||||
|
||||
@@ -23,6 +23,40 @@ for good reason.
|
||||
|
||||
## Code Changes
|
||||
|
||||
### LLM ("AI") and coding agents
|
||||
|
||||
**Note:** submitting LLM ("AI") generated code is strongly discouraged, as such
|
||||
code is often (subtly) incorrect or overcomplicated (for example: unnecessarily
|
||||
pulling in extra libraries for functionality already covered by existing
|
||||
libraries). It also often makes unrelated changes that increase the risk of
|
||||
introducing new issues and complicates reviewing. Even when it doesn't do any
|
||||
of the before mentioned things, it will often not fit the coding style and flow
|
||||
of existing code, requiring excessive refactoring.
|
||||
|
||||
While we cannot ever control or be sure if LLMs were used to generate the
|
||||
submitted code, it is your responsibility to ensure that whatever code you
|
||||
submit is correct and fits within the design of existing code. It is never
|
||||
acceptable to defend a change by stating a LLM suggested it.
|
||||
|
||||
This is a personal plea more than anything: please understand that writing code
|
||||
is the easy part. The hard part is making sure the code fits the design of the
|
||||
rest of the application and is maintainable. Reviewing is a very time-consuming
|
||||
task for this reason. Please do not use LLMs to quickly generate a "fix" and
|
||||
moving the cost of labor to me as a reviewer. If you do use LLMs to generate
|
||||
part of your code, please be open about this, explain what was generated how
|
||||
and how you confirmed and refactored the code to fit the project and minimized
|
||||
risk.
|
||||
|
||||
Please never submit LLM-generated code as-is.
|
||||
|
||||
### Kotlin and Compose migration
|
||||
|
||||
Catima uses a lot of "legacy" tech, Java, Android XML. This will be ported to Kotlin,
|
||||
then Jetpack Compose; for further details, see
|
||||
<https://github.com/CatimaLoyalty/Android/issues/2483>.
|
||||
|
||||
Thus, prefer Kotlin and Jetpack Compose for new code/UI where practical, especially new files/features.
|
||||
|
||||
### Test Your Code
|
||||
|
||||
There are four possible tests you can run to verify your code. The first
|
||||
|
||||
@@ -1,8 +1,3 @@
|
||||
source "https://rubygems.org"
|
||||
|
||||
gem "fastlane"
|
||||
|
||||
# https://github.com/fastlane/fastlane/issues/29183
|
||||
gem "abbrev"
|
||||
gem "mutex_m"
|
||||
gem "ostruct"
|
||||
+68
-62
@@ -1,40 +1,41 @@
|
||||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
CFPropertyList (3.0.7)
|
||||
base64
|
||||
nkf
|
||||
rexml
|
||||
CFPropertyList (3.0.8)
|
||||
abbrev (0.1.2)
|
||||
addressable (2.8.7)
|
||||
public_suffix (>= 2.0.2, < 7.0)
|
||||
addressable (2.9.0)
|
||||
public_suffix (>= 2.0.2, < 8.0)
|
||||
artifactory (3.0.17)
|
||||
atomos (0.1.3)
|
||||
aws-eventstream (1.4.0)
|
||||
aws-partitions (1.1117.0)
|
||||
aws-sdk-core (3.226.0)
|
||||
aws-partitions (1.1249.0)
|
||||
aws-sdk-core (3.247.0)
|
||||
aws-eventstream (~> 1, >= 1.3.0)
|
||||
aws-partitions (~> 1, >= 1.992.0)
|
||||
aws-sigv4 (~> 1.9)
|
||||
base64
|
||||
bigdecimal
|
||||
jmespath (~> 1, >= 1.6.1)
|
||||
logger
|
||||
aws-sdk-kms (1.105.0)
|
||||
aws-sdk-core (~> 3, >= 3.225.0)
|
||||
aws-sdk-kms (1.125.0)
|
||||
aws-sdk-core (~> 3, >= 3.247.0)
|
||||
aws-sigv4 (~> 1.5)
|
||||
aws-sdk-s3 (1.189.1)
|
||||
aws-sdk-core (~> 3, >= 3.225.0)
|
||||
aws-sdk-s3 (1.222.0)
|
||||
aws-sdk-core (~> 3, >= 3.247.0)
|
||||
aws-sdk-kms (~> 1)
|
||||
aws-sigv4 (~> 1.5)
|
||||
aws-sigv4 (1.12.1)
|
||||
aws-eventstream (~> 1, >= 1.0.2)
|
||||
babosa (1.0.4)
|
||||
base64 (0.3.0)
|
||||
benchmark (0.5.0)
|
||||
bigdecimal (4.1.2)
|
||||
claide (1.1.0)
|
||||
colored (1.2)
|
||||
colored2 (3.1.2)
|
||||
commander (4.6.0)
|
||||
highline (~> 2.0.0)
|
||||
csv (3.3.5)
|
||||
declarative (0.0.20)
|
||||
digest-crc (0.7.0)
|
||||
rake (>= 12.0.0, < 14.0.0)
|
||||
@@ -42,7 +43,7 @@ GEM
|
||||
dotenv (2.8.1)
|
||||
emoji_regex (3.2.3)
|
||||
excon (0.112.0)
|
||||
faraday (1.10.4)
|
||||
faraday (1.10.5)
|
||||
faraday-em_http (~> 1.0)
|
||||
faraday-em_synchrony (~> 1.0)
|
||||
faraday-excon (~> 1.1)
|
||||
@@ -54,32 +55,36 @@ GEM
|
||||
faraday-rack (~> 1.0)
|
||||
faraday-retry (~> 1.0)
|
||||
ruby2_keywords (>= 0.0.4)
|
||||
faraday-cookie_jar (0.0.7)
|
||||
faraday-cookie_jar (0.0.8)
|
||||
faraday (>= 0.8.0)
|
||||
http-cookie (~> 1.0.0)
|
||||
http-cookie (>= 1.0.0)
|
||||
faraday-em_http (1.0.0)
|
||||
faraday-em_synchrony (1.0.1)
|
||||
faraday-excon (1.1.0)
|
||||
faraday-httpclient (1.0.1)
|
||||
faraday-multipart (1.1.1)
|
||||
faraday-multipart (1.2.0)
|
||||
multipart-post (~> 2.0)
|
||||
faraday-net_http (1.0.2)
|
||||
faraday-net_http_persistent (1.2.0)
|
||||
faraday-patron (1.0.0)
|
||||
faraday-rack (1.0.0)
|
||||
faraday-retry (1.0.3)
|
||||
faraday-retry (1.0.4)
|
||||
faraday_middleware (1.2.1)
|
||||
faraday (~> 1.0)
|
||||
fastimage (2.4.0)
|
||||
fastlane (2.228.0)
|
||||
CFPropertyList (>= 2.3, < 4.0.0)
|
||||
fastimage (2.4.1)
|
||||
fastlane (2.234.0)
|
||||
CFPropertyList (>= 2.3, < 5.0.0)
|
||||
abbrev (~> 0.1)
|
||||
addressable (>= 2.8, < 3.0.0)
|
||||
artifactory (~> 3.0)
|
||||
aws-sdk-s3 (~> 1.0)
|
||||
aws-sdk-s3 (~> 1.197)
|
||||
babosa (>= 1.0.3, < 2.0.0)
|
||||
bundler (>= 1.12.0, < 3.0.0)
|
||||
base64 (~> 0.2)
|
||||
benchmark (>= 0.1.0)
|
||||
bundler (>= 1.17.3, < 5.0.0)
|
||||
colored (~> 1.2)
|
||||
commander (~> 4.6)
|
||||
csv (~> 3.3)
|
||||
dotenv (>= 2.1.1, < 3.0.0)
|
||||
emoji_regex (>= 0.1, < 4.0)
|
||||
excon (>= 0.71.0, < 1.0.0)
|
||||
@@ -87,20 +92,24 @@ GEM
|
||||
faraday-cookie_jar (~> 0.0.6)
|
||||
faraday_middleware (~> 1.0)
|
||||
fastimage (>= 2.1.0, < 3.0.0)
|
||||
fastlane-sirp (>= 1.0.0)
|
||||
fastlane-sirp (>= 1.1.0)
|
||||
gh_inspector (>= 1.1.2, < 2.0.0)
|
||||
google-apis-androidpublisher_v3 (~> 0.3)
|
||||
google-apis-playcustomapp_v1 (~> 0.1)
|
||||
google-cloud-env (>= 1.6.0, < 2.0.0)
|
||||
google-cloud-env (>= 1.6.0, <= 2.1.1)
|
||||
google-cloud-storage (~> 1.31)
|
||||
highline (~> 2.0)
|
||||
http-cookie (~> 1.0.5)
|
||||
json (< 3.0.0)
|
||||
jwt (>= 2.1.0, < 3)
|
||||
logger (>= 1.6, < 2.0)
|
||||
mini_magick (>= 4.9.4, < 5.0.0)
|
||||
multipart-post (>= 2.0.0, < 3.0.0)
|
||||
mutex_m (~> 0.3)
|
||||
naturally (~> 2.2)
|
||||
nkf (~> 0.2)
|
||||
optparse (>= 0.1.1, < 1.0.0)
|
||||
ostruct (>= 0.1.0)
|
||||
plist (>= 3.1.0, < 4.0.0)
|
||||
rubyzip (>= 2.0.0, < 3.0.0)
|
||||
security (= 0.1.5)
|
||||
@@ -113,41 +122,42 @@ GEM
|
||||
xcodeproj (>= 1.13.0, < 2.0.0)
|
||||
xcpretty (~> 0.4.1)
|
||||
xcpretty-travis-formatter (>= 0.0.3, < 2.0.0)
|
||||
fastlane-sirp (1.0.0)
|
||||
sysrandom (~> 1.0)
|
||||
fastlane-sirp (1.1.0)
|
||||
gh_inspector (1.1.3)
|
||||
google-apis-androidpublisher_v3 (0.54.0)
|
||||
google-apis-core (>= 0.11.0, < 2.a)
|
||||
google-apis-core (0.11.3)
|
||||
google-apis-androidpublisher_v3 (0.100.0)
|
||||
google-apis-core (>= 0.15.0, < 2.a)
|
||||
google-apis-core (0.18.0)
|
||||
addressable (~> 2.5, >= 2.5.1)
|
||||
googleauth (>= 0.16.2, < 2.a)
|
||||
httpclient (>= 2.8.1, < 3.a)
|
||||
googleauth (~> 1.9)
|
||||
httpclient (>= 2.8.3, < 3.a)
|
||||
mini_mime (~> 1.0)
|
||||
mutex_m
|
||||
representable (~> 3.0)
|
||||
retriable (>= 2.0, < 4.a)
|
||||
rexml
|
||||
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.31.0)
|
||||
google-apis-core (>= 0.11.0, < 2.a)
|
||||
google-apis-iamcredentials_v1 (0.27.0)
|
||||
google-apis-core (>= 0.15.0, < 2.a)
|
||||
google-apis-playcustomapp_v1 (0.17.0)
|
||||
google-apis-core (>= 0.15.0, < 2.a)
|
||||
google-apis-storage_v1 (0.62.0)
|
||||
google-apis-core (>= 0.15.0, < 2.a)
|
||||
google-cloud-core (1.8.0)
|
||||
google-cloud-env (>= 1.0, < 3.a)
|
||||
google-cloud-errors (~> 1.0)
|
||||
google-cloud-env (1.6.0)
|
||||
faraday (>= 0.17.3, < 3.0)
|
||||
google-cloud-errors (1.5.0)
|
||||
google-cloud-storage (1.47.0)
|
||||
google-cloud-env (2.1.1)
|
||||
faraday (>= 1.0, < 3.a)
|
||||
google-cloud-errors (1.6.0)
|
||||
google-cloud-storage (1.60.0)
|
||||
addressable (~> 2.8)
|
||||
digest-crc (~> 0.4)
|
||||
google-apis-iamcredentials_v1 (~> 0.1)
|
||||
google-apis-storage_v1 (~> 0.31.0)
|
||||
google-apis-core (>= 0.18, < 2)
|
||||
google-apis-iamcredentials_v1 (~> 0.18)
|
||||
google-apis-storage_v1 (>= 0.42)
|
||||
google-cloud-core (~> 1.6)
|
||||
googleauth (>= 0.16.2, < 2.a)
|
||||
googleauth (~> 1.9)
|
||||
mini_mime (~> 1.0)
|
||||
googleauth (1.8.1)
|
||||
faraday (>= 0.17.3, < 3.a)
|
||||
googleauth (1.11.2)
|
||||
faraday (>= 1.0, < 3.a)
|
||||
google-cloud-env (~> 2.1)
|
||||
jwt (>= 1.4, < 3.0)
|
||||
multi_json (~> 1.11)
|
||||
os (>= 0.9, < 2.0)
|
||||
@@ -158,43 +168,42 @@ GEM
|
||||
httpclient (2.9.0)
|
||||
mutex_m
|
||||
jmespath (1.6.2)
|
||||
json (2.12.2)
|
||||
jwt (2.10.1)
|
||||
json (2.19.5)
|
||||
jwt (2.10.2)
|
||||
base64
|
||||
logger (1.7.0)
|
||||
mini_magick (4.13.2)
|
||||
mini_mime (1.1.5)
|
||||
multi_json (1.15.0)
|
||||
multi_json (1.21.1)
|
||||
multipart-post (2.4.1)
|
||||
mutex_m (0.3.0)
|
||||
nanaimo (0.4.0)
|
||||
naturally (2.3.0)
|
||||
nkf (0.2.0)
|
||||
optparse (0.6.0)
|
||||
optparse (0.8.1)
|
||||
os (1.1.4)
|
||||
ostruct (0.6.1)
|
||||
ostruct (0.6.3)
|
||||
plist (3.7.2)
|
||||
public_suffix (6.0.2)
|
||||
rake (13.3.0)
|
||||
public_suffix (7.0.5)
|
||||
rake (13.4.2)
|
||||
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.4.1)
|
||||
retriable (3.4.1)
|
||||
rexml (3.4.4)
|
||||
rouge (3.28.0)
|
||||
ruby2_keywords (0.0.5)
|
||||
rubyzip (2.4.1)
|
||||
security (0.1.5)
|
||||
signet (0.20.0)
|
||||
signet (0.21.0)
|
||||
addressable (~> 2.8)
|
||||
faraday (>= 0.17.5, < 3.a)
|
||||
jwt (>= 1.5, < 3.0)
|
||||
jwt (>= 1.5, < 4.0)
|
||||
multi_json (~> 1.10)
|
||||
simctl (1.6.10)
|
||||
CFPropertyList
|
||||
naturally
|
||||
sysrandom (1.0.5)
|
||||
terminal-notifier (2.0.0)
|
||||
terminal-table (3.0.2)
|
||||
unicode-display_width (>= 1.1.1, < 3)
|
||||
@@ -222,10 +231,7 @@ PLATFORMS
|
||||
ruby
|
||||
|
||||
DEPENDENCIES
|
||||
abbrev
|
||||
fastlane
|
||||
mutex_m
|
||||
ostruct
|
||||
|
||||
BUNDLED WITH
|
||||
2.5.22
|
||||
+8
-1
@@ -1,5 +1,5 @@
|
||||
**Last updated**
|
||||
August 30 2023
|
||||
April 29 2026
|
||||
|
||||
# Privacy Policy
|
||||
Catima does not collect or transmit any personal information.
|
||||
@@ -7,10 +7,17 @@ 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.
|
||||
- NFC: We need NFC access to temporarily disable NFC while showing a barcode to prevent NFC payment apps from triggering (such as when using a check-in gates which support both barcodes and NFC payments). This functionality can be disabled in settings.
|
||||
- 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.
|
||||
|
||||
## Crash reporting privacy
|
||||
|
||||
In the FOSS version of Catima (the version used on IzzyOnDroid, F-Droid and GitHub), the open source crash reporter ACRA is used for crash reporting. When a crash is detected, Catima will ask the user if they are willing to report the crash. If they choose to do so, the user's mail client is opened so they can review the data that would be sent. Crash reporting data is only sent when the user explicitly chooses to do so, it is **never** sent automatically. Crash reporting data is only used to solve crashes and no (potentially) sensitive information is ever shared. Users who do not want to be asked to report crashes can disable the "Ask to send crash reports" setting in Catima settings.
|
||||
|
||||
For the Google Play version of Catima, crash reporting is [managed by Google](https://support.google.com/googleplay/android-developer/answer/9859174?hl=en). Users can opt in or out of crash reporting through the Google app under the "Usage and diagnostics" setting.
|
||||
|
||||
# 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.
|
||||
|
||||
|
||||
+60
-49
@@ -1,8 +1,10 @@
|
||||
import com.android.build.gradle.internal.tasks.factory.dependsOn
|
||||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||
|
||||
plugins {
|
||||
id("com.android.application")
|
||||
id("org.jetbrains.kotlin.android")
|
||||
alias(libs.plugins.com.android.application)
|
||||
alias(libs.plugins.org.jetbrains.kotlin.android)
|
||||
alias(libs.plugins.org.jetbrains.kotlin.plugin.compose)
|
||||
}
|
||||
|
||||
kotlin {
|
||||
@@ -15,15 +17,15 @@ android {
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "me.hackerchick.catima"
|
||||
minSdk = 21
|
||||
minSdk = 23
|
||||
targetSdk = 36
|
||||
versionCode = 152
|
||||
versionName = "2.38.0"
|
||||
versionCode = 167
|
||||
versionName = "2.43.0"
|
||||
|
||||
vectorDrawables.useSupportLibrary = true
|
||||
multiDexEnabled = true
|
||||
|
||||
resourceConfigurations += listOf("ar", "be", "bg", "bn", "bn-rIN", "bs", "cs", "da", "de", "el-rGR", "en", "eo", "es", "es-rAR", "et", "fa", "fi", "fr", "gl", "he-rIL", "hi", "hr", "hu", "in-rID", "is", "it", "ja", "ko", "lt", "lv", "nb-rNO", "nl", "oc", "pl", "pt", "pt-rBR", "pt-rPT", "ro-rRO", "ru", "sk", "sl", "sr", "sv", "ta", "tr", "uk", "vi", "zh-rCN", "zh-rTW")
|
||||
resourceConfigurations += listOf("ar", "be", "bg", "bn", "bn-rIN", "bs", "cs", "da", "de", "el-rGR", "en", "eo", "es", "es-rAR", "et", "fa", "fi", "fr", "gl", "he-rIL", "hi", "hr", "hu", "in-rID", "is", "it", "ja", "ko", "lt", "lv", "nb-rNO", "nl", "oc", "peo", "pl", "pt", "pt-rBR", "pt-rPT", "ro-rRO", "ru", "sk", "sl", "sr", "sv", "ta", "tr", "uk", "vi", "zh-rCN", "zh-rTW")
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
|
||||
@@ -36,7 +38,7 @@ android {
|
||||
release {
|
||||
isMinifyEnabled = true
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android.txt"),
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro"
|
||||
)
|
||||
}
|
||||
@@ -47,6 +49,7 @@ android {
|
||||
|
||||
buildFeatures {
|
||||
buildConfig = true
|
||||
compose = true
|
||||
viewBinding = true
|
||||
}
|
||||
|
||||
@@ -74,16 +77,6 @@ android {
|
||||
}
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
encoding = "UTF-8"
|
||||
|
||||
// Flag to enable support for the new language APIs
|
||||
isCoreLibraryDesugaringEnabled = true
|
||||
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
getByName("test") {
|
||||
resources.srcDirs("src/test/res")
|
||||
@@ -102,10 +95,18 @@ android {
|
||||
lint {
|
||||
lintConfig = file("lint.xml")
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = "21"
|
||||
|
||||
kotlin {
|
||||
compilerOptions {
|
||||
jvmTarget = JvmTarget.JVM_21
|
||||
}
|
||||
}
|
||||
compileOptions {
|
||||
encoding = "UTF-8"
|
||||
|
||||
// Flag to enable support for the new language APIs
|
||||
isCoreLibraryDesugaringEnabled = true
|
||||
|
||||
sourceCompatibility = JavaVersion.VERSION_21
|
||||
targetCompatibility = JavaVersion.VERSION_21
|
||||
}
|
||||
@@ -113,43 +114,53 @@ android {
|
||||
|
||||
dependencies {
|
||||
// AndroidX
|
||||
implementation("androidx.appcompat:appcompat:1.7.1")
|
||||
implementation("androidx.constraintlayout:constraintlayout:2.2.1")
|
||||
implementation("androidx.core:core-ktx:1.17.0")
|
||||
implementation("androidx.core:core-remoteviews:1.1.0")
|
||||
implementation("androidx.core:core-splashscreen:1.0.1")
|
||||
implementation("androidx.exifinterface:exifinterface:1.4.1")
|
||||
implementation("androidx.palette:palette:1.0.0")
|
||||
implementation("androidx.preference:preference:1.2.1")
|
||||
implementation("com.google.android.material:material:1.13.0")
|
||||
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.5")
|
||||
implementation(libs.androidx.appcompat.appcompat)
|
||||
implementation(libs.androidx.constraintlayout.constraintlayout)
|
||||
implementation(libs.androidx.core.core.ktx)
|
||||
implementation(libs.androidx.core.core.remoteviews)
|
||||
implementation(libs.androidx.core.core.splashscreen)
|
||||
implementation(libs.androidx.exifinterface.exifinterface)
|
||||
implementation(libs.androidx.palette.palette)
|
||||
implementation(libs.androidx.preference.preference)
|
||||
implementation(libs.com.google.android.material.material)
|
||||
coreLibraryDesugaring(libs.com.android.tools.desugar.jdk.libs)
|
||||
|
||||
// Compose
|
||||
implementation(libs.androidx.activity.activity.compose)
|
||||
val composeBom = platform(libs.androidx.compose.compose.bom)
|
||||
implementation(composeBom)
|
||||
implementation(libs.androidx.compose.foundation.foundation)
|
||||
implementation(libs.androidx.compose.material3.material3)
|
||||
implementation(libs.androidx.compose.material.material.icons.extended)
|
||||
implementation(libs.androidx.compose.ui.ui.tooling.preview.android)
|
||||
debugImplementation(libs.androidx.compose.ui.ui.test.manifest)
|
||||
|
||||
androidTestImplementation(composeBom)
|
||||
androidTestImplementation(libs.androidx.compose.ui.ui.test.junit4)
|
||||
|
||||
// Third-party
|
||||
implementation("com.journeyapps:zxing-android-embedded:4.3.0@aar")
|
||||
implementation("com.github.yalantis:ucrop:2.2.10")
|
||||
implementation("com.google.zxing:core:3.5.3")
|
||||
implementation("org.apache.commons:commons-csv:1.9.0")
|
||||
implementation("com.jaredrummler:colorpicker:1.1.0")
|
||||
implementation("net.lingala.zip4j:zip4j:2.11.5")
|
||||
implementation(libs.com.journeyapps.zxing.android.embedded)
|
||||
implementation(libs.com.github.yalantis.ucrop)
|
||||
implementation(libs.com.google.zxing.core)
|
||||
implementation(libs.org.apache.commons.commons.csv)
|
||||
implementation(libs.com.jaredrummler.colorpicker)
|
||||
implementation(libs.net.lingala.zip4j.zip4j)
|
||||
|
||||
// Crash reporting
|
||||
val acraVersion = "5.12.0"
|
||||
implementation("ch.acra:acra-mail:$acraVersion")
|
||||
implementation("ch.acra:acra-dialog:$acraVersion")
|
||||
implementation(libs.bundles.acra)
|
||||
|
||||
// Testing
|
||||
val androidXTestVersion = "1.7.0"
|
||||
val junitVersion = "4.13.2"
|
||||
testImplementation("androidx.test:core:$androidXTestVersion")
|
||||
testImplementation("junit:junit:$junitVersion")
|
||||
testImplementation("org.robolectric:robolectric:4.16")
|
||||
testImplementation(libs.androidx.test.core)
|
||||
testImplementation(libs.junit.junit)
|
||||
testImplementation(libs.org.robolectric.robolectric)
|
||||
|
||||
androidTestImplementation("androidx.test:core:$androidXTestVersion")
|
||||
androidTestImplementation("junit:junit:$junitVersion")
|
||||
androidTestImplementation("androidx.test.ext:junit:1.3.0")
|
||||
androidTestImplementation("androidx.test:runner:$androidXTestVersion")
|
||||
androidTestImplementation("androidx.test.uiautomator:uiautomator:2.3.0")
|
||||
androidTestImplementation("androidx.test.espresso:espresso-core:3.7.0")
|
||||
androidTestImplementation(libs.bundles.androidx.test)
|
||||
androidTestImplementation(libs.junit.junit)
|
||||
androidTestImplementation(libs.androidx.test.ext.junit)
|
||||
androidTestImplementation(libs.androidx.test.rules)
|
||||
androidTestImplementation(libs.androidx.test.runner)
|
||||
androidTestImplementation(libs.androidx.test.uiautomator.uiautomator)
|
||||
androidTestImplementation(libs.androidx.test.espresso.espresso.core)
|
||||
}
|
||||
|
||||
tasks.register("copyRawResFiles", Copy::class) {
|
||||
|
||||
Vendored
+16
-1
@@ -21,4 +21,19 @@
|
||||
-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# This keep the class and method names the same, for debugging stack traces
|
||||
-dontobfuscate
|
||||
-dontobfuscate
|
||||
|
||||
# Required for uCrop 2.2.11
|
||||
# This is generated automatically by the Android Gradle plugin.
|
||||
-dontwarn javax.annotation.processing.AbstractProcessor
|
||||
-dontwarn javax.annotation.processing.SupportedOptions
|
||||
-dontwarn okhttp3.Call
|
||||
-dontwarn okhttp3.Dispatcher
|
||||
-dontwarn okhttp3.OkHttpClient
|
||||
-dontwarn okhttp3.Request$Builder
|
||||
-dontwarn okhttp3.Request
|
||||
-dontwarn okhttp3.Response
|
||||
-dontwarn okhttp3.ResponseBody
|
||||
-dontwarn okio.BufferedSource
|
||||
-dontwarn okio.Okio
|
||||
-dontwarn okio.Sink
|
||||
@@ -0,0 +1,89 @@
|
||||
package protect.card_locker
|
||||
|
||||
import android.app.Instrumentation
|
||||
import androidx.compose.ui.test.ExperimentalTestApi
|
||||
import androidx.compose.ui.test.assertIsDisplayed
|
||||
import androidx.compose.ui.test.assertIsNotDisplayed
|
||||
import androidx.compose.ui.test.junit4.ComposeContentTestRule
|
||||
import androidx.compose.ui.test.junit4.createComposeRule
|
||||
import androidx.compose.ui.test.onNodeWithTag
|
||||
import androidx.compose.ui.test.onNodeWithText
|
||||
import androidx.compose.ui.test.performScrollTo
|
||||
import androidx.compose.ui.test.runComposeUiTest
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import protect.card_locker.compose.theme.CatimaTheme
|
||||
|
||||
@OptIn(ExperimentalTestApi::class)
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class AboutActivityTest {
|
||||
@get:Rule
|
||||
private val rule: ComposeContentTestRule = createComposeRule()
|
||||
|
||||
private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation()
|
||||
|
||||
private val content: AboutContent = AboutContent(instrumentation.targetContext)
|
||||
|
||||
@Test
|
||||
fun testInitialState(): Unit = runComposeUiTest {
|
||||
setContent {
|
||||
AboutScreenContent(content = content)
|
||||
}
|
||||
|
||||
onNodeWithTag("topbar_catima").assertIsDisplayed()
|
||||
|
||||
onNodeWithTag("card_version_history").assertIsDisplayed()
|
||||
onNodeWithText(content.versionHistory).assertIsDisplayed()
|
||||
|
||||
onNodeWithTag("card_credits").assertIsDisplayed()
|
||||
onNodeWithText(content.copyrightShort).assertIsDisplayed()
|
||||
|
||||
onNodeWithTag("card_translate").assertIsDisplayed()
|
||||
onNodeWithTag("card_license").assertIsDisplayed()
|
||||
|
||||
// We might be off the screen so start scrolling
|
||||
onNodeWithTag("card_source_github").performScrollTo().assertIsDisplayed()
|
||||
onNodeWithTag("card_privacy_policy").performScrollTo().assertIsDisplayed()
|
||||
onNodeWithTag("card_donate").performScrollTo().assertIsDisplayed()
|
||||
// Dont scroll to this, since its not displayed
|
||||
onNodeWithTag("card_rate_google").assertIsNotDisplayed()
|
||||
onNodeWithTag("card_report_error").performScrollTo().assertIsDisplayed()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testDonateAndGoogleCardVisible(): Unit = runComposeUiTest {
|
||||
setContent {
|
||||
CatimaTheme {
|
||||
AboutScreenContent(
|
||||
content = content,
|
||||
showDonate = true,
|
||||
showRateOnGooglePlay = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
onNodeWithTag("card_donate").performScrollTo().assertIsDisplayed()
|
||||
onNodeWithTag("card_rate_google").performScrollTo().assertIsDisplayed()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testDonateAndGoogleCardHidden(): Unit = runComposeUiTest {
|
||||
setContent {
|
||||
CatimaTheme {
|
||||
AboutScreenContent(
|
||||
content = content,
|
||||
showDonate = false,
|
||||
showRateOnGooglePlay = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
onNodeWithTag("card_privacy_policy").performScrollTo().assertIsDisplayed()
|
||||
onNodeWithTag("card_donate").assertIsNotDisplayed()
|
||||
onNodeWithTag("card_rate_google").assertIsNotDisplayed()
|
||||
onNodeWithTag("card_report_error").performScrollTo().assertIsDisplayed()
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name">Catima Debug</string>
|
||||
</resources>
|
||||
<string name="app_name">Debug di Catima</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name">Depuração do Catima</string>
|
||||
</resources>
|
||||
@@ -12,8 +12,12 @@
|
||||
<uses-sdk tools:overrideLibrary="com.google.zxing.client.android" />
|
||||
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
<uses-permission android:name="android.permission.NFC" />
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="23" />
|
||||
|
||||
<uses-feature
|
||||
android:name="android.hardware.nfc"
|
||||
android:required="false" />
|
||||
<uses-feature
|
||||
android:name="android.hardware.camera"
|
||||
android:required="false" />
|
||||
@@ -94,9 +98,12 @@
|
||||
android:theme="@style/AppTheme.NoActionBar"
|
||||
android:windowSoftInputMode="adjustResize"/>
|
||||
<activity
|
||||
android:name=".LoyaltyCardViewActivity"
|
||||
android:name=".cardview.LoyaltyCardViewActivity"
|
||||
android:exported="true"
|
||||
android:theme="@style/AppTheme.NoActionBar" />
|
||||
<activity
|
||||
android:name=".cardimageview.LoyaltyCardImageViewActivity"
|
||||
android:theme="@style/AppTheme.NoActionBar" />
|
||||
<activity
|
||||
android:name=".LoyaltyCardEditActivity"
|
||||
android:exported="true"
|
||||
|
||||
@@ -1,149 +1,169 @@
|
||||
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.activity.OnBackPressedDispatcher
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.TextLinkStyles
|
||||
import androidx.compose.ui.text.fromHtml
|
||||
import androidx.compose.ui.text.style.TextDecoration
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import protect.card_locker.compose.CatimaAboutSection
|
||||
import protect.card_locker.compose.CatimaTopAppBar
|
||||
import protect.card_locker.compose.theme.CatimaTheme
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.core.view.isVisible
|
||||
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
|
||||
import protect.card_locker.databinding.AboutActivityBinding
|
||||
|
||||
class AboutActivity : CatimaAppCompatActivity() {
|
||||
private companion object {
|
||||
private const val TAG = "Catima"
|
||||
}
|
||||
|
||||
private lateinit var binding: AboutActivityBinding
|
||||
class AboutActivity : CatimaComponentActivity() {
|
||||
private lateinit var content: AboutContent
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = AboutActivityBinding.inflate(layoutInflater)
|
||||
|
||||
fixedEdgeToEdge()
|
||||
|
||||
content = AboutContent(this)
|
||||
title = content.pageTitle
|
||||
setContentView(binding.root)
|
||||
setSupportActionBar(binding.toolbar)
|
||||
enableToolbarBackButton()
|
||||
|
||||
binding.apply {
|
||||
creditsSub.text = content.copyrightShort
|
||||
versionHistorySub.text = content.versionHistory
|
||||
|
||||
versionHistory.tag = "https://catima.app/changelog/"
|
||||
translate.tag = "https://hosted.weblate.org/engage/catima/"
|
||||
license.tag = "https://github.com/CatimaLoyalty/Android/blob/main/LICENSE"
|
||||
repo.tag = "https://github.com/CatimaLoyalty/Android/"
|
||||
privacy.tag = "https://catima.app/privacy-policy/"
|
||||
reportError.tag = "https://github.com/CatimaLoyalty/Android/issues"
|
||||
rate.tag = "https://play.google.com/store/apps/details?id=me.hackerchick.catima"
|
||||
donate.tag = "https://catima.app/donate"
|
||||
|
||||
// Hide Google Play rate button if not on Google Play
|
||||
rate.isVisible = BuildConfig.showRateOnGooglePlay
|
||||
// Hide donate button on Google Play (Google Play doesn't allow donation links)
|
||||
donate.isVisible = BuildConfig.showDonate
|
||||
}
|
||||
|
||||
bindClickListeners()
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
return when (item.itemId) {
|
||||
android.R.id.home -> {
|
||||
finish()
|
||||
true
|
||||
setContent {
|
||||
CatimaTheme {
|
||||
AboutScreenContent(
|
||||
content = content,
|
||||
showDonate = BuildConfig.showDonate,
|
||||
showRateOnGooglePlay = BuildConfig.showRateOnGooglePlay,
|
||||
onBackPressedDispatcher = onBackPressedDispatcher
|
||||
)
|
||||
}
|
||||
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
content.destroy()
|
||||
clearClickListeners()
|
||||
}
|
||||
|
||||
private fun bindClickListeners() {
|
||||
binding.apply {
|
||||
versionHistory.setOnClickListener { showHistory(it) }
|
||||
translate.setOnClickListener { openExternalBrowser(it) }
|
||||
license.setOnClickListener { showLicense(it) }
|
||||
repo.setOnClickListener { openExternalBrowser(it) }
|
||||
privacy.setOnClickListener { showPrivacy(it) }
|
||||
reportError.setOnClickListener { openExternalBrowser(it) }
|
||||
rate.setOnClickListener { openExternalBrowser(it) }
|
||||
donate.setOnClickListener { openExternalBrowser(it) }
|
||||
credits.setOnClickListener { showCredits() }
|
||||
}
|
||||
}
|
||||
|
||||
private fun clearClickListeners() {
|
||||
binding.apply {
|
||||
versionHistory.setOnClickListener(null)
|
||||
translate.setOnClickListener(null)
|
||||
license.setOnClickListener(null)
|
||||
repo.setOnClickListener(null)
|
||||
privacy.setOnClickListener(null)
|
||||
reportError.setOnClickListener(null)
|
||||
rate.setOnClickListener(null)
|
||||
donate.setOnClickListener(null)
|
||||
credits.setOnClickListener(null)
|
||||
}
|
||||
}
|
||||
|
||||
private fun showCredits() {
|
||||
showHTML(R.string.credits, content.contributorInfo, null)
|
||||
}
|
||||
|
||||
private fun showHistory(view: View) {
|
||||
showHTML(R.string.version_history, content.historyInfo, view)
|
||||
}
|
||||
|
||||
private fun showLicense(view: View) {
|
||||
showHTML(R.string.license, content.licenseInfo, view)
|
||||
}
|
||||
|
||||
private fun showPrivacy(view: View) {
|
||||
showHTML(R.string.privacy_policy, content.privacyInfo, view)
|
||||
}
|
||||
|
||||
private fun showHTML(@StringRes title: Int, text: Spanned, view: View?) {
|
||||
val dialogContentPadding = resources.getDimensionPixelSize(R.dimen.alert_dialog_content_padding)
|
||||
val textView = TextView(this).apply {
|
||||
setText(text)
|
||||
Utils.makeTextViewLinksClickable(this, text)
|
||||
}
|
||||
|
||||
val scrollView = ScrollView(this).apply {
|
||||
addView(textView)
|
||||
setPadding(dialogContentPadding, dialogContentPadding / 2, dialogContentPadding, 0)
|
||||
}
|
||||
|
||||
MaterialAlertDialogBuilder(this).apply {
|
||||
setTitle(title)
|
||||
setView(scrollView)
|
||||
setPositiveButton(R.string.ok, null)
|
||||
|
||||
// Add View online button if an URL is linked to this view
|
||||
view?.tag?.let {
|
||||
setNeutralButton(R.string.view_online) { _, _ -> openExternalBrowser(view) }
|
||||
}
|
||||
|
||||
show()
|
||||
}
|
||||
}
|
||||
|
||||
private fun openExternalBrowser(view: View) {
|
||||
val tag = view.tag
|
||||
if (tag is String && tag.startsWith("https://")) {
|
||||
OpenWebLinkHandler().openBrowser(this, tag)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AboutScreenContent(
|
||||
content: AboutContent,
|
||||
showDonate: Boolean = true,
|
||||
showRateOnGooglePlay: Boolean = false,
|
||||
onBackPressedDispatcher: OnBackPressedDispatcher? = null,
|
||||
) {
|
||||
Scaffold(
|
||||
topBar = { CatimaTopAppBar(content.pageTitle.toString(), onBackPressedDispatcher) }
|
||||
) { innerPadding ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(innerPadding)
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
CatimaAboutSection(
|
||||
stringResource(R.string.version_history),
|
||||
content.versionHistory,
|
||||
modifier = Modifier.testTag("card_version_history"),
|
||||
onClickUrl = "https://catima.app/changelog/",
|
||||
onClickDialogText = AnnotatedString.fromHtml(
|
||||
htmlString = content.historyHtml,
|
||||
linkStyles = TextLinkStyles(
|
||||
style = SpanStyle(
|
||||
textDecoration = TextDecoration.Underline,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
CatimaAboutSection(
|
||||
stringResource(R.string.credits),
|
||||
content.copyrightShort,
|
||||
modifier = Modifier.testTag("card_credits"),
|
||||
onClickDialogText = AnnotatedString.fromHtml(
|
||||
htmlString = content.contributorInfoHtml,
|
||||
linkStyles = TextLinkStyles(
|
||||
style = SpanStyle(
|
||||
textDecoration = TextDecoration.Underline,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
CatimaAboutSection(
|
||||
stringResource(R.string.help_translate_this_app),
|
||||
stringResource(R.string.translate_platform),
|
||||
modifier = Modifier.testTag("card_translate"),
|
||||
onClickUrl = "https://hosted.weblate.org/engage/catima/"
|
||||
)
|
||||
CatimaAboutSection(
|
||||
stringResource(R.string.license),
|
||||
stringResource(R.string.app_license),
|
||||
modifier = Modifier.testTag("card_license"),
|
||||
onClickUrl = "https://github.com/CatimaLoyalty/Android/blob/main/LICENSE",
|
||||
onClickDialogText = AnnotatedString.fromHtml(
|
||||
htmlString = content.licenseHtml,
|
||||
linkStyles = TextLinkStyles(
|
||||
style = SpanStyle(
|
||||
textDecoration = TextDecoration.Underline,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
CatimaAboutSection(
|
||||
stringResource(R.string.source_repository),
|
||||
stringResource(R.string.on_github),
|
||||
modifier = Modifier.testTag("card_source_github"),
|
||||
onClickUrl = "https://github.com/CatimaLoyalty/Android/"
|
||||
)
|
||||
CatimaAboutSection(
|
||||
stringResource(R.string.privacy_policy),
|
||||
stringResource(R.string.and_data_usage),
|
||||
modifier = Modifier.testTag("card_privacy_policy"),
|
||||
onClickUrl = "https://catima.app/privacy-policy/",
|
||||
onClickDialogText = AnnotatedString.fromHtml(
|
||||
htmlString = content.privacyHtml,
|
||||
linkStyles = TextLinkStyles(
|
||||
style = SpanStyle(
|
||||
textDecoration = TextDecoration.Underline,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
if (showDonate) {
|
||||
CatimaAboutSection(
|
||||
stringResource(R.string.donate),
|
||||
"",
|
||||
modifier = Modifier.testTag("card_donate"),
|
||||
onClickUrl = "https://catima.app/donate"
|
||||
)
|
||||
}
|
||||
if (showRateOnGooglePlay) {
|
||||
CatimaAboutSection(
|
||||
stringResource(R.string.rate_this_app),
|
||||
stringResource(R.string.on_google_play),
|
||||
modifier = Modifier.testTag("card_rate_google"),
|
||||
onClickUrl = "https://play.google.com/store/apps/details?id=me.hackerchick.catima"
|
||||
)
|
||||
}
|
||||
CatimaAboutSection(
|
||||
stringResource(R.string.report_error),
|
||||
stringResource(R.string.on_github),
|
||||
modifier = Modifier.testTag("card_report_error"),
|
||||
onClickUrl = "https://github.com/CatimaLoyalty/Android/issues"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun AboutActivityPreview() {
|
||||
AboutScreenContent(AboutContent(LocalContext.current))
|
||||
}
|
||||
@@ -3,11 +3,8 @@ 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;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Calendar;
|
||||
@@ -55,7 +52,7 @@ public class AboutContent {
|
||||
return context.getString(R.string.app_copyright_short);
|
||||
}
|
||||
|
||||
public String getContributors() {
|
||||
public String getContributorsHtml() {
|
||||
String contributors;
|
||||
try {
|
||||
contributors = "<br/>" + Utils.readTextFile(context, R.raw.contributors);
|
||||
@@ -65,7 +62,7 @@ public class AboutContent {
|
||||
return contributors.replace("\n", "<br />");
|
||||
}
|
||||
|
||||
public String getHistory() {
|
||||
public String getHistoryHtml() {
|
||||
String versionHistory;
|
||||
try {
|
||||
versionHistory = Utils.readTextFile(context, R.raw.changelog)
|
||||
@@ -77,7 +74,7 @@ public class AboutContent {
|
||||
.replace("\n", "<br />");
|
||||
}
|
||||
|
||||
public String getLicense() {
|
||||
public String getLicenseHtml() {
|
||||
try {
|
||||
return Utils.readTextFile(context, R.raw.license);
|
||||
} catch (IOException ignored) {
|
||||
@@ -85,7 +82,7 @@ public class AboutContent {
|
||||
}
|
||||
}
|
||||
|
||||
public String getPrivacy() {
|
||||
public String getPrivacyHtml() {
|
||||
String privacyPolicy;
|
||||
try {
|
||||
privacyPolicy = Utils.readTextFile(context, R.raw.privacy)
|
||||
@@ -97,11 +94,11 @@ public class AboutContent {
|
||||
.replace("\n", "<br />");
|
||||
}
|
||||
|
||||
public String getThirdPartyLibraries() {
|
||||
public String getThirdPartyLibrariesHtml() {
|
||||
final List<ThirdPartyInfo> usedLibraries = new ArrayList<>();
|
||||
usedLibraries.add(new ThirdPartyInfo("ACRA", "https://github.com/ACRA/acra", "Apache 2.0"));
|
||||
usedLibraries.add(new ThirdPartyInfo("Color Picker", "https://github.com/jaredrummler/ColorPicker", "Apache 2.0"));
|
||||
usedLibraries.add(new ThirdPartyInfo("Commons CSV", "https://commons.apache.org/proper/commons-csv/", "Apache 2.0"));
|
||||
usedLibraries.add(new ThirdPartyInfo("NumberPickerPreference", "https://github.com/invissvenska/NumberPickerPreference", "GNU LGPL 3.0"));
|
||||
usedLibraries.add(new ThirdPartyInfo("uCrop", "https://github.com/Yalantis/uCrop", "Apache 2.0"));
|
||||
usedLibraries.add(new ThirdPartyInfo("Zip4j", "https://github.com/srikanth-lingala/zip4j", "Apache 2.0"));
|
||||
usedLibraries.add(new ThirdPartyInfo("ZXing", "https://github.com/zxing/zxing", "Apache 2.0"));
|
||||
@@ -116,7 +113,7 @@ public class AboutContent {
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
public String getUsedThirdPartyAssets() {
|
||||
public String getUsedThirdPartyAssetsHtml() {
|
||||
final List<ThirdPartyInfo> usedAssets = new ArrayList<>();
|
||||
usedAssets.add(new ThirdPartyInfo("Android icons", "https://fonts.google.com/icons?selected=Material+Icons", "Apache 2.0"));
|
||||
|
||||
@@ -129,31 +126,19 @@ public class AboutContent {
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
public Spanned getContributorInfo() {
|
||||
public String getContributorInfoHtml() {
|
||||
StringBuilder contributorInfo = new StringBuilder();
|
||||
contributorInfo.append(getCopyright());
|
||||
contributorInfo.append("<br/><br/>");
|
||||
contributorInfo.append(context.getString(R.string.app_copyright_old));
|
||||
contributorInfo.append("<br/><br/>");
|
||||
contributorInfo.append(String.format(context.getString(R.string.app_contributors), getContributors()));
|
||||
contributorInfo.append(String.format(context.getString(R.string.app_contributors), getContributorsHtml()));
|
||||
contributorInfo.append("<br/><br/>");
|
||||
contributorInfo.append(String.format(context.getString(R.string.app_libraries), getThirdPartyLibraries()));
|
||||
contributorInfo.append(String.format(context.getString(R.string.app_libraries), getThirdPartyLibrariesHtml()));
|
||||
contributorInfo.append("<br/><br/>");
|
||||
contributorInfo.append(String.format(context.getString(R.string.app_resources), getUsedThirdPartyAssets()));
|
||||
contributorInfo.append(String.format(context.getString(R.string.app_resources), getUsedThirdPartyAssetsHtml()));
|
||||
|
||||
return HtmlCompat.fromHtml(contributorInfo.toString(), HtmlCompat.FROM_HTML_MODE_COMPACT);
|
||||
}
|
||||
|
||||
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);
|
||||
return contributorInfo.toString();
|
||||
}
|
||||
|
||||
public String getVersionHistory() {
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
package protect.card_locker;
|
||||
|
||||
public interface BarcodeImageWriterResultCallback {
|
||||
void onBarcodeImageWriterResult(boolean success);
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package protect.card_locker
|
||||
|
||||
interface BarcodeImageWriterResultCallback {
|
||||
fun onBarcodeImageWriterResult(success: Boolean)
|
||||
}
|
||||
@@ -4,17 +4,27 @@ import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.util.ArrayMap;
|
||||
import android.util.Log;
|
||||
import android.util.TypedValue;
|
||||
import android.view.View;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.google.zxing.EncodeHintType;
|
||||
import com.google.zxing.MultiFormatWriter;
|
||||
import com.google.zxing.WriterException;
|
||||
import com.google.zxing.common.BitMatrix;
|
||||
import com.google.zxing.common.StringUtils;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
import protect.card_locker.async.CompatCallable;
|
||||
|
||||
@@ -39,6 +49,7 @@ public class BarcodeImageWriterTask implements CompatCallable<Bitmap> {
|
||||
private final WeakReference<TextView> textViewReference;
|
||||
private String cardId;
|
||||
private final CatimaBarcode format;
|
||||
private final Charset encoding;
|
||||
private final int imageHeight;
|
||||
private final int imageWidth;
|
||||
private final int imagePadding;
|
||||
@@ -46,9 +57,9 @@ public class BarcodeImageWriterTask implements CompatCallable<Bitmap> {
|
||||
private final boolean showFallback;
|
||||
private final BarcodeImageWriterResultCallback callback;
|
||||
|
||||
BarcodeImageWriterTask(
|
||||
public BarcodeImageWriterTask(
|
||||
Context context, ImageView imageView, String cardIdString,
|
||||
CatimaBarcode barcodeFormat, TextView textView,
|
||||
CatimaBarcode barcodeFormat, @NonNull Charset barcodeEncoding, TextView textView,
|
||||
boolean showFallback, BarcodeImageWriterResultCallback callback, boolean roundCornerPadding, boolean isFullscreen
|
||||
) {
|
||||
mContext = context;
|
||||
@@ -62,6 +73,7 @@ public class BarcodeImageWriterTask implements CompatCallable<Bitmap> {
|
||||
|
||||
cardId = cardIdString;
|
||||
format = barcodeFormat;
|
||||
encoding = barcodeEncoding;
|
||||
|
||||
int imageViewHeight = imageView.getHeight();
|
||||
int imageViewWidth = imageView.getWidth();
|
||||
@@ -172,10 +184,32 @@ public class BarcodeImageWriterTask implements CompatCallable<Bitmap> {
|
||||
}
|
||||
|
||||
MultiFormatWriter writer = new MultiFormatWriter();
|
||||
|
||||
Map<EncodeHintType, Object> encodeHints = new ArrayMap<>();
|
||||
// We don't want to pass the ISO-8859-1 as an encoding hint as zxing may add this as ECI
|
||||
// inside the barcode.
|
||||
//
|
||||
// Due to many barcode scanners in the wild being badly coded they may trip over ECI
|
||||
// info existing and fail to scan.
|
||||
// See:
|
||||
// - https://github.com/CatimaLoyalty/Android/issues/2921
|
||||
// - https://github.com/CatimaLoyalty/Android/issues/2932
|
||||
//
|
||||
// Just not always passing the encoding hint is slightly hacky, but in 5+ years of Catima
|
||||
// cards without encode hints have never caused any issues (unless they were UTF-8), yet
|
||||
// just days after passing ISO-8859-1 as CHARACTER_SET in the encode hints already 2
|
||||
// scan failures were reported (one for QR, one for Aztec).
|
||||
if (!Objects.equals(encoding.name(), StandardCharsets.ISO_8859_1.name())) {
|
||||
Log.d(TAG, "Chosen encoding is not ISO_8859_1, so passing as encoding hint");
|
||||
encodeHints.put(EncodeHintType.CHARACTER_SET, encoding);
|
||||
} else {
|
||||
Log.d(TAG, "Not passing encoding as encoding hint");
|
||||
}
|
||||
|
||||
BitMatrix bitMatrix;
|
||||
try {
|
||||
try {
|
||||
bitMatrix = writer.encode(cardId, format.format(), imageWidth, imageHeight, null);
|
||||
bitMatrix = writer.encode(cardId, format.format(), imageWidth, imageHeight, encodeHints);
|
||||
} catch (Exception e) {
|
||||
// Cast a wider net here and catch any exception, as there are some
|
||||
// cases where an encoder may fail if the data is invalid for the
|
||||
|
||||
@@ -10,6 +10,7 @@ import android.widget.ArrayAdapter;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
|
||||
import protect.card_locker.async.TaskHandler;
|
||||
@@ -74,6 +75,9 @@ public class BarcodeSelectorAdapter extends ArrayAdapter<CatimaBarcodeWithValue>
|
||||
return viewHolder.image.getTag() != null && (boolean) viewHolder.image.getTag();
|
||||
}
|
||||
|
||||
// FIXME: The examples in the selector activity are always rendering using ISO-8859-1, even if UTF-8 is the selected type
|
||||
// This needs some refactoring to properly retrieve the barcode encoding when rendering an active barcode
|
||||
// This is the case for example when editing an active card, pressing the "Edit barcode" button, then "More options" and then "Enter the barcode manually"
|
||||
private void createBarcodeOption(final ImageView image, final String formatType, final String cardId, final TextView text) {
|
||||
final CatimaBarcode format = CatimaBarcode.fromName(formatType);
|
||||
|
||||
@@ -92,13 +96,13 @@ public class BarcodeSelectorAdapter extends ArrayAdapter<CatimaBarcodeWithValue>
|
||||
|
||||
Log.d(TAG, "Generating barcode for type " + formatType);
|
||||
|
||||
BarcodeImageWriterTask barcodeWriter = new BarcodeImageWriterTask(getContext(), image, cardId, format, text, true, null, true, false);
|
||||
BarcodeImageWriterTask barcodeWriter = new BarcodeImageWriterTask(getContext(), image, cardId, format, StandardCharsets.ISO_8859_1, text, true, null, true, false);
|
||||
mTasks.executeTask(TaskHandler.TYPE.BARCODE, barcodeWriter);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
Log.d(TAG, "Generating barcode for type " + formatType);
|
||||
BarcodeImageWriterTask barcodeWriter = new BarcodeImageWriterTask(getContext(), image, cardId, format, text, true, null, true, false);
|
||||
BarcodeImageWriterTask barcodeWriter = new BarcodeImageWriterTask(getContext(), image, cardId, format, StandardCharsets.ISO_8859_1, text, true, null, true, false);
|
||||
mTasks.executeTask(TaskHandler.TYPE.BARCODE, barcodeWriter);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,8 @@ import java.util.List;
|
||||
import java.util.concurrent.Flow;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import protect.card_locker.cardview.LoyaltyCardViewActivity;
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.R)
|
||||
public class CardsOnPowerScreenService extends ControlsProviderService {
|
||||
|
||||
|
||||
@@ -38,15 +38,10 @@ public class CatimaAppCompatActivity extends AppCompatActivity {
|
||||
Window window = getWindow();
|
||||
if (window != null) {
|
||||
boolean darkMode = Utils.isDarkModeEnabled(this);
|
||||
if (Build.VERSION.SDK_INT >= 23) {
|
||||
View decorView = window.getDecorView();
|
||||
WindowInsetsControllerCompat wic = new WindowInsetsControllerCompat(window, decorView);
|
||||
wic.setAppearanceLightStatusBars(!darkMode);
|
||||
window.setStatusBarColor(Color.TRANSPARENT);
|
||||
} else {
|
||||
// icons are always white back then
|
||||
window.setStatusBarColor(darkMode ? Color.TRANSPARENT : Color.argb(127, 0, 0, 0));
|
||||
}
|
||||
View decorView = window.getDecorView();
|
||||
WindowInsetsControllerCompat wic = new WindowInsetsControllerCompat(window, decorView);
|
||||
wic.setAppearanceLightStatusBars(!darkMode);
|
||||
window.setStatusBarColor(Color.TRANSPARENT);
|
||||
}
|
||||
// XXX android 9 and below has a nasty rendering bug if the theme was patched earlier
|
||||
Utils.postPatchColors(this);
|
||||
@@ -66,7 +61,4 @@ public class CatimaAppCompatActivity extends AppCompatActivity {
|
||||
actionBar.setDisplayHomeAsUpEnabled(true);
|
||||
}
|
||||
}
|
||||
|
||||
public void onMockedRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package protect.card_locker
|
||||
|
||||
import android.graphics.Color
|
||||
import android.os.Bundle
|
||||
import android.os.PersistableBundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.SystemBarStyle
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import protect.card_locker.preferences.Settings
|
||||
|
||||
open class CatimaComponentActivity() : ComponentActivity() {
|
||||
fun fixedEdgeToEdge() {
|
||||
// Fix edge-to-edge
|
||||
// When overriding onCreate this does not correctly get applied, which is why it is its own function
|
||||
|
||||
// We explicitly need to set the systemBarStyle ourselves, to prevent issues where Android
|
||||
// for example renders white icons on top of a white statusbar (or black on black)
|
||||
val settings = Settings(this)
|
||||
val systemBarStyle = when (settings.theme) {
|
||||
AppCompatDelegate.MODE_NIGHT_NO ->
|
||||
SystemBarStyle.light(
|
||||
scrim = Color.TRANSPARENT,
|
||||
darkScrim = Color.TRANSPARENT,
|
||||
)
|
||||
AppCompatDelegate.MODE_NIGHT_YES ->
|
||||
SystemBarStyle.dark(
|
||||
scrim = Color.TRANSPARENT,
|
||||
)
|
||||
else ->
|
||||
SystemBarStyle.auto(
|
||||
lightScrim = Color.TRANSPARENT,
|
||||
darkScrim = Color.TRANSPARENT
|
||||
)
|
||||
}
|
||||
|
||||
enableEdgeToEdge(
|
||||
statusBarStyle = systemBarStyle,
|
||||
navigationBarStyle = systemBarStyle
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -10,8 +10,12 @@ import android.database.sqlite.SQLiteOpenHelper;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.io.FileNotFoundException;
|
||||
import java.math.BigDecimal;
|
||||
import java.nio.charset.Charset;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Currency;
|
||||
@@ -23,7 +27,7 @@ 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 = 17;
|
||||
public static final int DATABASE_VERSION = 20;
|
||||
|
||||
// NB: changing these values requires a migration
|
||||
public static final int DEFAULT_ZOOM_LEVEL = 100;
|
||||
@@ -49,6 +53,7 @@ public class DBHelper extends SQLiteOpenHelper {
|
||||
public static final String CARD_ID = "cardid";
|
||||
public static final String BARCODE_ID = "barcodeid";
|
||||
public static final String BARCODE_TYPE = "barcodetype";
|
||||
public static final String BARCODE_ENCODING = "barcodeencoding";
|
||||
public static final String STAR_STATUS = "starstatus";
|
||||
public static final String LAST_USED = "lastused";
|
||||
public static final String ZOOM_LEVEL = "zoomlevel";
|
||||
@@ -71,6 +76,7 @@ public class DBHelper extends SQLiteOpenHelper {
|
||||
|
||||
public enum LoyaltyCardOrder {
|
||||
Alpha,
|
||||
LastAdded,
|
||||
LastUsed,
|
||||
ValidFrom,
|
||||
Expiry
|
||||
@@ -112,6 +118,7 @@ public class DBHelper extends SQLiteOpenHelper {
|
||||
LoyaltyCardDbIds.CARD_ID + " TEXT not null," +
|
||||
LoyaltyCardDbIds.BARCODE_ID + " TEXT," +
|
||||
LoyaltyCardDbIds.BARCODE_TYPE + " TEXT," +
|
||||
LoyaltyCardDbIds.BARCODE_ENCODING + " TEXT not null," +
|
||||
LoyaltyCardDbIds.STAR_STATUS + " INTEGER DEFAULT '0'," +
|
||||
LoyaltyCardDbIds.LAST_USED + " INTEGER DEFAULT '0', " +
|
||||
LoyaltyCardDbIds.ZOOM_LEVEL + " INTEGER DEFAULT '" + DEFAULT_ZOOM_LEVEL + "', " +
|
||||
@@ -335,6 +342,160 @@ public class DBHelper extends SQLiteOpenHelper {
|
||||
db.execSQL("ALTER TABLE " + LoyaltyCardDbIds.TABLE
|
||||
+ " ADD COLUMN " + LoyaltyCardDbIds.ZOOM_LEVEL_WIDTH + " INTEGER DEFAULT '100' ");
|
||||
}
|
||||
|
||||
if (oldVersion < 18 && newVersion >= 18) {
|
||||
db.execSQL("ALTER TABLE " + LoyaltyCardDbIds.TABLE
|
||||
+ " ADD COLUMN " + LoyaltyCardDbIds.BARCODE_ENCODING + " TEXT");
|
||||
}
|
||||
|
||||
// On upgrade to 2.41.3, force all existing "Automatic" cards to ISO-8859-1.
|
||||
// UTF-8 support was only added in 2.42.0, before that, all barcodes were saved without
|
||||
// any encoding info. As many scanners deal badly with ECI info, automatically guessing
|
||||
// UTF-8 for old barcodes may break them.
|
||||
//
|
||||
// New cards will be saved using either "Automatic" encoding to guess or, for pkpass files,
|
||||
// whatever encoding is specified.
|
||||
if (oldVersion < 19 && newVersion >= 19) {
|
||||
db.execSQL("UPDATE " + LoyaltyCardDbIds.TABLE + " SET " + LoyaltyCardDbIds.BARCODE_ENCODING + " = 'ISO-8859-1' WHERE " + LoyaltyCardDbIds.BARCODE_ENCODING + " IS NULL");
|
||||
}
|
||||
|
||||
if (oldVersion < 20 && newVersion >= 20) {
|
||||
// SQLite doesn't support modify column
|
||||
// So we need to create a temp column to make barcode encoding non-nullable
|
||||
// https://www.sqlite.org/faq.html#q11
|
||||
db.beginTransaction();
|
||||
|
||||
// First migrate all cards to ISO-8859-1
|
||||
db.execSQL("UPDATE " + LoyaltyCardDbIds.TABLE + " SET " + LoyaltyCardDbIds.BARCODE_ENCODING + " = 'ISO-8859-1' WHERE " + LoyaltyCardDbIds.BARCODE_ENCODING + " IS NULL");
|
||||
|
||||
// create temp table
|
||||
db.execSQL("CREATE TEMPORARY TABLE tmp (" +
|
||||
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," +
|
||||
LoyaltyCardDbIds.HEADER_COLOR + " INTEGER," +
|
||||
LoyaltyCardDbIds.CARD_ID + " TEXT not null," +
|
||||
LoyaltyCardDbIds.BARCODE_ID + " TEXT," +
|
||||
LoyaltyCardDbIds.BARCODE_TYPE + " TEXT," +
|
||||
LoyaltyCardDbIds.BARCODE_ENCODING + " TEXT not null," +
|
||||
LoyaltyCardDbIds.STAR_STATUS + " INTEGER DEFAULT '0'," +
|
||||
LoyaltyCardDbIds.LAST_USED + " INTEGER DEFAULT '0', " +
|
||||
LoyaltyCardDbIds.ZOOM_LEVEL + " INTEGER DEFAULT '" + DEFAULT_ZOOM_LEVEL + "', " +
|
||||
LoyaltyCardDbIds.ZOOM_LEVEL_WIDTH + " INTEGER DEFAULT '" + DEFAULT_ZOOM_LEVEL_WIDTH + "', " +
|
||||
LoyaltyCardDbIds.ARCHIVE_STATUS + " INTEGER DEFAULT '0' )");
|
||||
|
||||
// Insert all cards into temp table
|
||||
db.execSQL("INSERT INTO tmp (" +
|
||||
LoyaltyCardDbIds.ID + " ," +
|
||||
LoyaltyCardDbIds.STORE + " ," +
|
||||
LoyaltyCardDbIds.NOTE + " ," +
|
||||
LoyaltyCardDbIds.VALID_FROM + " , " +
|
||||
LoyaltyCardDbIds.EXPIRY + " ," +
|
||||
LoyaltyCardDbIds.BALANCE + " ," +
|
||||
LoyaltyCardDbIds.BALANCE_TYPE + " ," +
|
||||
LoyaltyCardDbIds.HEADER_COLOR + " ," +
|
||||
LoyaltyCardDbIds.CARD_ID + " ," +
|
||||
LoyaltyCardDbIds.BARCODE_ID + " ," +
|
||||
LoyaltyCardDbIds.BARCODE_TYPE + " ," +
|
||||
LoyaltyCardDbIds.BARCODE_ENCODING + " ," +
|
||||
LoyaltyCardDbIds.STAR_STATUS + " ," +
|
||||
LoyaltyCardDbIds.LAST_USED + " ," +
|
||||
LoyaltyCardDbIds.ZOOM_LEVEL + " ," +
|
||||
LoyaltyCardDbIds.ZOOM_LEVEL_WIDTH + " ," +
|
||||
LoyaltyCardDbIds.ARCHIVE_STATUS + ")" +
|
||||
" SELECT " +
|
||||
LoyaltyCardDbIds.ID + " ," +
|
||||
LoyaltyCardDbIds.STORE + " ," +
|
||||
LoyaltyCardDbIds.NOTE + " ," +
|
||||
LoyaltyCardDbIds.VALID_FROM + " ," +
|
||||
LoyaltyCardDbIds.EXPIRY + " ," +
|
||||
LoyaltyCardDbIds.BALANCE + " ," +
|
||||
LoyaltyCardDbIds.BALANCE_TYPE + " ," +
|
||||
LoyaltyCardDbIds.HEADER_COLOR + " ," +
|
||||
LoyaltyCardDbIds.CARD_ID + " ," +
|
||||
LoyaltyCardDbIds.BARCODE_ID + " ," +
|
||||
LoyaltyCardDbIds.BARCODE_TYPE + " ," +
|
||||
LoyaltyCardDbIds.BARCODE_ENCODING + " ," +
|
||||
LoyaltyCardDbIds.STAR_STATUS + " ," +
|
||||
LoyaltyCardDbIds.LAST_USED + " ," +
|
||||
LoyaltyCardDbIds.ZOOM_LEVEL + " ," +
|
||||
LoyaltyCardDbIds.ZOOM_LEVEL_WIDTH + " ," +
|
||||
LoyaltyCardDbIds.ARCHIVE_STATUS +
|
||||
" FROM " + LoyaltyCardDbIds.TABLE);
|
||||
|
||||
// Drop old table
|
||||
db.execSQL("DROP TABLE " + LoyaltyCardDbIds.TABLE);
|
||||
|
||||
// Create table again
|
||||
db.execSQL("CREATE TABLE " + LoyaltyCardDbIds.TABLE + "(" +
|
||||
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," +
|
||||
LoyaltyCardDbIds.HEADER_COLOR + " INTEGER," +
|
||||
LoyaltyCardDbIds.CARD_ID + " TEXT not null," +
|
||||
LoyaltyCardDbIds.BARCODE_ID + " TEXT," +
|
||||
LoyaltyCardDbIds.BARCODE_TYPE + " TEXT," +
|
||||
LoyaltyCardDbIds.BARCODE_ENCODING + " TEXT not null," +
|
||||
LoyaltyCardDbIds.STAR_STATUS + " INTEGER DEFAULT '0'," +
|
||||
LoyaltyCardDbIds.LAST_USED + " INTEGER DEFAULT '0', " +
|
||||
LoyaltyCardDbIds.ZOOM_LEVEL + " INTEGER DEFAULT '" + DEFAULT_ZOOM_LEVEL + "', " +
|
||||
LoyaltyCardDbIds.ZOOM_LEVEL_WIDTH + " INTEGER DEFAULT '" + DEFAULT_ZOOM_LEVEL_WIDTH + "', " +
|
||||
LoyaltyCardDbIds.ARCHIVE_STATUS + " INTEGER DEFAULT '0' )");
|
||||
|
||||
// Insert all temp cards into newly created table
|
||||
db.execSQL("INSERT INTO " + LoyaltyCardDbIds.TABLE + "(" +
|
||||
LoyaltyCardDbIds.ID + " ," +
|
||||
LoyaltyCardDbIds.STORE + " ," +
|
||||
LoyaltyCardDbIds.NOTE + " ," +
|
||||
LoyaltyCardDbIds.VALID_FROM + " , " +
|
||||
LoyaltyCardDbIds.EXPIRY + " ," +
|
||||
LoyaltyCardDbIds.BALANCE + " ," +
|
||||
LoyaltyCardDbIds.BALANCE_TYPE + " ," +
|
||||
LoyaltyCardDbIds.HEADER_COLOR + " ," +
|
||||
LoyaltyCardDbIds.CARD_ID + " ," +
|
||||
LoyaltyCardDbIds.BARCODE_ID + " ," +
|
||||
LoyaltyCardDbIds.BARCODE_TYPE + " ," +
|
||||
LoyaltyCardDbIds.BARCODE_ENCODING + " ," +
|
||||
LoyaltyCardDbIds.STAR_STATUS + " ," +
|
||||
LoyaltyCardDbIds.LAST_USED + " ," +
|
||||
LoyaltyCardDbIds.ZOOM_LEVEL + " ," +
|
||||
LoyaltyCardDbIds.ZOOM_LEVEL_WIDTH + " ," +
|
||||
LoyaltyCardDbIds.ARCHIVE_STATUS + ")" +
|
||||
" SELECT " +
|
||||
LoyaltyCardDbIds.ID + " ," +
|
||||
LoyaltyCardDbIds.STORE + " ," +
|
||||
LoyaltyCardDbIds.NOTE + " ," +
|
||||
LoyaltyCardDbIds.VALID_FROM + " ," +
|
||||
LoyaltyCardDbIds.EXPIRY + " ," +
|
||||
LoyaltyCardDbIds.BALANCE + " ," +
|
||||
LoyaltyCardDbIds.BALANCE_TYPE + " ," +
|
||||
LoyaltyCardDbIds.HEADER_COLOR + " ," +
|
||||
LoyaltyCardDbIds.CARD_ID + " ," +
|
||||
LoyaltyCardDbIds.BARCODE_ID + " ," +
|
||||
LoyaltyCardDbIds.BARCODE_TYPE + " ," +
|
||||
LoyaltyCardDbIds.BARCODE_ENCODING + " ," +
|
||||
LoyaltyCardDbIds.STAR_STATUS + " ," +
|
||||
LoyaltyCardDbIds.LAST_USED + " ," +
|
||||
LoyaltyCardDbIds.ZOOM_LEVEL + " ," +
|
||||
LoyaltyCardDbIds.ZOOM_LEVEL_WIDTH + " ," +
|
||||
LoyaltyCardDbIds.ARCHIVE_STATUS +
|
||||
" FROM tmp");
|
||||
|
||||
// Drop tmp table
|
||||
db.execSQL("DROP TABLE tmp");
|
||||
|
||||
db.setTransactionSuccessful();
|
||||
db.endTransaction();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static Set<String> imageFiles(Context context, final SQLiteDatabase database) {
|
||||
@@ -396,7 +557,8 @@ public class DBHelper extends SQLiteOpenHelper {
|
||||
public static long insertLoyaltyCard(
|
||||
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 String barcodeId, final CatimaBarcode barcodeType, final @NonNull Charset barcodeEncoding,
|
||||
final Integer headerColor,
|
||||
final int starStatus, final Long lastUsed, final int archiveStatus) {
|
||||
database.beginTransaction();
|
||||
|
||||
@@ -411,6 +573,7 @@ public class DBHelper extends SQLiteOpenHelper {
|
||||
contentValues.put(LoyaltyCardDbIds.CARD_ID, cardId);
|
||||
contentValues.put(LoyaltyCardDbIds.BARCODE_ID, barcodeId);
|
||||
contentValues.put(LoyaltyCardDbIds.BARCODE_TYPE, barcodeType != null ? barcodeType.name() : null);
|
||||
contentValues.put(LoyaltyCardDbIds.BARCODE_ENCODING, barcodeEncoding.name());
|
||||
contentValues.put(LoyaltyCardDbIds.HEADER_COLOR, headerColor);
|
||||
contentValues.put(LoyaltyCardDbIds.STAR_STATUS, starStatus);
|
||||
contentValues.put(LoyaltyCardDbIds.LAST_USED, lastUsed != null ? lastUsed : Utils.getUnixTime());
|
||||
@@ -430,7 +593,8 @@ public class DBHelper extends SQLiteOpenHelper {
|
||||
final SQLiteDatabase database, final int id, 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 CatimaBarcode barcodeType, final @NonNull Charset barcodeEncoding,
|
||||
final Integer headerColor, final int starStatus,
|
||||
final Long lastUsed, final int archiveStatus) {
|
||||
database.beginTransaction();
|
||||
|
||||
@@ -446,6 +610,7 @@ public class DBHelper extends SQLiteOpenHelper {
|
||||
contentValues.put(LoyaltyCardDbIds.CARD_ID, cardId);
|
||||
contentValues.put(LoyaltyCardDbIds.BARCODE_ID, barcodeId);
|
||||
contentValues.put(LoyaltyCardDbIds.BARCODE_TYPE, barcodeType != null ? barcodeType.name() : null);
|
||||
contentValues.put(LoyaltyCardDbIds.BARCODE_ENCODING, barcodeEncoding.name());
|
||||
contentValues.put(LoyaltyCardDbIds.HEADER_COLOR, headerColor);
|
||||
contentValues.put(LoyaltyCardDbIds.STAR_STATUS, starStatus);
|
||||
contentValues.put(LoyaltyCardDbIds.LAST_USED, lastUsed != null ? lastUsed : Utils.getUnixTime());
|
||||
@@ -465,7 +630,8 @@ public class DBHelper extends SQLiteOpenHelper {
|
||||
SQLiteDatabase database, final int id, 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 CatimaBarcode barcodeType, final @NonNull Charset barcodeEncoding,
|
||||
final Integer headerColor, final int starStatus,
|
||||
final Long lastUsed, final int archiveStatus) {
|
||||
database.beginTransaction();
|
||||
|
||||
@@ -480,6 +646,7 @@ public class DBHelper extends SQLiteOpenHelper {
|
||||
contentValues.put(LoyaltyCardDbIds.CARD_ID, cardId);
|
||||
contentValues.put(LoyaltyCardDbIds.BARCODE_ID, barcodeId);
|
||||
contentValues.put(LoyaltyCardDbIds.BARCODE_TYPE, barcodeType != null ? barcodeType.name() : null);
|
||||
contentValues.put(LoyaltyCardDbIds.BARCODE_ENCODING, barcodeEncoding.name());
|
||||
contentValues.put(LoyaltyCardDbIds.HEADER_COLOR, headerColor);
|
||||
contentValues.put(LoyaltyCardDbIds.STAR_STATUS, starStatus);
|
||||
contentValues.put(LoyaltyCardDbIds.LAST_USED, lastUsed != null ? lastUsed : Utils.getUnixTime());
|
||||
@@ -923,6 +1090,10 @@ public class DBHelper extends SQLiteOpenHelper {
|
||||
return LoyaltyCardDbIds.STORE;
|
||||
}
|
||||
|
||||
if (order == LoyaltyCardOrder.LastAdded) {
|
||||
return LoyaltyCardDbIds.ID;
|
||||
}
|
||||
|
||||
if (order == LoyaltyCardOrder.LastUsed) {
|
||||
return LoyaltyCardDbIds.LAST_USED;
|
||||
}
|
||||
@@ -939,8 +1110,8 @@ public class DBHelper extends SQLiteOpenHelper {
|
||||
}
|
||||
|
||||
private static String getDbDirection(LoyaltyCardOrder order, LoyaltyCardOrderDirection direction) {
|
||||
if (order == LoyaltyCardOrder.LastUsed) {
|
||||
// We want the default sorting to put the most recently used first
|
||||
if (order == LoyaltyCardOrder.LastAdded || order == LoyaltyCardOrder.LastUsed) {
|
||||
// We want the default sorting to put the most recently added/used first
|
||||
return direction == LoyaltyCardOrderDirection.Descending ? "ASC" : "DESC";
|
||||
}
|
||||
|
||||
|
||||
@@ -1,395 +0,0 @@
|
||||
package protect.card_locker;
|
||||
|
||||
import android.content.ActivityNotFoundException;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.text.InputType;
|
||||
import android.util.Log;
|
||||
import android.view.MenuItem;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.Button;
|
||||
import android.widget.EditText;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.activity.result.ActivityResultLauncher;
|
||||
import androidx.activity.result.contract.ActivityResultContracts;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
import com.google.android.material.textfield.TextInputLayout;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import protect.card_locker.async.TaskHandler;
|
||||
import protect.card_locker.databinding.ImportExportActivityBinding;
|
||||
import protect.card_locker.importexport.DataFormat;
|
||||
import protect.card_locker.importexport.ImportExportResult;
|
||||
import protect.card_locker.importexport.ImportExportResultType;
|
||||
|
||||
public class ImportExportActivity extends CatimaAppCompatActivity {
|
||||
private ImportExportActivityBinding binding;
|
||||
private static final String TAG = "Catima";
|
||||
|
||||
private ImportExportTask importExporter;
|
||||
|
||||
private String importAlertTitle;
|
||||
private String importAlertMessage;
|
||||
private DataFormat importDataFormat;
|
||||
private String exportPassword;
|
||||
|
||||
private ActivityResultLauncher<Intent> fileCreateLauncher;
|
||||
private ActivityResultLauncher<String> fileOpenLauncher;
|
||||
|
||||
final private TaskHandler mTasks = new TaskHandler();
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
binding = ImportExportActivityBinding.inflate(getLayoutInflater());
|
||||
setTitle(R.string.importExport);
|
||||
setContentView(binding.getRoot());
|
||||
Utils.applyWindowInsets(binding.getRoot());
|
||||
Toolbar toolbar = binding.toolbar;
|
||||
setSupportActionBar(toolbar);
|
||||
enableToolbarBackButton();
|
||||
|
||||
Intent fileIntent = getIntent();
|
||||
if (fileIntent != null && fileIntent.getType() != null) {
|
||||
chooseImportType(fileIntent.getData());
|
||||
}
|
||||
|
||||
// would use ActivityResultContracts.CreateDocument() but mime type cannot be set
|
||||
fileCreateLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> {
|
||||
Intent intent = result.getData();
|
||||
if (intent == null) {
|
||||
Log.e(TAG, "Activity returned NULL data");
|
||||
return;
|
||||
}
|
||||
Uri uri = intent.getData();
|
||||
if (uri == null) {
|
||||
Log.e(TAG, "Activity returned NULL uri");
|
||||
return;
|
||||
}
|
||||
// Running this in a thread prevents Android from throwing a NetworkOnMainThreadException for large files
|
||||
// FIXME: This is still suboptimal, because showing that the export started is delayed until the network request finishes
|
||||
new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
OutputStream writer = getContentResolver().openOutputStream(uri);
|
||||
Log.d(TAG, "Starting file export with: " + result);
|
||||
startExport(writer, uri, exportPassword.toCharArray(), true);
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "Failed to export file: " + result, e);
|
||||
onExportComplete(new ImportExportResult(ImportExportResultType.GenericFailure, result.toString()), uri);
|
||||
}
|
||||
}
|
||||
}.start();
|
||||
});
|
||||
fileOpenLauncher = registerForActivityResult(new ActivityResultContracts.GetContent(), result -> {
|
||||
if (result == null) {
|
||||
Log.e(TAG, "Activity returned NULL data");
|
||||
return;
|
||||
}
|
||||
openFileForImport(result, null);
|
||||
});
|
||||
|
||||
// Check that there is a file manager available
|
||||
final Intent intentCreateDocumentAction = new Intent(Intent.ACTION_CREATE_DOCUMENT);
|
||||
intentCreateDocumentAction.addCategory(Intent.CATEGORY_OPENABLE);
|
||||
intentCreateDocumentAction.setType("application/zip");
|
||||
intentCreateDocumentAction.putExtra(Intent.EXTRA_TITLE, "catima.zip");
|
||||
|
||||
Button exportButton = binding.exportButton;
|
||||
exportButton.setOnClickListener(v -> {
|
||||
AlertDialog.Builder builder = new MaterialAlertDialogBuilder(ImportExportActivity.this);
|
||||
builder.setTitle(R.string.exportPassword);
|
||||
|
||||
FrameLayout container = new FrameLayout(ImportExportActivity.this);
|
||||
|
||||
final TextInputLayout textInputLayout = new TextInputLayout(ImportExportActivity.this);
|
||||
textInputLayout.setEndIconMode(TextInputLayout.END_ICON_PASSWORD_TOGGLE);
|
||||
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
params.setMargins(50, 10, 50, 0);
|
||||
textInputLayout.setLayoutParams(params);
|
||||
|
||||
final EditText input = new EditText(ImportExportActivity.this);
|
||||
input.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD);
|
||||
input.setHint(R.string.exportPasswordHint);
|
||||
|
||||
textInputLayout.addView(input);
|
||||
container.addView(textInputLayout);
|
||||
builder.setView(container);
|
||||
builder.setPositiveButton(R.string.ok, (dialogInterface, i) -> {
|
||||
exportPassword = input.getText().toString();
|
||||
try {
|
||||
fileCreateLauncher.launch(intentCreateDocumentAction);
|
||||
} catch (ActivityNotFoundException e) {
|
||||
Toast.makeText(getApplicationContext(), R.string.failedOpeningFileManager, Toast.LENGTH_LONG).show();
|
||||
Log.e(TAG, "No activity found to handle intent", e);
|
||||
}
|
||||
});
|
||||
builder.setNegativeButton(R.string.cancel, (dialogInterface, i) -> dialogInterface.cancel());
|
||||
builder.show();
|
||||
});
|
||||
|
||||
// Check that there is a file manager available
|
||||
Button importFilesystem = binding.importOptionFilesystemButton;
|
||||
importFilesystem.setOnClickListener(v -> chooseImportType(null));
|
||||
|
||||
// FIXME: The importer/exporter is currently quite broken
|
||||
// To prevent the screen from turning off during import/export and some devices killing Catima as it's no longer foregrounded, force the screen to stay on here
|
||||
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
|
||||
}
|
||||
|
||||
private void openFileForImport(Uri uri, char[] password) {
|
||||
// Running this in a thread prevents Android from throwing a NetworkOnMainThreadException for large files
|
||||
// FIXME: This is still suboptimal, because showing that the import started is delayed until the network request finishes
|
||||
new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
InputStream reader = getContentResolver().openInputStream(uri);
|
||||
Log.d(TAG, "Starting file import with: " + uri);
|
||||
startImport(reader, uri, importDataFormat, password, true);
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "Failed to import file: " + uri, e);
|
||||
onImportComplete(new ImportExportResult(ImportExportResultType.GenericFailure, e.toString()), uri, importDataFormat);
|
||||
}
|
||||
}
|
||||
}.start();
|
||||
}
|
||||
|
||||
private void chooseImportType(@Nullable Uri fileData) {
|
||||
|
||||
List<CharSequence> betaImportOptions = new ArrayList<>();
|
||||
betaImportOptions.add("Fidme");
|
||||
List<CharSequence> importOptions = new ArrayList<>();
|
||||
|
||||
for (String importOption : getResources().getStringArray(R.array.import_types_array)) {
|
||||
if (betaImportOptions.contains(importOption)) {
|
||||
importOption = importOption + " (BETA)";
|
||||
}
|
||||
|
||||
importOptions.add(importOption);
|
||||
}
|
||||
|
||||
AlertDialog.Builder builder = new MaterialAlertDialogBuilder(this);
|
||||
builder.setTitle(R.string.chooseImportType)
|
||||
.setItems(importOptions.toArray(new CharSequence[importOptions.size()]), (dialog, which) -> {
|
||||
switch (which) {
|
||||
// Catima
|
||||
case 0:
|
||||
importAlertTitle = getString(R.string.importCatima);
|
||||
importAlertMessage = getString(R.string.importCatimaMessage);
|
||||
importDataFormat = DataFormat.Catima;
|
||||
break;
|
||||
// Fidme
|
||||
case 1:
|
||||
importAlertTitle = getString(R.string.importFidme);
|
||||
importAlertMessage = getString(R.string.importFidmeMessage);
|
||||
importDataFormat = DataFormat.Fidme;
|
||||
break;
|
||||
// Loyalty Card Keychain
|
||||
case 2:
|
||||
importAlertTitle = getString(R.string.importLoyaltyCardKeychain);
|
||||
importAlertMessage = getString(R.string.importLoyaltyCardKeychainMessage);
|
||||
importDataFormat = DataFormat.Catima;
|
||||
break;
|
||||
// Voucher Vault
|
||||
case 3:
|
||||
importAlertTitle = getString(R.string.importVoucherVault);
|
||||
importAlertMessage = getString(R.string.importVoucherVaultMessage);
|
||||
importDataFormat = DataFormat.VoucherVault;
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("Unknown DataFormat");
|
||||
}
|
||||
|
||||
if (fileData != null) {
|
||||
openFileForImport(fileData, null);
|
||||
return;
|
||||
}
|
||||
|
||||
new MaterialAlertDialogBuilder(this)
|
||||
.setTitle(importAlertTitle)
|
||||
.setMessage(importAlertMessage)
|
||||
.setPositiveButton(R.string.ok, (dialog1, which1) -> {
|
||||
try {
|
||||
fileOpenLauncher.launch("*/*");
|
||||
} catch (ActivityNotFoundException e) {
|
||||
Toast.makeText(getApplicationContext(), R.string.failedOpeningFileManager, Toast.LENGTH_LONG).show();
|
||||
Log.e(TAG, "No activity found to handle intent", e);
|
||||
}
|
||||
})
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.show();
|
||||
});
|
||||
builder.show();
|
||||
}
|
||||
|
||||
private void startImport(final InputStream target, final Uri targetUri, final DataFormat dataFormat, final char[] password, final boolean closeWhenDone) {
|
||||
mTasks.flushTaskList(TaskHandler.TYPE.IMPORT, true, false, false);
|
||||
ImportExportTask.TaskCompleteListener listener = new ImportExportTask.TaskCompleteListener() {
|
||||
@Override
|
||||
public void onTaskComplete(ImportExportResult result, DataFormat dataFormat) {
|
||||
onImportComplete(result, targetUri, dataFormat);
|
||||
if (closeWhenDone) {
|
||||
try {
|
||||
target.close();
|
||||
} catch (IOException ioException) {
|
||||
ioException.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
importExporter = new ImportExportTask(ImportExportActivity.this,
|
||||
dataFormat, target, password, listener);
|
||||
mTasks.executeTask(TaskHandler.TYPE.IMPORT, importExporter);
|
||||
}
|
||||
|
||||
private void startExport(final OutputStream target, final Uri targetUri, char[] password, final boolean closeWhenDone) {
|
||||
mTasks.flushTaskList(TaskHandler.TYPE.EXPORT, true, false, false);
|
||||
ImportExportTask.TaskCompleteListener listener = new ImportExportTask.TaskCompleteListener() {
|
||||
@Override
|
||||
public void onTaskComplete(ImportExportResult result, DataFormat dataFormat) {
|
||||
onExportComplete(result, targetUri);
|
||||
if (closeWhenDone) {
|
||||
try {
|
||||
target.close();
|
||||
} catch (IOException ioException) {
|
||||
ioException.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
importExporter = new ImportExportTask(ImportExportActivity.this,
|
||||
DataFormat.Catima, target, password, listener);
|
||||
mTasks.executeTask(TaskHandler.TYPE.EXPORT, importExporter);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
mTasks.flushTaskList(TaskHandler.TYPE.IMPORT, true, false, false);
|
||||
mTasks.flushTaskList(TaskHandler.TYPE.EXPORT, true, false, false);
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
int id = item.getItemId();
|
||||
|
||||
if (id == android.R.id.home) {
|
||||
finish();
|
||||
return true;
|
||||
}
|
||||
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
private void retryWithPassword(DataFormat dataFormat, Uri uri) {
|
||||
AlertDialog.Builder builder = new MaterialAlertDialogBuilder(this);
|
||||
builder.setTitle(R.string.passwordRequired);
|
||||
|
||||
FrameLayout container = new FrameLayout(ImportExportActivity.this);
|
||||
|
||||
final TextInputLayout textInputLayout = new TextInputLayout(ImportExportActivity.this);
|
||||
textInputLayout.setEndIconMode(TextInputLayout.END_ICON_PASSWORD_TOGGLE);
|
||||
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
params.setMargins(50, 10, 50, 0);
|
||||
textInputLayout.setLayoutParams(params);
|
||||
|
||||
final EditText input = new EditText(ImportExportActivity.this);
|
||||
input.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD);
|
||||
input.setHint(R.string.exportPasswordHint);
|
||||
|
||||
textInputLayout.addView(input);
|
||||
container.addView(textInputLayout);
|
||||
builder.setView(container);
|
||||
|
||||
builder.setPositiveButton(R.string.ok, (dialogInterface, i) -> {
|
||||
openFileForImport(uri, input.getText().toString().toCharArray());
|
||||
});
|
||||
builder.setNegativeButton(R.string.cancel, (dialogInterface, i) -> dialogInterface.cancel());
|
||||
|
||||
builder.show();
|
||||
}
|
||||
|
||||
private String buildResultDialogMessage(ImportExportResult result, boolean isImport) {
|
||||
int messageId;
|
||||
|
||||
if (result.resultType() == ImportExportResultType.Success) {
|
||||
messageId = isImport ? R.string.importSuccessful : R.string.exportSuccessful;
|
||||
} else {
|
||||
messageId = isImport ? R.string.importFailed : R.string.exportFailed;
|
||||
}
|
||||
|
||||
StringBuilder messageBuilder = new StringBuilder(getResources().getString(messageId));
|
||||
if (result.developerDetails() != null) {
|
||||
messageBuilder.append("\n\n");
|
||||
messageBuilder.append(getResources().getString(R.string.include_if_asking_support));
|
||||
messageBuilder.append("\n\n");
|
||||
messageBuilder.append(result.developerDetails());
|
||||
}
|
||||
|
||||
return messageBuilder.toString();
|
||||
}
|
||||
|
||||
private void onImportComplete(ImportExportResult result, Uri path, DataFormat dataFormat) {
|
||||
ImportExportResultType resultType = result.resultType();
|
||||
|
||||
if (resultType == ImportExportResultType.BadPassword) {
|
||||
retryWithPassword(dataFormat, path);
|
||||
return;
|
||||
}
|
||||
|
||||
AlertDialog.Builder builder = new MaterialAlertDialogBuilder(this);
|
||||
builder.setTitle(resultType == ImportExportResultType.Success ? R.string.importSuccessfulTitle : R.string.importFailedTitle);
|
||||
builder.setMessage(buildResultDialogMessage(result, true));
|
||||
builder.setNeutralButton(R.string.ok, (dialog, which) -> dialog.dismiss());
|
||||
|
||||
builder.create().show();
|
||||
}
|
||||
|
||||
private void onExportComplete(ImportExportResult result, final Uri path) {
|
||||
ImportExportResultType resultType = result.resultType();
|
||||
|
||||
AlertDialog.Builder builder = new MaterialAlertDialogBuilder(this);
|
||||
builder.setTitle(resultType == ImportExportResultType.Success ? R.string.exportSuccessfulTitle : R.string.exportFailedTitle);
|
||||
builder.setMessage(buildResultDialogMessage(result, false));
|
||||
builder.setNeutralButton(R.string.ok, (dialog, which) -> dialog.dismiss());
|
||||
|
||||
if (resultType == ImportExportResultType.Success) {
|
||||
final CharSequence sendLabel = ImportExportActivity.this.getResources().getText(R.string.sendLabel);
|
||||
|
||||
builder.setPositiveButton(sendLabel, (dialog, which) -> {
|
||||
Intent sendIntent = new Intent(Intent.ACTION_SEND);
|
||||
sendIntent.putExtra(Intent.EXTRA_STREAM, path);
|
||||
sendIntent.setType("text/csv");
|
||||
|
||||
// set flag to give temporary permission to external app to use the FileProvider
|
||||
sendIntent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
||||
|
||||
ImportExportActivity.this.startActivity(Intent.createChooser(sendIntent,
|
||||
sendLabel));
|
||||
|
||||
dialog.dismiss();
|
||||
});
|
||||
}
|
||||
|
||||
builder.create().show();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,424 @@
|
||||
package protect.card_locker
|
||||
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.text.InputType
|
||||
import android.util.Log
|
||||
import android.view.MenuItem
|
||||
import android.view.ViewGroup
|
||||
import android.view.WindowManager
|
||||
import android.widget.Button
|
||||
import android.widget.EditText
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.Toast
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.textfield.TextInputLayout
|
||||
import protect.card_locker.async.TaskHandler
|
||||
import protect.card_locker.databinding.ImportExportActivityBinding
|
||||
import protect.card_locker.importexport.DataFormat
|
||||
import protect.card_locker.importexport.ImportExportResult
|
||||
import protect.card_locker.importexport.ImportExportResultType
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
|
||||
class ImportExportActivity : CatimaAppCompatActivity() {
|
||||
private lateinit var binding: ImportExportActivityBinding
|
||||
|
||||
private var importExporter: ImportExportTask? = null
|
||||
|
||||
private var importAlertTitle: String? = null
|
||||
private var importAlertMessage: String? = null
|
||||
private var importDataFormat: DataFormat? = null
|
||||
private var exportPassword: String? = null
|
||||
|
||||
private lateinit var fileCreateLauncher: ActivityResultLauncher<Intent>
|
||||
private lateinit var fileOpenLauncher: ActivityResultLauncher<String>
|
||||
|
||||
private val mTasks = TaskHandler()
|
||||
|
||||
companion object {
|
||||
private const val TAG = "Catima"
|
||||
|
||||
internal fun getExportFilename(): String {
|
||||
val sdf = SimpleDateFormat("yyyyMMdd", Locale.US)
|
||||
return "catima_${sdf.format(Date())}.zip"
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = ImportExportActivityBinding.inflate(layoutInflater)
|
||||
setTitle(R.string.importExport)
|
||||
setContentView(binding.root)
|
||||
Utils.applyWindowInsets(binding.root)
|
||||
val toolbar: Toolbar = binding.toolbar
|
||||
setSupportActionBar(toolbar)
|
||||
enableToolbarBackButton()
|
||||
|
||||
val fileIntent = intent
|
||||
if (fileIntent?.type != null) {
|
||||
chooseImportType(fileIntent.data)
|
||||
}
|
||||
|
||||
// would use ActivityResultContracts.CreateDocument() but mime type cannot be set
|
||||
fileCreateLauncher =
|
||||
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||
val intent = result.data
|
||||
if (intent == null) {
|
||||
Log.e(TAG, "Activity returned NULL data")
|
||||
return@registerForActivityResult
|
||||
}
|
||||
val uri = intent.data
|
||||
if (uri == null) {
|
||||
Log.e(TAG, "Activity returned NULL uri")
|
||||
return@registerForActivityResult
|
||||
}
|
||||
// Running this in a thread prevents Android from throwing a NetworkOnMainThreadException for large files
|
||||
// FIXME: This is still suboptimal, because showing that the export started is delayed until the network request finishes
|
||||
Thread {
|
||||
try {
|
||||
val writer = contentResolver.openOutputStream(uri)
|
||||
Log.d(TAG, "Starting file export with: $result")
|
||||
startExport(writer, uri, exportPassword?.toCharArray(), true)
|
||||
} catch (e: IOException) {
|
||||
Log.e(TAG, "Failed to export file: $result", e)
|
||||
onExportComplete(
|
||||
ImportExportResult(
|
||||
ImportExportResultType.GenericFailure,
|
||||
result.toString()
|
||||
), uri
|
||||
)
|
||||
}
|
||||
}.start()
|
||||
}
|
||||
|
||||
fileOpenLauncher =
|
||||
registerForActivityResult(ActivityResultContracts.GetContent()) { result ->
|
||||
if (result == null) {
|
||||
Log.e(TAG, "Activity returned NULL data")
|
||||
return@registerForActivityResult
|
||||
}
|
||||
openFileForImport(result, null)
|
||||
}
|
||||
|
||||
// Check that there is a file manager available
|
||||
val intentCreateDocumentAction = Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
|
||||
addCategory(Intent.CATEGORY_OPENABLE)
|
||||
type = "application/zip"
|
||||
putExtra(Intent.EXTRA_TITLE, getExportFilename())
|
||||
}
|
||||
|
||||
val exportButton: Button = binding.exportButton
|
||||
exportButton.setOnClickListener {
|
||||
val builder = MaterialAlertDialogBuilder(this@ImportExportActivity)
|
||||
builder.setTitle(R.string.exportPassword)
|
||||
|
||||
val container = FrameLayout(this@ImportExportActivity)
|
||||
|
||||
val textInputLayout = TextInputLayout(this@ImportExportActivity).apply {
|
||||
endIconMode = TextInputLayout.END_ICON_PASSWORD_TOGGLE
|
||||
layoutParams = LinearLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
).apply {
|
||||
setMargins(50, 10, 50, 0)
|
||||
}
|
||||
}
|
||||
|
||||
val input = EditText(this@ImportExportActivity).apply {
|
||||
inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD
|
||||
setHint(R.string.exportPasswordHint)
|
||||
}
|
||||
|
||||
textInputLayout.addView(input)
|
||||
container.addView(textInputLayout)
|
||||
builder.setView(container)
|
||||
builder.setPositiveButton(R.string.ok) { _, _ ->
|
||||
exportPassword = input.text.toString()
|
||||
try {
|
||||
fileCreateLauncher.launch(intentCreateDocumentAction)
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
Toast.makeText(
|
||||
applicationContext,
|
||||
R.string.failedOpeningFileManager,
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
Log.e(TAG, "No activity found to handle intent", e)
|
||||
}
|
||||
}
|
||||
builder.setNegativeButton(R.string.cancel) { dialogInterface, _ -> dialogInterface.cancel() }
|
||||
builder.show()
|
||||
}
|
||||
|
||||
// Check that there is a file manager available
|
||||
val importFilesystem: Button = binding.importOptionFilesystemButton
|
||||
importFilesystem.setOnClickListener { chooseImportType(null) }
|
||||
|
||||
// FIXME: The importer/exporter is currently quite broken
|
||||
// To prevent the screen from turning off during import/export and some devices killing Catima as it's no longer foregrounded, force the screen to stay on here
|
||||
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
||||
}
|
||||
|
||||
private fun openFileForImport(uri: Uri, password: CharArray?) {
|
||||
// Running this in a thread prevents Android from throwing a NetworkOnMainThreadException for large files
|
||||
// FIXME: This is still suboptimal, because showing that the import started is delayed until the network request finishes
|
||||
Thread {
|
||||
try {
|
||||
val reader = contentResolver.openInputStream(uri)
|
||||
Log.d(TAG, "Starting file import with: $uri")
|
||||
startImport(reader, uri, importDataFormat, password, true)
|
||||
} catch (e: IOException) {
|
||||
Log.e(TAG, "Failed to import file: $uri", e)
|
||||
onImportComplete(
|
||||
ImportExportResult(
|
||||
ImportExportResultType.GenericFailure,
|
||||
e.toString()
|
||||
), uri, importDataFormat
|
||||
)
|
||||
}
|
||||
}.start()
|
||||
}
|
||||
|
||||
private fun chooseImportType(fileData: Uri?) {
|
||||
val betaImportOptions = mutableListOf<CharSequence>()
|
||||
betaImportOptions.add("Fidme")
|
||||
val importOptions = mutableListOf<CharSequence>()
|
||||
|
||||
for (importOption in resources.getStringArray(R.array.import_types_array)) {
|
||||
var option = importOption
|
||||
if (betaImportOptions.contains(importOption)) {
|
||||
option = "$importOption (BETA)"
|
||||
}
|
||||
importOptions.add(option)
|
||||
}
|
||||
|
||||
val builder = MaterialAlertDialogBuilder(this)
|
||||
builder.setTitle(R.string.chooseImportType)
|
||||
.setItems(importOptions.toTypedArray()) { _, which ->
|
||||
when (which) {
|
||||
// Catima
|
||||
0 -> {
|
||||
importAlertTitle = getString(R.string.importCatima)
|
||||
importAlertMessage = getString(R.string.importCatimaMessage)
|
||||
importDataFormat = DataFormat.Catima
|
||||
}
|
||||
// Fidme
|
||||
1 -> {
|
||||
importAlertTitle = getString(R.string.importFidme)
|
||||
importAlertMessage = getString(R.string.importFidmeMessage)
|
||||
importDataFormat = DataFormat.Fidme
|
||||
}
|
||||
// Loyalty Card Keychain
|
||||
2 -> {
|
||||
importAlertTitle = getString(R.string.importLoyaltyCardKeychain)
|
||||
importAlertMessage = getString(R.string.importLoyaltyCardKeychainMessage)
|
||||
importDataFormat = DataFormat.Catima
|
||||
}
|
||||
// Voucher Vault
|
||||
3 -> {
|
||||
importAlertTitle = getString(R.string.importVoucherVault)
|
||||
importAlertMessage = getString(R.string.importVoucherVaultMessage)
|
||||
importDataFormat = DataFormat.VoucherVault
|
||||
}
|
||||
|
||||
else -> throw IllegalArgumentException("Unknown DataFormat")
|
||||
}
|
||||
|
||||
if (fileData != null) {
|
||||
openFileForImport(fileData, null)
|
||||
return@setItems
|
||||
}
|
||||
|
||||
MaterialAlertDialogBuilder(this)
|
||||
.setTitle(importAlertTitle)
|
||||
.setMessage(importAlertMessage)
|
||||
.setPositiveButton(R.string.ok) { _, _ ->
|
||||
try {
|
||||
fileOpenLauncher.launch("*/*")
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
Toast.makeText(
|
||||
applicationContext,
|
||||
R.string.failedOpeningFileManager,
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
Log.e(TAG, "No activity found to handle intent", e)
|
||||
}
|
||||
}
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.show()
|
||||
}
|
||||
builder.show()
|
||||
}
|
||||
|
||||
private fun startImport(
|
||||
target: InputStream?,
|
||||
targetUri: Uri,
|
||||
dataFormat: DataFormat?,
|
||||
password: CharArray?,
|
||||
closeWhenDone: Boolean
|
||||
) {
|
||||
mTasks.flushTaskList(TaskHandler.TYPE.IMPORT, true, false, false)
|
||||
val listener = ImportExportTask.TaskCompleteListener { result, dataFormat ->
|
||||
onImportComplete(result, targetUri, dataFormat)
|
||||
if (closeWhenDone) {
|
||||
try {
|
||||
target?.close()
|
||||
} catch (ioException: IOException) {
|
||||
ioException.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
importExporter = ImportExportTask(
|
||||
this@ImportExportActivity,
|
||||
dataFormat, target, password, listener
|
||||
)
|
||||
mTasks.executeTask(TaskHandler.TYPE.IMPORT, importExporter)
|
||||
}
|
||||
|
||||
private fun startExport(
|
||||
target: OutputStream?,
|
||||
targetUri: Uri,
|
||||
password: CharArray?,
|
||||
closeWhenDone: Boolean
|
||||
) {
|
||||
mTasks.flushTaskList(TaskHandler.TYPE.EXPORT, true, false, false)
|
||||
val listener = ImportExportTask.TaskCompleteListener { result, dataFormat ->
|
||||
onExportComplete(result, targetUri)
|
||||
if (closeWhenDone) {
|
||||
try {
|
||||
target?.close()
|
||||
} catch (ioException: IOException) {
|
||||
ioException.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
importExporter = ImportExportTask(
|
||||
this@ImportExportActivity,
|
||||
DataFormat.Catima, target, password, listener
|
||||
)
|
||||
mTasks.executeTask(TaskHandler.TYPE.EXPORT, importExporter)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
mTasks.flushTaskList(TaskHandler.TYPE.IMPORT, true, false, false)
|
||||
mTasks.flushTaskList(TaskHandler.TYPE.EXPORT, true, false, false)
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
val id = item.itemId
|
||||
|
||||
if (id == android.R.id.home) {
|
||||
finish()
|
||||
return true
|
||||
}
|
||||
|
||||
return super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
private fun retryWithPassword(dataFormat: DataFormat, uri: Uri) {
|
||||
val builder = MaterialAlertDialogBuilder(this)
|
||||
builder.setTitle(R.string.passwordRequired)
|
||||
|
||||
val container = FrameLayout(this@ImportExportActivity)
|
||||
|
||||
val textInputLayout = TextInputLayout(this@ImportExportActivity).apply {
|
||||
endIconMode = TextInputLayout.END_ICON_PASSWORD_TOGGLE
|
||||
layoutParams = LinearLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
).apply {
|
||||
setMargins(50, 10, 50, 0)
|
||||
}
|
||||
}
|
||||
|
||||
val input = EditText(this@ImportExportActivity).apply {
|
||||
inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD
|
||||
setHint(R.string.exportPasswordHint)
|
||||
}
|
||||
|
||||
textInputLayout.addView(input)
|
||||
container.addView(textInputLayout)
|
||||
builder.setView(container)
|
||||
|
||||
builder.setPositiveButton(R.string.ok) { _, _ ->
|
||||
openFileForImport(uri, input.text.toString().toCharArray())
|
||||
}
|
||||
builder.setNegativeButton(R.string.cancel) { dialogInterface, _ -> dialogInterface.cancel() }
|
||||
|
||||
builder.show()
|
||||
}
|
||||
|
||||
private fun buildResultDialogMessage(result: ImportExportResult, isImport: Boolean): String {
|
||||
val messageId = if (result.resultType() == ImportExportResultType.Success) {
|
||||
if (isImport) R.string.importSuccessful else R.string.exportSuccessful
|
||||
} else {
|
||||
if (isImport) R.string.importFailed else R.string.exportFailed
|
||||
}
|
||||
|
||||
val messageBuilder = StringBuilder(resources.getString(messageId))
|
||||
if (result.developerDetails() != null) {
|
||||
messageBuilder.append("\n\n")
|
||||
messageBuilder.append(resources.getString(R.string.include_if_asking_support))
|
||||
messageBuilder.append("\n\n")
|
||||
messageBuilder.append(result.developerDetails())
|
||||
}
|
||||
|
||||
return messageBuilder.toString()
|
||||
}
|
||||
|
||||
private fun onImportComplete(result: ImportExportResult, path: Uri, dataFormat: DataFormat?) {
|
||||
val resultType = result.resultType()
|
||||
|
||||
if (resultType == ImportExportResultType.BadPassword) {
|
||||
retryWithPassword(dataFormat!!, path)
|
||||
return
|
||||
}
|
||||
|
||||
val builder = MaterialAlertDialogBuilder(this)
|
||||
builder.setTitle(if (resultType == ImportExportResultType.Success) R.string.importSuccessfulTitle else R.string.importFailedTitle)
|
||||
builder.setMessage(buildResultDialogMessage(result, true))
|
||||
builder.setNeutralButton(R.string.ok) { dialog, _ -> dialog.dismiss() }
|
||||
|
||||
builder.create().show()
|
||||
}
|
||||
|
||||
private fun onExportComplete(result: ImportExportResult, path: Uri) {
|
||||
val resultType = result.resultType()
|
||||
|
||||
val builder = MaterialAlertDialogBuilder(this)
|
||||
builder.setTitle(if (resultType == ImportExportResultType.Success) R.string.exportSuccessfulTitle else R.string.exportFailedTitle)
|
||||
builder.setMessage(buildResultDialogMessage(result, false))
|
||||
builder.setNeutralButton(R.string.ok) { dialog, _ -> dialog.dismiss() }
|
||||
|
||||
if (resultType == ImportExportResultType.Success) {
|
||||
val sendLabel = this@ImportExportActivity.resources.getText(R.string.sendLabel)
|
||||
|
||||
builder.setPositiveButton(sendLabel) { dialog, _ ->
|
||||
val sendIntent = Intent(Intent.ACTION_SEND).apply {
|
||||
putExtra(Intent.EXTRA_STREAM, path)
|
||||
type = "text/csv"
|
||||
// set flag to give temporary permission to external app to use the FileProvider
|
||||
flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||
}
|
||||
|
||||
this@ImportExportActivity.startActivity(Intent.createChooser(sendIntent, sendLabel))
|
||||
dialog.dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
builder.create().show()
|
||||
}
|
||||
}
|
||||
@@ -4,11 +4,14 @@ import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.io.InvalidObjectException;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.math.BigDecimal;
|
||||
import java.net.URLDecoder;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Currency;
|
||||
import java.util.Date;
|
||||
@@ -25,6 +28,7 @@ public class ImportURIHelper {
|
||||
private static final String CARD_ID = DBHelper.LoyaltyCardDbIds.CARD_ID;
|
||||
private static final String BARCODE_ID = DBHelper.LoyaltyCardDbIds.BARCODE_ID;
|
||||
private static final String BARCODE_TYPE = DBHelper.LoyaltyCardDbIds.BARCODE_TYPE;
|
||||
private static final String BARCODE_ENCODING = DBHelper.LoyaltyCardDbIds.BARCODE_ENCODING;
|
||||
private static final String HEADER_COLOR = DBHelper.LoyaltyCardDbIds.HEADER_COLOR;
|
||||
|
||||
private final Context context;
|
||||
@@ -66,6 +70,7 @@ public class ImportURIHelper {
|
||||
try {
|
||||
// These values are allowed to be null
|
||||
CatimaBarcode barcodeType = null;
|
||||
Charset barcodeEncoding = StandardCharsets.ISO_8859_1;
|
||||
Date validFrom = null;
|
||||
Date expiry = null;
|
||||
BigDecimal balance = new BigDecimal("0");
|
||||
@@ -103,6 +108,11 @@ public class ImportURIHelper {
|
||||
barcodeType = CatimaBarcode.fromName(unparsedBarcodeType);
|
||||
}
|
||||
|
||||
String unparsedBarcodeEncoding = kv.get(BARCODE_ENCODING);
|
||||
if (unparsedBarcodeEncoding != null && !unparsedBarcodeEncoding.equals("")) {
|
||||
barcodeEncoding = Charset.forName(unparsedBarcodeEncoding);
|
||||
}
|
||||
|
||||
String unparsedBalance = kv.get(BALANCE);
|
||||
if (unparsedBalance != null && !unparsedBalance.equals("")) {
|
||||
balance = new BigDecimal(unparsedBalance);
|
||||
@@ -136,6 +146,7 @@ public class ImportURIHelper {
|
||||
cardId,
|
||||
barcodeId,
|
||||
barcodeType,
|
||||
barcodeEncoding,
|
||||
headerColor,
|
||||
0,
|
||||
Utils.getUnixTime(),
|
||||
@@ -195,6 +206,7 @@ public class ImportURIHelper {
|
||||
if (loyaltyCard.barcodeType != null) {
|
||||
fragment = appendFragment(fragment, BARCODE_TYPE, loyaltyCard.barcodeType.name());
|
||||
}
|
||||
fragment = appendFragment(fragment, BARCODE_ENCODING, loyaltyCard.barcodeEncoding.name());
|
||||
if (loyaltyCard.headerColor != null) {
|
||||
fragment = appendFragment(fragment, HEADER_COLOR, loyaltyCard.headerColor.toString());
|
||||
}
|
||||
|
||||
@@ -1,145 +0,0 @@
|
||||
package protect.card_locker;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.Resources;
|
||||
import android.content.res.TypedArray;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.Typeface;
|
||||
import android.text.TextPaint;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.core.graphics.PaintCompat;
|
||||
|
||||
/**
|
||||
* Original from https://github.com/andOTP/andOTP/blob/master/app/src/main/java/org/shadowice/flocke/andotp/Utilities/LetterBitmap.java
|
||||
* which was originally from http://stackoverflow.com/questions/23122088/colored-boxed-with-letters-a-la-gmail
|
||||
* Used to create a {@link Bitmap} that contains a letter used in the English
|
||||
* alphabet or digit, if there is no letter or digit available, a default image
|
||||
* is shown instead.
|
||||
*/
|
||||
class LetterBitmap {
|
||||
|
||||
/**
|
||||
* The number of available tile colors
|
||||
*/
|
||||
private static final int NUM_OF_TILE_COLORS = 8;
|
||||
/**
|
||||
* The letter bitmap
|
||||
*/
|
||||
private final Bitmap mBitmap;
|
||||
/**
|
||||
* The background color of the letter bitmap
|
||||
*/
|
||||
private final Integer mColor;
|
||||
|
||||
/**
|
||||
* Constructor for <code>LetterTileProvider</code>
|
||||
*
|
||||
* @param context The {@link Context} to use
|
||||
* @param displayName The name used to create the letter for the tile
|
||||
* @param key The key used to generate the background color for the tile
|
||||
* @param tileLetterFontSize The font size used to display the letter
|
||||
* @param width The desired width of the tile
|
||||
* @param height The desired height of the tile
|
||||
* @param backgroundColor (optional) color to use for background.
|
||||
* @param textColor (optional) color to use for text.
|
||||
*/
|
||||
public LetterBitmap(Context context, String displayName, String key, int tileLetterFontSize,
|
||||
int width, int height, Integer backgroundColor, Integer textColor) {
|
||||
TextPaint paint = new TextPaint();
|
||||
|
||||
if (textColor != null) {
|
||||
paint.setColor(textColor);
|
||||
} else {
|
||||
paint.setColor(Color.WHITE);
|
||||
}
|
||||
|
||||
paint.setTextAlign(Paint.Align.CENTER);
|
||||
paint.setAntiAlias(true);
|
||||
paint.setTextSize(tileLetterFontSize);
|
||||
paint.setTypeface(Typeface.defaultFromStyle(Typeface.BOLD));
|
||||
|
||||
if (backgroundColor == null) {
|
||||
mColor = getDefaultColor(context, key);
|
||||
} else {
|
||||
mColor = backgroundColor;
|
||||
}
|
||||
|
||||
mBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
|
||||
String firstChar = displayName.substring(0, 1).toUpperCase();
|
||||
int firstCharEnd = 2;
|
||||
while (firstCharEnd <= displayName.length()) {
|
||||
// Test for the longest render-able string
|
||||
// But ignore containing only a-Z0-9 to not render things like ffi as a single character
|
||||
String test = displayName.substring(0, firstCharEnd);
|
||||
if (!isAlphabetical(test) && PaintCompat.hasGlyph(paint, test)) {
|
||||
firstChar = test;
|
||||
}
|
||||
firstCharEnd++;
|
||||
}
|
||||
|
||||
Log.d("LetterBitmap", "using sequence " + firstChar + " to render first char which has length " + firstChar.length());
|
||||
|
||||
final Canvas c = new Canvas();
|
||||
c.setBitmap(mBitmap);
|
||||
c.drawColor(mColor);
|
||||
|
||||
Rect bounds = new Rect();
|
||||
paint.getTextBounds(firstChar, 0, firstChar.length(), bounds);
|
||||
c.drawText(firstChar,
|
||||
0, firstChar.length(),
|
||||
width / 2.0f, (height - (bounds.bottom + bounds.top)) / 2.0f
|
||||
, paint);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @return A {@link Bitmap} that contains a letter used in the English
|
||||
* alphabet or digit, if there is no letter or digit available, a
|
||||
* default image is shown instead
|
||||
*/
|
||||
public Bitmap getLetterTile() {
|
||||
return mBitmap;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return background color used for letter title.
|
||||
*/
|
||||
public int getBackgroundColor() {
|
||||
return mColor;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param key The key used to generate the tile color
|
||||
* @return A new or previously chosen color for <code>key</code> used as the
|
||||
* tile background color
|
||||
*/
|
||||
private static int pickColor(String key, TypedArray colors) {
|
||||
// String.hashCode() is not supposed to change across java versions, so
|
||||
// this should guarantee the same key always maps to the same color
|
||||
final int color = Math.abs(key.hashCode()) % NUM_OF_TILE_COLORS;
|
||||
return colors.getColor(color, Color.BLACK);
|
||||
}
|
||||
|
||||
private static boolean isAlphabetical(String string) {
|
||||
return string.matches("[a-zA-Z0-9]*");
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine the color which the letter tile will use if no default
|
||||
* color is provided.
|
||||
*/
|
||||
public static int getDefaultColor(Context context, String key) {
|
||||
final Resources res = context.getResources();
|
||||
|
||||
TypedArray colors = res.obtainTypedArray(R.array.letter_tile_colors);
|
||||
int color = pickColor(key, colors);
|
||||
colors.recycle();
|
||||
|
||||
return color;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
package protect.card_locker
|
||||
|
||||
import android.content.Context
|
||||
import android.content.res.TypedArray
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Color
|
||||
import android.graphics.Paint
|
||||
import android.graphics.Rect
|
||||
import android.graphics.Typeface
|
||||
import android.text.TextPaint
|
||||
import android.util.Log
|
||||
import androidx.core.graphics.PaintCompat
|
||||
import java.util.Locale
|
||||
import kotlin.math.abs
|
||||
|
||||
/**
|
||||
* Original from https://github.com/andOTP/andOTP/blob/master/app/src/main/java/org/shadowice/flocke/andotp/Utilities/LetterBitmap.java
|
||||
* which was originally from http://stackoverflow.com/questions/23122088/colored-boxed-with-letters-a-la-gmail
|
||||
* Used to create a {@link Bitmap} that contains a letter used in the English
|
||||
* alphabet or digit, if there is no letter or digit available, a default image
|
||||
* is shown instead.
|
||||
*
|
||||
* @constructor Constructor for <code>LetterTileProvider</code>
|
||||
* @param context The {@link Context} to use
|
||||
* @param displayName The name used to create the letter for the tile
|
||||
* @param key The key used to generate the background color for the tile
|
||||
* @param tileLetterFontSize The font size used to display the letter
|
||||
* @param width The desired width of the tile
|
||||
* @param height The desired height of the tile
|
||||
* @param backgroundColor (optional) color to use for background.
|
||||
* @param textColor (optional) color to use for text.
|
||||
*/
|
||||
class LetterBitmap(
|
||||
context: Context, displayName: String, key: String, tileLetterFontSize: Int,
|
||||
width: Int, height: Int, backgroundColor: Int?, textColor: Int?
|
||||
) {
|
||||
/**
|
||||
* A {@link Bitmap} that contains a letter used in the English
|
||||
* alphabet or digit, if there is no letter or digit available, a
|
||||
* default image is shown instead
|
||||
*/
|
||||
private val letterTile: Bitmap
|
||||
|
||||
/**
|
||||
* The background color of the letter bitmap
|
||||
*/
|
||||
private val mColor: Int
|
||||
|
||||
init {
|
||||
val paint = TextPaint().apply {
|
||||
color = textColor ?: Color.WHITE
|
||||
textAlign = Paint.Align.CENTER
|
||||
isAntiAlias = true
|
||||
textSize = tileLetterFontSize.toFloat()
|
||||
typeface = Typeface.defaultFromStyle(Typeface.BOLD)
|
||||
}
|
||||
|
||||
mColor = backgroundColor ?: getDefaultColor(context, key)
|
||||
|
||||
this.letterTile = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
|
||||
|
||||
var firstChar = displayName.substring(0, 1).uppercase(Locale.getDefault())
|
||||
var firstCharEnd = 2
|
||||
while (firstCharEnd <= displayName.length) {
|
||||
// Test for the longest render-able string
|
||||
// But ignore containing only a-Z0-9 to not render things like ffi as a single character
|
||||
val test = displayName.substring(0, firstCharEnd)
|
||||
if (!isAlphabetical(test) && PaintCompat.hasGlyph(paint, test)) {
|
||||
firstChar = test
|
||||
}
|
||||
firstCharEnd++
|
||||
}
|
||||
|
||||
Log.d(
|
||||
"LetterBitmap",
|
||||
"using sequence $firstChar to render first char which has length ${firstChar.length}"
|
||||
)
|
||||
|
||||
Canvas().apply {
|
||||
setBitmap(this@LetterBitmap.letterTile)
|
||||
drawColor(mColor)
|
||||
|
||||
val bounds = Rect()
|
||||
paint.getTextBounds(firstChar, 0, firstChar.length, bounds)
|
||||
drawText(
|
||||
firstChar,
|
||||
0, firstChar.length,
|
||||
width / 2.0f, (height - (bounds.bottom + bounds.top)) / 2.0f,
|
||||
paint
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val backgroundColor: Int
|
||||
/**
|
||||
* @return background color used for letter title.
|
||||
*/
|
||||
get() = mColor
|
||||
|
||||
fun getLetterTile(): Bitmap {
|
||||
return letterTile
|
||||
}
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* @param key The key used to generate the tile color
|
||||
* @return A new or previously chosen color for `key` used as the
|
||||
* tile background color
|
||||
*/
|
||||
private fun pickColor(key: String, colors: TypedArray): Int {
|
||||
// String.hashCode() is not supposed to change across java versions, so
|
||||
// this should guarantee the same key always maps to the same color
|
||||
val color = abs(key.hashCode()) % colors.length()
|
||||
return colors.getColor(color, Color.BLACK)
|
||||
}
|
||||
|
||||
private fun isAlphabetical(string: String): Boolean {
|
||||
return string.matches("[a-zA-Z0-9]*".toRegex())
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine the color which the letter tile will use if no default
|
||||
* color is provided.
|
||||
*/
|
||||
fun getDefaultColor(context: Context, key: String): Int {
|
||||
val res = context.resources
|
||||
|
||||
val colors = res.obtainTypedArray(R.array.letter_tile_colors)
|
||||
val color: Int = pickColor(key, colors)
|
||||
colors.recycle()
|
||||
|
||||
return color
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,7 @@ import android.view.View
|
||||
import android.widget.RemoteViews
|
||||
import androidx.core.widget.RemoteViewsCompat
|
||||
import protect.card_locker.DBHelper.LoyaltyCardArchiveFilter
|
||||
import protect.card_locker.cardview.LoyaltyCardViewActivity
|
||||
|
||||
class ListWidget : AppWidgetProvider() {
|
||||
fun updateAll(context: Context) {
|
||||
@@ -69,7 +70,9 @@ class ListWidget : AppWidgetProvider() {
|
||||
if (hasCards) {
|
||||
// If we have cards, create the list
|
||||
views = RemoteViews(context.packageName, R.layout.list_widget)
|
||||
val templateIntent = Intent(context, LoyaltyCardViewActivity::class.java)
|
||||
val templateIntent = Intent(context, LoyaltyCardViewActivity::class.java).apply {
|
||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||
}
|
||||
val pendingIntent = PendingIntent.getActivity(
|
||||
context,
|
||||
0,
|
||||
@@ -100,8 +103,7 @@ class ListWidget : AppWidgetProvider() {
|
||||
val foreground = if (Utils.needsDarkForeground(headerColor)) Color.BLACK else Color.WHITE
|
||||
setInt(R.id.item_container_foreground, "setBackgroundColor", headerColor)
|
||||
val icon = loyaltyCard.getImageThumbnail(context)
|
||||
// setImageViewIcon is not supported on Android 5, so force Android 5 down the text path
|
||||
// FIXME: The icon flow causes a crash up to Android 12L, so SDK_INT is forced up from 23 to 33
|
||||
// FIXME: The icon flow causes a crash up to Android 12L, so force anything below 33 down this path
|
||||
if (icon != null && Build.VERSION.SDK_INT >= 32) {
|
||||
setInt(R.id.item_container_foreground, "setBackgroundColor", foreground)
|
||||
setImageViewIcon(R.id.item_image, Icon.createWithBitmap(icon))
|
||||
@@ -128,4 +130,4 @@ class ListWidget : AppWidgetProvider() {
|
||||
|
||||
return remoteViews
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,8 @@ import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Currency;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
@@ -30,6 +32,7 @@ public class LoyaltyCard {
|
||||
public String barcodeId;
|
||||
@Nullable
|
||||
public CatimaBarcode barcodeType;
|
||||
public Charset barcodeEncoding;
|
||||
@Nullable
|
||||
public Integer headerColor;
|
||||
public int starStatus;
|
||||
@@ -62,6 +65,7 @@ public class LoyaltyCard {
|
||||
public static final String BUNDLE_LOYALTY_CARD_CARD_ID = "loyaltyCardCardId";
|
||||
public static final String BUNDLE_LOYALTY_CARD_BARCODE_ID = "loyaltyCardBarcodeId";
|
||||
public static final String BUNDLE_LOYALTY_CARD_BARCODE_TYPE = "loyaltyCardBarcodeType";
|
||||
public static final String BUNDLE_LOYALTY_CARD_BARCODE_ENCODING = "loyaltyCardBarcodeEncoding";
|
||||
public static final String BUNDLE_LOYALTY_CARD_HEADER_COLOR = "loyaltyCardHeaderColor";
|
||||
public static final String BUNDLE_LOYALTY_CARD_STAR_STATUS = "loyaltyCardStarStatus";
|
||||
public static final String BUNDLE_LOYALTY_CARD_LAST_USED = "loyaltyCardLastUsed";
|
||||
@@ -90,6 +94,7 @@ public class LoyaltyCard {
|
||||
setCardId("");
|
||||
setBarcodeId(null);
|
||||
setBarcodeType(null);
|
||||
setBarcodeEncoding(StandardCharsets.ISO_8859_1);
|
||||
setHeaderColor(null);
|
||||
setStarStatus(0);
|
||||
setLastUsed(Utils.getUnixTime());
|
||||
@@ -124,7 +129,7 @@ public class LoyaltyCard {
|
||||
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,
|
||||
@NonNull final Charset barcodeEncoding, @Nullable final Integer headerColor, final int starStatus,
|
||||
final long lastUsed, final int zoomLevel, final int zoomLevelWidth, final int archiveStatus,
|
||||
@Nullable Bitmap imageThumbnail, @Nullable String imageThumbnailPath,
|
||||
@Nullable Bitmap imageFront, @Nullable String imageFrontPath,
|
||||
@@ -139,6 +144,7 @@ public class LoyaltyCard {
|
||||
setCardId(cardId);
|
||||
setBarcodeId(barcodeId);
|
||||
setBarcodeType(barcodeType);
|
||||
setBarcodeEncoding(barcodeEncoding);
|
||||
setHeaderColor(headerColor);
|
||||
setStarStatus(starStatus);
|
||||
setLastUsed(lastUsed);
|
||||
@@ -244,6 +250,10 @@ public class LoyaltyCard {
|
||||
this.barcodeType = barcodeType;
|
||||
}
|
||||
|
||||
public void setBarcodeEncoding(@NonNull Charset barcodeEncoding) {
|
||||
this.barcodeEncoding = barcodeEncoding;
|
||||
}
|
||||
|
||||
public void setHeaderColor(@Nullable Integer headerColor) {
|
||||
this.headerColor = headerColor;
|
||||
}
|
||||
@@ -379,6 +389,11 @@ public class LoyaltyCard {
|
||||
} else if (requireFull) {
|
||||
throw new IllegalArgumentException("Missing key " + BUNDLE_LOYALTY_CARD_BARCODE_TYPE);
|
||||
}
|
||||
if (bundle.containsKey(BUNDLE_LOYALTY_CARD_BARCODE_ENCODING)) {
|
||||
setBarcodeEncoding(Charset.forName(bundle.getString(BUNDLE_LOYALTY_CARD_BARCODE_ENCODING)));
|
||||
} else if (requireFull) {
|
||||
throw new IllegalArgumentException("Missing key " + BUNDLE_LOYALTY_CARD_BARCODE_ENCODING);
|
||||
}
|
||||
if (bundle.containsKey(BUNDLE_LOYALTY_CARD_HEADER_COLOR)) {
|
||||
int tmpHeaderColor = bundle.getInt(BUNDLE_LOYALTY_CARD_HEADER_COLOR);
|
||||
setHeaderColor(tmpHeaderColor != -1 ? tmpHeaderColor : null);
|
||||
@@ -462,6 +477,9 @@ public class LoyaltyCard {
|
||||
if (!exportIsLimited || exportLimit.contains(BUNDLE_LOYALTY_CARD_BARCODE_TYPE)) {
|
||||
bundle.putString(BUNDLE_LOYALTY_CARD_BARCODE_TYPE, barcodeType != null ? barcodeType.name() : null);
|
||||
}
|
||||
if (!exportIsLimited || exportLimit.contains(BUNDLE_LOYALTY_CARD_BARCODE_ENCODING)) {
|
||||
bundle.putString(BUNDLE_LOYALTY_CARD_BARCODE_ENCODING, barcodeEncoding.name());
|
||||
}
|
||||
if (!exportIsLimited || exportLimit.contains(BUNDLE_LOYALTY_CARD_HEADER_COLOR)) {
|
||||
bundle.putInt(BUNDLE_LOYALTY_CARD_HEADER_COLOR, headerColor != null ? headerColor : -1);
|
||||
}
|
||||
@@ -539,6 +557,8 @@ public class LoyaltyCard {
|
||||
// barcodeType
|
||||
int barcodeTypeColumn = cursor.getColumnIndexOrThrow(DBHelper.LoyaltyCardDbIds.BARCODE_TYPE);
|
||||
CatimaBarcode barcodeType = !cursor.isNull(barcodeTypeColumn) ? CatimaBarcode.fromName(cursor.getString(barcodeTypeColumn)) : null;
|
||||
// barcodeEncoding
|
||||
Charset barcodeEncoding = Charset.forName(cursor.getString(cursor.getColumnIndexOrThrow(DBHelper.LoyaltyCardDbIds.BARCODE_ENCODING)));
|
||||
// headerColor
|
||||
int headerColorColumn = cursor.getColumnIndexOrThrow(DBHelper.LoyaltyCardDbIds.HEADER_COLOR);
|
||||
Integer headerColor = !cursor.isNull(headerColorColumn) ? cursor.getInt(headerColorColumn) : null;
|
||||
@@ -564,6 +584,7 @@ public class LoyaltyCard {
|
||||
cardId,
|
||||
barcodeId,
|
||||
barcodeType,
|
||||
barcodeEncoding,
|
||||
headerColor,
|
||||
starStatus,
|
||||
lastUsed,
|
||||
@@ -593,6 +614,7 @@ public class LoyaltyCard {
|
||||
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()
|
||||
a.barcodeEncoding.name().equals(b.barcodeEncoding.name()) && // non-nullable String
|
||||
Utils.equals(a.headerColor, b.headerColor) && // nullable Integer
|
||||
a.starStatus == b.starStatus && // non-nullable int
|
||||
a.archiveStatus == b.archiveStatus && // non-nullable int
|
||||
@@ -619,7 +641,7 @@ public class LoyaltyCard {
|
||||
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"
|
||||
+ " balance=%s,%n balanceType=%s,%n cardId=%s,%n barcodeId=%s,%n barcodeType=%s,%n barcodeEncoding=%s,%n"
|
||||
+ " headerColor=%s,%n starStatus=%s,%n lastUsed=%s,%n zoomLevel=%s,%n zoomLevelWidth=%s,%n archiveStatus=%s%n"
|
||||
+ " imageThumbnail=%s,%n imageThumbnailPath=%s,%n imageFront=%s,%n imageFrontPath=%s,%n imageBack=%s,%n imageBackPath=%s,%n}",
|
||||
this.id,
|
||||
@@ -632,6 +654,7 @@ public class LoyaltyCard {
|
||||
this.cardId,
|
||||
this.barcodeId,
|
||||
this.barcodeType != null ? this.barcodeType.format() : null,
|
||||
this.barcodeEncoding.name(),
|
||||
this.headerColor,
|
||||
this.starStatus,
|
||||
this.lastUsed,
|
||||
|
||||
@@ -105,6 +105,12 @@ public class LoyaltyCardCursorAdapter extends BaseCursorAdapter<LoyaltyCardCurso
|
||||
inputHolder.setNoteField(null);
|
||||
}
|
||||
|
||||
if (mLoyaltyCardListDisplayOptions.showingCardId() && !loyaltyCard.cardId.isEmpty()) {
|
||||
inputHolder.setExtraField(inputHolder.mCardIdField, loyaltyCard.cardId, null, showDivider);
|
||||
} else {
|
||||
inputHolder.setExtraField(inputHolder.mCardIdField, null, null, false);
|
||||
}
|
||||
|
||||
if (mLoyaltyCardListDisplayOptions.showingBalance() && !loyaltyCard.balance.equals(new BigDecimal("0"))) {
|
||||
inputHolder.setExtraField(inputHolder.mBalanceField, Utils.formatBalance(mContext, loyaltyCard.balance, loyaltyCard.balanceType), null, showDivider);
|
||||
} else {
|
||||
@@ -211,7 +217,7 @@ public class LoyaltyCardCursorAdapter extends BaseCursorAdapter<LoyaltyCardCurso
|
||||
|
||||
public class LoyaltyCardListItemViewHolder extends RecyclerView.ViewHolder {
|
||||
|
||||
public TextView mCardText, mStoreField, mNoteField, mBalanceField, mValidFromField, mExpiryField;
|
||||
public TextView mCardText, mStoreField, mNoteField, mCardIdField, mBalanceField, mValidFromField, mExpiryField;
|
||||
public ImageView mCardIcon, mTickIcon;
|
||||
public MaterialCardView mRow;
|
||||
public ConstraintLayout mStar, mArchived;
|
||||
@@ -224,6 +230,7 @@ public class LoyaltyCardCursorAdapter extends BaseCursorAdapter<LoyaltyCardCurso
|
||||
mDivider = loyaltyCardLayoutBinding.infoDivider;
|
||||
mStoreField = loyaltyCardLayoutBinding.store;
|
||||
mNoteField = loyaltyCardLayoutBinding.note;
|
||||
mCardIdField = loyaltyCardLayoutBinding.cardId;
|
||||
mBalanceField = loyaltyCardLayoutBinding.balance;
|
||||
mValidFromField = loyaltyCardLayoutBinding.validFrom;
|
||||
mExpiryField = loyaltyCardLayoutBinding.expiry;
|
||||
|
||||
@@ -70,6 +70,8 @@ import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.InvalidObjectException;
|
||||
import java.math.BigDecimal;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.text.DateFormat;
|
||||
import java.text.ParseException;
|
||||
import java.util.ArrayList;
|
||||
@@ -123,11 +125,12 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity implements
|
||||
ChipGroup groupsChips;
|
||||
AutoCompleteTextView validFromField;
|
||||
AutoCompleteTextView expiryField;
|
||||
EditText balanceField;
|
||||
AutoCompleteTextView balanceCurrencyField;
|
||||
EditText balanceField;
|
||||
TextView cardIdFieldView;
|
||||
AutoCompleteTextView barcodeIdField;
|
||||
AutoCompleteTextView barcodeTypeField;
|
||||
AutoCompleteTextView barcodeEncodingField;
|
||||
ImageView barcodeImage;
|
||||
View barcodeImageLayout;
|
||||
View barcodeCaptureLayout;
|
||||
@@ -148,9 +151,9 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity implements
|
||||
boolean onRestoring = false;
|
||||
AlertDialog confirmExitDialog = null;
|
||||
|
||||
boolean validBalance = true;
|
||||
HashMap<String, Currency> currencies = new HashMap<>();
|
||||
HashMap<String, String> currencySymbols = new HashMap<>();
|
||||
boolean validBalance = true;
|
||||
|
||||
ActivityResultLauncher<Uri> mPhotoTakerLauncher;
|
||||
ActivityResultLauncher<Intent> mPhotoPickerLauncher;
|
||||
@@ -193,14 +196,14 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity implements
|
||||
viewModel.setHasChanged(true);
|
||||
}
|
||||
|
||||
protected void setLoyaltyCardBalance(@NonNull BigDecimal balance) {
|
||||
viewModel.getLoyaltyCard().setBalance(balance);
|
||||
protected void setLoyaltyCardBalanceType(@Nullable Currency balanceType) {
|
||||
viewModel.getLoyaltyCard().setBalanceType(balanceType);
|
||||
|
||||
viewModel.setHasChanged(true);
|
||||
}
|
||||
|
||||
protected void setLoyaltyCardBalanceType(@Nullable Currency balanceType) {
|
||||
viewModel.getLoyaltyCard().setBalanceType(balanceType);
|
||||
protected void setLoyaltyCardBalance(@NonNull BigDecimal balance) {
|
||||
viewModel.getLoyaltyCard().setBalance(balance);
|
||||
|
||||
viewModel.setHasChanged(true);
|
||||
}
|
||||
@@ -229,6 +232,14 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity implements
|
||||
viewModel.setHasChanged(true);
|
||||
}
|
||||
|
||||
protected void setLoyaltyCardBarcodeEncoding(@NonNull Charset barcodeEncoding) {
|
||||
viewModel.getLoyaltyCard().setBarcodeEncoding(barcodeEncoding);
|
||||
|
||||
generateBarcode();
|
||||
|
||||
viewModel.setHasChanged(true);
|
||||
}
|
||||
|
||||
protected void setLoyaltyCardHeaderColor(@Nullable Integer headerColor) {
|
||||
viewModel.getLoyaltyCard().setHeaderColor(headerColor);
|
||||
|
||||
@@ -329,11 +340,12 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity implements
|
||||
groupsChips = binding.groupChips;
|
||||
validFromField = binding.validFromField;
|
||||
expiryField = binding.expiryField;
|
||||
balanceField = binding.balanceField;
|
||||
balanceCurrencyField = binding.balanceCurrencyField;
|
||||
balanceField = binding.balanceField;
|
||||
cardIdFieldView = binding.cardIdView;
|
||||
barcodeIdField = binding.barcodeIdField;
|
||||
barcodeTypeField = binding.barcodeTypeField;
|
||||
barcodeEncodingField = binding.barcodeEncodingField;
|
||||
barcodeImage = binding.barcode;
|
||||
barcodeImage.setClipToOutline(true);
|
||||
barcodeImageLayout = binding.barcodeLayout;
|
||||
@@ -373,33 +385,6 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity implements
|
||||
|
||||
setMaterialDatePickerResultListener();
|
||||
|
||||
balanceField.setOnFocusChangeListener((v, hasFocus) -> {
|
||||
if (!hasFocus && !onResuming && !onRestoring) {
|
||||
if (balanceField.getText().toString().isEmpty()) {
|
||||
setLoyaltyCardBalance(BigDecimal.valueOf(0));
|
||||
}
|
||||
|
||||
balanceField.setText(Utils.formatBalanceWithoutCurrencySymbol(viewModel.getLoyaltyCard().balance, viewModel.getLoyaltyCard().balanceType));
|
||||
}
|
||||
});
|
||||
|
||||
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(), viewModel.getLoyaltyCard().balanceType);
|
||||
setLoyaltyCardBalance(balance);
|
||||
balanceField.setError(null);
|
||||
validBalance = true;
|
||||
} catch (ParseException e) {
|
||||
e.printStackTrace();
|
||||
balanceField.setError(getString(R.string.balanceParsingFailed));
|
||||
validBalance = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
balanceCurrencyField.addTextChangedListener(new SimpleTextWatcher() {
|
||||
@Override
|
||||
public void onTextChanged(CharSequence s, int start, int before, int count) {
|
||||
@@ -452,6 +437,33 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity implements
|
||||
}
|
||||
});
|
||||
|
||||
balanceField.setOnFocusChangeListener((v, hasFocus) -> {
|
||||
if (!hasFocus && !onResuming && !onRestoring) {
|
||||
if (balanceField.getText().toString().isEmpty()) {
|
||||
setLoyaltyCardBalance(BigDecimal.valueOf(0));
|
||||
}
|
||||
|
||||
balanceField.setText(Utils.formatBalanceWithoutCurrencySymbol(viewModel.getLoyaltyCard().balance, viewModel.getLoyaltyCard().balanceType));
|
||||
}
|
||||
});
|
||||
|
||||
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(), viewModel.getLoyaltyCard().balanceType);
|
||||
setLoyaltyCardBalance(balance);
|
||||
balanceField.setError(null);
|
||||
validBalance = true;
|
||||
} catch (ParseException e) {
|
||||
e.printStackTrace();
|
||||
balanceField.setError(getString(R.string.balanceParsingFailed));
|
||||
validBalance = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
cardIdFieldView.addTextChangedListener(new SimpleTextWatcher() {
|
||||
@Override
|
||||
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
|
||||
@@ -577,6 +589,25 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity implements
|
||||
}
|
||||
});
|
||||
|
||||
barcodeEncodingField.addTextChangedListener(new SimpleTextWatcher() {
|
||||
@Override
|
||||
public void onTextChanged(CharSequence s, int start, int before, int count) {
|
||||
if (!s.toString().isEmpty()) {
|
||||
Log.d(TAG, "Setting barcode encoding to " + s.toString());
|
||||
setLoyaltyCardBarcodeEncoding(Charset.forName(s.toString()));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterTextChanged(Editable s) {
|
||||
ArrayList<String> barcodeEncodingList = new ArrayList<>();
|
||||
barcodeEncodingList.add(StandardCharsets.ISO_8859_1.name());
|
||||
barcodeEncodingList.add(StandardCharsets.UTF_8.name());
|
||||
ArrayAdapter<String> barcodeEncodingAdapter = new ArrayAdapter<>(LoyaltyCardEditActivity.this, android.R.layout.select_dialog_item, barcodeEncodingList);
|
||||
barcodeEncodingField.setAdapter(barcodeEncodingAdapter);
|
||||
}
|
||||
});
|
||||
|
||||
binding.tabs.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
|
||||
@Override
|
||||
public void onTabSelected(TabLayout.Tab tab) {
|
||||
@@ -688,6 +719,14 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity implements
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
if (mDatabase != null && mDatabase.isOpen()) {
|
||||
mDatabase.close();
|
||||
}
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
private void selectTab(int index) {
|
||||
binding.tabs.selectTab(binding.tabs.getTabAt(index));
|
||||
viewModel.setTabIndex(index);
|
||||
@@ -719,7 +758,6 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity implements
|
||||
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);
|
||||
mCropperOptions.setToolbarWidgetColor(colorOnSurface);
|
||||
mCropperOptions.setRootViewBackgroundColor(colorBackground);
|
||||
// set tool tip to be the darker of primary color
|
||||
@@ -774,6 +812,8 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity implements
|
||||
barcodeIdField.setText(barcodeId != null && !barcodeId.isEmpty() ? barcodeId : getString(R.string.sameAsCardId));
|
||||
CatimaBarcode barcodeType = viewModel.getLoyaltyCard().barcodeType;
|
||||
barcodeTypeField.setText(barcodeType != null ? barcodeType.prettyName() : getString(R.string.noBarcode));
|
||||
Charset barcodeEncoding = viewModel.getLoyaltyCard().barcodeEncoding;
|
||||
barcodeEncodingField.setText(barcodeEncoding.name());
|
||||
|
||||
// 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.
|
||||
@@ -1480,9 +1520,9 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity implements
|
||||
// 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 (viewModel.getUpdateLoyaltyCard()) {
|
||||
DBHelper.updateLoyaltyCard(mDatabase, viewModel.getLoyaltyCardId(), viewModel.getLoyaltyCard().store, viewModel.getLoyaltyCard().note, viewModel.getLoyaltyCard().validFrom, viewModel.getLoyaltyCard().expiry, viewModel.getLoyaltyCard().balance, viewModel.getLoyaltyCard().balanceType, viewModel.getLoyaltyCard().cardId, viewModel.getLoyaltyCard().barcodeId, viewModel.getLoyaltyCard().barcodeType, viewModel.getLoyaltyCard().headerColor, viewModel.getLoyaltyCard().starStatus, null, viewModel.getLoyaltyCard().archiveStatus);
|
||||
DBHelper.updateLoyaltyCard(mDatabase, viewModel.getLoyaltyCardId(), viewModel.getLoyaltyCard().store, viewModel.getLoyaltyCard().note, viewModel.getLoyaltyCard().validFrom, viewModel.getLoyaltyCard().expiry, viewModel.getLoyaltyCard().balance, viewModel.getLoyaltyCard().balanceType, viewModel.getLoyaltyCard().cardId, viewModel.getLoyaltyCard().barcodeId, viewModel.getLoyaltyCard().barcodeType, viewModel.getLoyaltyCard().barcodeEncoding, viewModel.getLoyaltyCard().headerColor, viewModel.getLoyaltyCard().starStatus, null, viewModel.getLoyaltyCard().archiveStatus);
|
||||
} else {
|
||||
viewModel.setLoyaltyCardId((int) DBHelper.insertLoyaltyCard(mDatabase, viewModel.getLoyaltyCard().store, viewModel.getLoyaltyCard().note, viewModel.getLoyaltyCard().validFrom, viewModel.getLoyaltyCard().expiry, viewModel.getLoyaltyCard().balance, viewModel.getLoyaltyCard().balanceType, viewModel.getLoyaltyCard().cardId, viewModel.getLoyaltyCard().barcodeId, viewModel.getLoyaltyCard().barcodeType, viewModel.getLoyaltyCard().headerColor, 0, null, 0));
|
||||
viewModel.setLoyaltyCardId((int) DBHelper.insertLoyaltyCard(mDatabase, viewModel.getLoyaltyCard().store, viewModel.getLoyaltyCard().note, viewModel.getLoyaltyCard().validFrom, viewModel.getLoyaltyCard().expiry, viewModel.getLoyaltyCard().balance, viewModel.getLoyaltyCard().balanceType, viewModel.getLoyaltyCard().cardId, viewModel.getLoyaltyCard().barcodeId, viewModel.getLoyaltyCard().barcodeType, viewModel.getLoyaltyCard().barcodeEncoding, viewModel.getLoyaltyCard().headerColor, 0, null, 0));
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -1495,7 +1535,7 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity implements
|
||||
|
||||
DBHelper.setLoyaltyCardGroups(mDatabase, viewModel.getLoyaltyCardId(), selectedGroups);
|
||||
|
||||
ShortcutHelper.updateShortcuts(this, DBHelper.getLoyaltyCard(this, mDatabase, viewModel.getLoyaltyCardId()));
|
||||
ShortcutHelper.updateShortcuts(this);
|
||||
|
||||
if (viewModel.getDuplicateFromLoyaltyCardId()) {
|
||||
Intent intent = new Intent(getApplicationContext(), MainActivity.class);
|
||||
@@ -1597,6 +1637,7 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity implements
|
||||
|
||||
String cardIdString = viewModel.getLoyaltyCard().barcodeId != null ? viewModel.getLoyaltyCard().barcodeId : viewModel.getLoyaltyCard().cardId;
|
||||
CatimaBarcode barcodeFormat = viewModel.getLoyaltyCard().barcodeType;
|
||||
Charset barcodeEncoding = viewModel.getLoyaltyCard().barcodeEncoding;
|
||||
|
||||
if (cardIdString == null || cardIdString.isEmpty() || barcodeFormat == null) {
|
||||
barcodeImageLayout.setVisibility(View.GONE);
|
||||
@@ -1616,13 +1657,13 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity implements
|
||||
barcodeImage.getViewTreeObserver().removeOnGlobalLayoutListener(this);
|
||||
|
||||
Log.d(TAG, "ImageView size now known");
|
||||
BarcodeImageWriterTask barcodeWriter = new BarcodeImageWriterTask(getApplicationContext(), barcodeImage, cardIdString, barcodeFormat, null, false, LoyaltyCardEditActivity.this, true, false);
|
||||
BarcodeImageWriterTask barcodeWriter = new BarcodeImageWriterTask(getApplicationContext(), barcodeImage, cardIdString, barcodeFormat, barcodeEncoding, null, false, LoyaltyCardEditActivity.this, true, false);
|
||||
viewModel.getTaskHandler().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, this, true, false);
|
||||
BarcodeImageWriterTask barcodeWriter = new BarcodeImageWriterTask(getApplicationContext(), barcodeImage, cardIdString, barcodeFormat, barcodeEncoding, null, false, this, true, false);
|
||||
viewModel.getTaskHandler().executeTask(TaskHandler.TYPE.BARCODE, barcodeWriter);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ public class LoyaltyCardListDisplayOptionsManager {
|
||||
|
||||
private boolean mShowNameBelowThumbnail;
|
||||
private boolean mShowNote;
|
||||
private boolean mShowCardId;
|
||||
private boolean mShowBalance;
|
||||
private boolean mShowValidity;
|
||||
private boolean mShowArchivedCards;
|
||||
@@ -50,6 +51,7 @@ public class LoyaltyCardListDisplayOptionsManager {
|
||||
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);
|
||||
mShowCardId = mCardDetailsPref.getBoolean(mContext.getString(R.string.sharedpreference_card_details_show_card_id), false);
|
||||
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);
|
||||
@@ -83,6 +85,17 @@ public class LoyaltyCardListDisplayOptionsManager {
|
||||
return mShowNote;
|
||||
}
|
||||
|
||||
public void showCardId(boolean show) {
|
||||
mShowCardId = show;
|
||||
mRefreshCardsCallback.run();
|
||||
|
||||
saveDetailState(R.string.sharedpreference_card_details_show_card_id, show);
|
||||
}
|
||||
|
||||
public boolean showingCardId() {
|
||||
return mShowCardId;
|
||||
}
|
||||
|
||||
public void showBalance(boolean show) {
|
||||
mShowBalance = show;
|
||||
mRefreshCardsCallback.run();
|
||||
@@ -137,6 +150,11 @@ public class LoyaltyCardListDisplayOptionsManager {
|
||||
showingNote(),
|
||||
this::showNote
|
||||
));
|
||||
displayOptions.add(new LoyaltyCardDisplayOption(
|
||||
mContext.getString(R.string.show_card_id),
|
||||
showingCardId(),
|
||||
this::showCardId
|
||||
));
|
||||
displayOptions.add(new LoyaltyCardDisplayOption(
|
||||
mContext.getString(R.string.show_balance),
|
||||
showingBalance(),
|
||||
|
||||
@@ -1,882 +0,0 @@
|
||||
package protect.card_locker;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.SearchManager;
|
||||
import android.appwidget.AppWidgetManager;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.database.CursorIndexOutOfBoundsException;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.util.DisplayMetrics;
|
||||
import android.util.Log;
|
||||
import android.util.TypedValue;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.widget.CheckBox;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.activity.OnBackPressedCallback;
|
||||
import androidx.activity.result.ActivityResultLauncher;
|
||||
import androidx.activity.result.contract.ActivityResultContracts;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.view.ActionMode;
|
||||
import androidx.appcompat.widget.SearchView;
|
||||
import androidx.core.splashscreen.SplashScreen;
|
||||
import androidx.recyclerview.widget.GridLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
import com.google.android.material.floatingactionbutton.FloatingActionButton;
|
||||
import com.google.android.material.tabs.TabLayout;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import protect.card_locker.databinding.ContentMainBinding;
|
||||
import protect.card_locker.databinding.MainActivityBinding;
|
||||
import protect.card_locker.databinding.SortingOptionBinding;
|
||||
import protect.card_locker.preferences.Settings;
|
||||
import protect.card_locker.preferences.SettingsActivity;
|
||||
|
||||
public class MainActivity extends CatimaAppCompatActivity implements LoyaltyCardCursorAdapter.CardAdapterListener {
|
||||
private MainActivityBinding binding;
|
||||
private ContentMainBinding contentMainBinding;
|
||||
private static final String TAG = "Catima";
|
||||
public static final String RESTART_ACTIVITY_INTENT = "restart_activity_intent";
|
||||
|
||||
private static final int MEDIUM_SCALE_FACTOR_DIP = 460;
|
||||
static final String STATE_SEARCH_QUERY = "SEARCH_QUERY";
|
||||
|
||||
private SQLiteDatabase mDatabase;
|
||||
private LoyaltyCardCursorAdapter mAdapter;
|
||||
private ActionMode mCurrentActionMode;
|
||||
private SearchView mSearchView;
|
||||
private int mLoyaltyCardCount = 0;
|
||||
protected String mFilter = "";
|
||||
private String currentQuery = "";
|
||||
private String finalQuery = "";
|
||||
protected Object mGroup = null;
|
||||
protected DBHelper.LoyaltyCardOrder mOrder = DBHelper.LoyaltyCardOrder.Alpha;
|
||||
protected DBHelper.LoyaltyCardOrderDirection mOrderDirection = DBHelper.LoyaltyCardOrderDirection.Ascending;
|
||||
protected int selectedTab = 0;
|
||||
private RecyclerView mCardList;
|
||||
private View mHelpSection;
|
||||
private View mNoMatchingCardsText;
|
||||
private View mNoGroupCardsText;
|
||||
private TabLayout groupsTabLayout;
|
||||
private Runnable mUpdateLoyaltyCardListRunnable;
|
||||
private ActivityResultLauncher<Intent> mBarcodeScannerLauncher;
|
||||
private ActivityResultLauncher<Intent> mSettingsLauncher;
|
||||
|
||||
private ActionMode.Callback mCurrentActionModeCallback = new ActionMode.Callback() {
|
||||
@Override
|
||||
public boolean onCreateActionMode(ActionMode inputMode, Menu inputMenu) {
|
||||
inputMode.getMenuInflater().inflate(R.menu.card_longclick_menu, inputMenu);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPrepareActionMode(ActionMode inputMode, Menu inputMenu) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onActionItemClicked(ActionMode inputMode, MenuItem inputItem) {
|
||||
if (inputItem.getItemId() == R.id.action_share) {
|
||||
final ImportURIHelper importURIHelper = new ImportURIHelper(MainActivity.this);
|
||||
try {
|
||||
importURIHelper.startShareIntent(mAdapter.getSelectedItems());
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
Toast.makeText(MainActivity.this, R.string.failedGeneratingShareURL, Toast.LENGTH_LONG).show();
|
||||
e.printStackTrace();
|
||||
}
|
||||
inputMode.finish();
|
||||
return true;
|
||||
} else if (inputItem.getItemId() == R.id.action_edit) {
|
||||
if (mAdapter.getSelectedItemCount() != 1) {
|
||||
throw new IllegalArgumentException("Cannot edit more than 1 card at a time");
|
||||
}
|
||||
|
||||
Intent intent = new Intent(getApplicationContext(), LoyaltyCardEditActivity.class);
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putInt(LoyaltyCardEditActivity.BUNDLE_ID, mAdapter.getSelectedItems().get(0).id);
|
||||
bundle.putBoolean(LoyaltyCardEditActivity.BUNDLE_UPDATE, true);
|
||||
intent.putExtras(bundle);
|
||||
startActivity(intent);
|
||||
inputMode.finish();
|
||||
return true;
|
||||
} else if (inputItem.getItemId() == R.id.action_delete) {
|
||||
AlertDialog.Builder builder = new MaterialAlertDialogBuilder(MainActivity.this);
|
||||
// The following may seem weird, but it is necessary to give translators enough flexibility.
|
||||
// For example, in Russian, Android's plural quantity "one" actually refers to "any number ending on 1 but not ending in 11".
|
||||
// So while in English the extra non-plural form seems unnecessary duplication, it is necessary to give translators enough flexibility.
|
||||
// In here, we use the plain string when meaning exactly 1, and otherwise use the plural forms
|
||||
if (mAdapter.getSelectedItemCount() == 1) {
|
||||
builder.setTitle(R.string.deleteTitle);
|
||||
builder.setMessage(R.string.deleteConfirmation);
|
||||
} else {
|
||||
builder.setTitle(getResources().getQuantityString(R.plurals.deleteCardsTitle, mAdapter.getSelectedItemCount(), mAdapter.getSelectedItemCount()));
|
||||
builder.setMessage(getResources().getQuantityString(R.plurals.deleteCardsConfirmation, mAdapter.getSelectedItemCount(), mAdapter.getSelectedItemCount()));
|
||||
}
|
||||
|
||||
builder.setPositiveButton(R.string.confirm, (dialog, which) -> {
|
||||
for (LoyaltyCard loyaltyCard : mAdapter.getSelectedItems()) {
|
||||
Log.d(TAG, "Deleting card: " + loyaltyCard.id);
|
||||
|
||||
DBHelper.deleteLoyaltyCard(mDatabase, MainActivity.this, loyaltyCard.id);
|
||||
|
||||
ShortcutHelper.removeShortcut(MainActivity.this, loyaltyCard.id);
|
||||
}
|
||||
|
||||
TabLayout.Tab tab = groupsTabLayout.getTabAt(selectedTab);
|
||||
mGroup = tab != null ? tab.getTag() : null;
|
||||
|
||||
updateLoyaltyCardList(true);
|
||||
|
||||
dialog.dismiss();
|
||||
});
|
||||
builder.setNegativeButton(R.string.cancel, (dialog, which) -> dialog.dismiss());
|
||||
AlertDialog dialog = builder.create();
|
||||
dialog.show();
|
||||
|
||||
return true;
|
||||
} else if (inputItem.getItemId() == R.id.action_archive) {
|
||||
for (LoyaltyCard loyaltyCard : mAdapter.getSelectedItems()) {
|
||||
Log.d(TAG, "Archiving card: " + loyaltyCard.id);
|
||||
DBHelper.updateLoyaltyCardArchiveStatus(mDatabase, loyaltyCard.id, 1);
|
||||
ShortcutHelper.removeShortcut(MainActivity.this, loyaltyCard.id);
|
||||
updateLoyaltyCardList(false);
|
||||
inputMode.finish();
|
||||
invalidateOptionsMenu();
|
||||
}
|
||||
return true;
|
||||
} else if (inputItem.getItemId() == R.id.action_unarchive) {
|
||||
for (LoyaltyCard loyaltyCard : mAdapter.getSelectedItems()) {
|
||||
Log.d(TAG, "Unarchiving card: " + loyaltyCard.id);
|
||||
DBHelper.updateLoyaltyCardArchiveStatus(mDatabase, loyaltyCard.id, 0);
|
||||
updateLoyaltyCardList(false);
|
||||
inputMode.finish();
|
||||
invalidateOptionsMenu();
|
||||
}
|
||||
return true;
|
||||
} else if (inputItem.getItemId() == R.id.action_star) {
|
||||
for (LoyaltyCard loyaltyCard : mAdapter.getSelectedItems()) {
|
||||
Log.d(TAG, "Starring card: " + loyaltyCard.id);
|
||||
DBHelper.updateLoyaltyCardStarStatus(mDatabase, loyaltyCard.id, 1);
|
||||
updateLoyaltyCardList(false);
|
||||
inputMode.finish();
|
||||
}
|
||||
return true;
|
||||
} else if (inputItem.getItemId() == R.id.action_unstar) {
|
||||
for (LoyaltyCard loyaltyCard : mAdapter.getSelectedItems()) {
|
||||
Log.d(TAG, "Unstarring card: " + loyaltyCard.id);
|
||||
DBHelper.updateLoyaltyCardStarStatus(mDatabase, loyaltyCard.id, 0);
|
||||
updateLoyaltyCardList(false);
|
||||
inputMode.finish();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyActionMode(ActionMode inputMode) {
|
||||
mAdapter.clearSelections();
|
||||
mCurrentActionMode = null;
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle inputSavedInstanceState) {
|
||||
SplashScreen.installSplashScreen(this);
|
||||
super.onCreate(inputSavedInstanceState);
|
||||
|
||||
// Delete old cache files
|
||||
// These could be temporary images for the cropper, temporary images in LoyaltyCard toBundle/writeParcel/ etc.
|
||||
new Thread(() -> {
|
||||
long twentyFourHoursAgo = System.currentTimeMillis() - (1000 * 60 * 60 * 24);
|
||||
|
||||
File[] tempFiles = getCacheDir().listFiles();
|
||||
|
||||
if (tempFiles == null) {
|
||||
Log.e(TAG, "getCacheDir().listFiles() somehow returned null, this should never happen... Skipping cache cleanup...");
|
||||
return;
|
||||
}
|
||||
|
||||
for (File file : tempFiles) {
|
||||
if (file.lastModified() < twentyFourHoursAgo) {
|
||||
if (!file.delete()) {
|
||||
Log.w(TAG, "Failed to delete cache file " + file.getPath());
|
||||
}
|
||||
};
|
||||
}
|
||||
}).start();
|
||||
|
||||
// We should extract the share intent after we called the super.onCreate as it may need to spawn a dialog window and the app needs to be initialized to not crash
|
||||
extractIntentFields(getIntent());
|
||||
|
||||
binding = MainActivityBinding.inflate(getLayoutInflater());
|
||||
setContentView(binding.getRoot());
|
||||
Utils.applyWindowInsets(binding.getRoot());
|
||||
setSupportActionBar(binding.toolbar);
|
||||
groupsTabLayout = binding.groups;
|
||||
contentMainBinding = ContentMainBinding.bind(binding.include.getRoot());
|
||||
|
||||
mDatabase = new DBHelper(this).getWritableDatabase();
|
||||
|
||||
mUpdateLoyaltyCardListRunnable = () -> {
|
||||
updateLoyaltyCardList(false);
|
||||
};
|
||||
|
||||
groupsTabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
|
||||
@Override
|
||||
public void onTabSelected(TabLayout.Tab tab) {
|
||||
selectedTab = tab.getPosition();
|
||||
Log.d("onTabSelected", "Tab Position " + tab.getPosition());
|
||||
mGroup = tab.getTag();
|
||||
updateLoyaltyCardList(false);
|
||||
// Store active tab in Shared Preference to restore next app launch
|
||||
SharedPreferences activeTabPref = getApplicationContext().getSharedPreferences(
|
||||
getString(R.string.sharedpreference_active_tab),
|
||||
Context.MODE_PRIVATE);
|
||||
SharedPreferences.Editor activeTabPrefEditor = activeTabPref.edit();
|
||||
activeTabPrefEditor.putInt(getString(R.string.sharedpreference_active_tab), tab.getPosition());
|
||||
activeTabPrefEditor.apply();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTabUnselected(TabLayout.Tab tab) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTabReselected(TabLayout.Tab tab) {
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
mHelpSection = contentMainBinding.helpSection;
|
||||
mNoMatchingCardsText = contentMainBinding.noMatchingCardsText;
|
||||
mNoGroupCardsText = contentMainBinding.noGroupCardsText;
|
||||
mCardList = contentMainBinding.list;
|
||||
|
||||
mAdapter = new LoyaltyCardCursorAdapter(this, null, this, mUpdateLoyaltyCardListRunnable);
|
||||
mCardList.setAdapter(mAdapter);
|
||||
registerForContextMenu(mCardList);
|
||||
|
||||
mBarcodeScannerLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> {
|
||||
// Exit early if the user cancelled the scan (pressed back/home)
|
||||
if (result.getResultCode() != RESULT_OK) {
|
||||
return;
|
||||
}
|
||||
|
||||
Intent editIntent = new Intent(getApplicationContext(), LoyaltyCardEditActivity.class);
|
||||
editIntent.putExtras(result.getData().getExtras());
|
||||
startActivity(editIntent);
|
||||
});
|
||||
|
||||
mSettingsLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> {
|
||||
if (result.getResultCode() == Activity.RESULT_OK) {
|
||||
Intent intent = result.getData();
|
||||
if (intent != null && intent.getBooleanExtra(RESTART_ACTIVITY_INTENT, false)) {
|
||||
recreate();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
getOnBackPressedDispatcher().addCallback(this, new OnBackPressedCallback(true) {
|
||||
@Override
|
||||
public void handleOnBackPressed() {
|
||||
if (mSearchView != null && !mSearchView.isIconified()) {
|
||||
mSearchView.setIconified(true);
|
||||
} else {
|
||||
finish();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
|
||||
if (mCurrentActionMode != null) {
|
||||
mAdapter.clearSelections();
|
||||
mCurrentActionMode.finish();
|
||||
}
|
||||
|
||||
if (mSearchView != null && !mSearchView.isIconified()) {
|
||||
mFilter = mSearchView.getQuery().toString();
|
||||
}
|
||||
// Start of active tab logic
|
||||
updateTabGroups(groupsTabLayout);
|
||||
|
||||
// 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
|
||||
mOrder = Utils.getLoyaltyCardOrder(this);
|
||||
mOrderDirection = Utils.getLoyaltyCardOrderDirection(this);
|
||||
|
||||
mGroup = null;
|
||||
|
||||
if (groupsTabLayout.getTabCount() != 0) {
|
||||
TabLayout.Tab tab = groupsTabLayout.getTabAt(selectedTab);
|
||||
if (tab == null) {
|
||||
tab = groupsTabLayout.getTabAt(0);
|
||||
}
|
||||
|
||||
groupsTabLayout.selectTab(tab);
|
||||
assert tab != null;
|
||||
mGroup = tab.getTag();
|
||||
} else {
|
||||
scaleScreen();
|
||||
}
|
||||
|
||||
updateLoyaltyCardList(true);
|
||||
// End of active tab logic
|
||||
|
||||
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();
|
||||
|
||||
var layoutManager = (GridLayoutManager) mCardList.getLayoutManager();
|
||||
if (layoutManager != null) {
|
||||
var settings = new Settings(this);
|
||||
layoutManager.setSpanCount(settings.getPreferredColumnCount());
|
||||
}
|
||||
}
|
||||
|
||||
private void displayCardSetupOptions(Menu menu, boolean shouldShow) {
|
||||
for (int id : new int[]{R.id.action_search, R.id.action_display_options, R.id.action_sort}) {
|
||||
menu.findItem(id).setVisible(shouldShow);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateLoyaltyCardCount() {
|
||||
mLoyaltyCardCount = DBHelper.getLoyaltyCardCount(mDatabase);
|
||||
}
|
||||
|
||||
private void updateLoyaltyCardList(boolean updateCount) {
|
||||
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));
|
||||
|
||||
if (updateCount) {
|
||||
updateLoyaltyCardCount();
|
||||
// Update menu icons if necessary
|
||||
invalidateOptionsMenu();
|
||||
}
|
||||
|
||||
if (mLoyaltyCardCount > 0) {
|
||||
// We want the cardList to be visible regardless of the filtered match count
|
||||
// to ensure that the noMatchingCardsText doesn't end up being shown below
|
||||
// the keyboard
|
||||
mHelpSection.setVisibility(View.GONE);
|
||||
mNoGroupCardsText.setVisibility(View.GONE);
|
||||
|
||||
if (mAdapter.getItemCount() > 0) {
|
||||
mCardList.setVisibility(View.VISIBLE);
|
||||
mNoMatchingCardsText.setVisibility(View.GONE);
|
||||
} else {
|
||||
mCardList.setVisibility(View.GONE);
|
||||
if (!mFilter.isEmpty()) {
|
||||
// Actual Empty Search Result
|
||||
mNoMatchingCardsText.setVisibility(View.VISIBLE);
|
||||
mNoGroupCardsText.setVisibility(View.GONE);
|
||||
} else {
|
||||
// Group Tab with no Group Cards
|
||||
mNoMatchingCardsText.setVisibility(View.GONE);
|
||||
mNoGroupCardsText.setVisibility(View.VISIBLE);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
mCardList.setVisibility(View.GONE);
|
||||
mHelpSection.setVisibility(View.VISIBLE);
|
||||
|
||||
mNoMatchingCardsText.setVisibility(View.GONE);
|
||||
mNoGroupCardsText.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
if (mCurrentActionMode != null) {
|
||||
mCurrentActionMode.finish();
|
||||
}
|
||||
|
||||
new ListWidget().updateAll(mAdapter.mContext);
|
||||
}
|
||||
|
||||
private void processParseResultList(List<ParseResult> parseResultList, String group, boolean closeAppOnNoBarcode) {
|
||||
if (parseResultList.isEmpty()) {
|
||||
throw new IllegalArgumentException("parseResultList may not be empty");
|
||||
}
|
||||
|
||||
Utils.makeUserChooseParseResultFromList(MainActivity.this, parseResultList, new ParseResultListDisambiguatorCallback() {
|
||||
@Override
|
||||
public void onUserChoseParseResult(ParseResult parseResult) {
|
||||
Intent intent = new Intent(getApplicationContext(), LoyaltyCardEditActivity.class);
|
||||
Bundle bundle = parseResult.toLoyaltyCardBundle(MainActivity.this);
|
||||
if (group != null) {
|
||||
bundle.putString(LoyaltyCardEditActivity.BUNDLE_ADDGROUP, group);
|
||||
}
|
||||
intent.putExtras(bundle);
|
||||
startActivity(intent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUserDismissedSelector() {
|
||||
if (closeAppOnNoBarcode) {
|
||||
finish();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void onSharedIntent(Intent intent) {
|
||||
String receivedAction = intent.getAction();
|
||||
String receivedType = intent.getType();
|
||||
|
||||
if (receivedAction == null || receivedType == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
List<ParseResult> parseResultList;
|
||||
|
||||
// Check for shared text
|
||||
if (receivedAction.equals(Intent.ACTION_SEND) && receivedType.equals("text/plain")) {
|
||||
LoyaltyCard loyaltyCard = new LoyaltyCard();
|
||||
loyaltyCard.setCardId(intent.getStringExtra(Intent.EXTRA_TEXT));
|
||||
parseResultList = Collections.singletonList(new ParseResult(ParseResultType.BARCODE_ONLY, loyaltyCard));
|
||||
} else {
|
||||
// Parse whatever file was sent, regardless of opening or sharing
|
||||
Uri data;
|
||||
if (receivedAction.equals(Intent.ACTION_VIEW)) {
|
||||
data = intent.getData();
|
||||
} else if (receivedAction.equals(Intent.ACTION_SEND)) {
|
||||
data = intent.getParcelableExtra(Intent.EXTRA_STREAM);
|
||||
} else {
|
||||
Log.e(TAG, "Wrong action type to parse intent");
|
||||
return;
|
||||
}
|
||||
|
||||
if (receivedType.startsWith("image/")) {
|
||||
parseResultList = Utils.retrieveBarcodesFromImage(this, data);
|
||||
} else if (receivedType.equals("application/pdf")) {
|
||||
parseResultList = Utils.retrieveBarcodesFromPdf(this, data);
|
||||
} else if (Arrays.asList("application/vnd.apple.pkpass", "application/vnd-com.apple.pkpass").contains(receivedType)) {
|
||||
parseResultList = Utils.retrieveBarcodesFromPkPass(this, data);
|
||||
} else if (receivedType.equals("application/vnd.espass-espass")) {
|
||||
// FIXME: espass is not pkpass
|
||||
// However, several users stated in https://github.com/CatimaLoyalty/Android/issues/2197 that the formats are extremely similar to the point they could rename an .espass file to .pkpass and have it imported
|
||||
// So it makes sense to "unofficially" treat it as a PKPASS for now, even though not completely correct
|
||||
parseResultList = Utils.retrieveBarcodesFromPkPass(this, data);
|
||||
} else if (receivedType.equals("application/vnd.apple.pkpasses")) {
|
||||
parseResultList = Utils.retrieveBarcodesFromPkPasses(this, data);
|
||||
} else {
|
||||
Log.e(TAG, "Wrong mime-type");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Give up if we should parse but there is nothing to parse
|
||||
if (parseResultList == null || parseResultList.isEmpty()) {
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
processParseResultList(parseResultList, null, true);
|
||||
}
|
||||
|
||||
private void extractIntentFields(Intent intent) {
|
||||
onSharedIntent(intent);
|
||||
}
|
||||
|
||||
public void updateTabGroups(TabLayout groupsTabLayout) {
|
||||
List<Group> newGroups = DBHelper.getGroups(mDatabase);
|
||||
|
||||
if (newGroups.size() == 0) {
|
||||
groupsTabLayout.removeAllTabs();
|
||||
groupsTabLayout.setVisibility(View.GONE);
|
||||
return;
|
||||
}
|
||||
|
||||
groupsTabLayout.removeAllTabs();
|
||||
|
||||
TabLayout.Tab allTab = groupsTabLayout.newTab();
|
||||
allTab.setText(R.string.all);
|
||||
allTab.setTag(null);
|
||||
groupsTabLayout.addTab(allTab, false);
|
||||
|
||||
for (Group group : newGroups) {
|
||||
TabLayout.Tab tab = groupsTabLayout.newTab();
|
||||
tab.setText(group._id);
|
||||
tab.setTag(group);
|
||||
groupsTabLayout.addTab(tab, false);
|
||||
}
|
||||
|
||||
groupsTabLayout.setVisibility(View.VISIBLE);
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
// Saving currentQuery to finalQuery for user, this will be used to restore search history, happens when user clicks a card from list
|
||||
protected void onSaveInstanceState(@NonNull Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
finalQuery = currentQuery;
|
||||
// Putting the query also into outState for later use in onRestoreInstanceState when rotating screen
|
||||
if (mSearchView != null) {
|
||||
outState.putString(STATE_SEARCH_QUERY, finalQuery);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
// Restoring instance state when rotation of screen happens with the goal to restore search query for user
|
||||
protected void onRestoreInstanceState(@NonNull Bundle savedInstanceState) {
|
||||
super.onRestoreInstanceState(savedInstanceState);
|
||||
finalQuery = savedInstanceState.getString(STATE_SEARCH_QUERY, "");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu inputMenu) {
|
||||
getMenuInflater().inflate(R.menu.main_menu, inputMenu);
|
||||
|
||||
displayCardSetupOptions(inputMenu, mLoyaltyCardCount > 0);
|
||||
|
||||
SearchManager searchManager = (SearchManager) getSystemService(Context.SEARCH_SERVICE);
|
||||
if (searchManager != null) {
|
||||
MenuItem searchMenuItem = inputMenu.findItem(R.id.action_search);
|
||||
mSearchView = (SearchView) searchMenuItem.getActionView();
|
||||
mSearchView.setSearchableInfo(searchManager.getSearchableInfo(getComponentName()));
|
||||
mSearchView.setSubmitButtonEnabled(false);
|
||||
mSearchView.setOnCloseListener(() -> {
|
||||
invalidateOptionsMenu();
|
||||
return false;
|
||||
});
|
||||
|
||||
/*
|
||||
* On Android 13 and later, pressing Back while the search view is open hides the keyboard
|
||||
* and collapses the search view at the same time.
|
||||
* This brings back the old behavior on Android 12 and lower: pressing Back once
|
||||
* hides the keyboard, press again while keyboard is hidden to collapse the search view.
|
||||
*/
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
searchMenuItem.setOnActionExpandListener(new MenuItem.OnActionExpandListener() {
|
||||
@Override
|
||||
public boolean onMenuItemActionExpand(@NonNull MenuItem item) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onMenuItemActionCollapse(@NonNull MenuItem item) {
|
||||
if (mSearchView.hasFocus()) {
|
||||
mSearchView.clearFocus();
|
||||
return false;
|
||||
}
|
||||
currentQuery = "";
|
||||
mFilter = "";
|
||||
updateLoyaltyCardList(false);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
mSearchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
|
||||
@Override
|
||||
public boolean onQueryTextSubmit(String query) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onQueryTextChange(String newText) {
|
||||
mFilter = newText;
|
||||
// New logic to ensure search history after coming back from picked card - user will see the last search query
|
||||
if (newText.isEmpty()) {
|
||||
if(!finalQuery.isEmpty()){
|
||||
// Setting the query text for user after coming back from picked card from finalQuery
|
||||
mSearchView.setQuery(finalQuery, false);
|
||||
}
|
||||
else if(!currentQuery.isEmpty()){
|
||||
// Else if is needed in case user deletes search - expected behaviour is to show all cards
|
||||
currentQuery = "";
|
||||
mSearchView.setQuery(currentQuery, false);
|
||||
}
|
||||
} else {
|
||||
// Setting search query each time user changes the text in search to temporary variable to be used later in finalQuery String which will be used to restore search history
|
||||
currentQuery = newText;
|
||||
}
|
||||
TabLayout.Tab currentTab = groupsTabLayout.getTabAt(groupsTabLayout.getSelectedTabPosition());
|
||||
mGroup = currentTab != null ? currentTab.getTag() : null;
|
||||
|
||||
updateLoyaltyCardList(false);
|
||||
|
||||
return true;
|
||||
}
|
||||
});
|
||||
// Check if we came from a picked card back to search, in that case we want to show the search view with previous search query
|
||||
if(!finalQuery.isEmpty()){
|
||||
// Expand the search view to show the query
|
||||
searchMenuItem.expandActionView();
|
||||
// Setting the query text to empty String due to behaviour of onQueryTextChange after coming back from picked card - onQueryTextChange is called automatically without users interaction
|
||||
finalQuery = "";
|
||||
mSearchView.setQuery(currentQuery, false);
|
||||
}
|
||||
}
|
||||
|
||||
return super.onCreateOptionsMenu(inputMenu);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem inputItem) {
|
||||
int id = inputItem.getItemId();
|
||||
|
||||
if (id == android.R.id.home) {
|
||||
getOnBackPressedDispatcher().onBackPressed();
|
||||
}
|
||||
|
||||
if (id == R.id.action_display_options) {
|
||||
mAdapter.showDisplayOptionsDialog();
|
||||
invalidateOptionsMenu();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if (id == R.id.action_sort) {
|
||||
AtomicInteger currentIndex = new AtomicInteger();
|
||||
List<DBHelper.LoyaltyCardOrder> loyaltyCardOrders = Arrays.asList(DBHelper.LoyaltyCardOrder.values());
|
||||
for (int i = 0; i < loyaltyCardOrders.size(); i++) {
|
||||
if (mOrder == loyaltyCardOrders.get(i)) {
|
||||
currentIndex.set(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
AlertDialog.Builder builder = new MaterialAlertDialogBuilder(MainActivity.this);
|
||||
builder.setTitle(R.string.sort_by);
|
||||
|
||||
SortingOptionBinding sortingOptionBinding = SortingOptionBinding
|
||||
.inflate(LayoutInflater.from(MainActivity.this), null, false);
|
||||
final View customLayout = sortingOptionBinding.getRoot();
|
||||
builder.setView(customLayout);
|
||||
|
||||
CheckBox showReversed = sortingOptionBinding.checkBoxReverse;
|
||||
|
||||
|
||||
showReversed.setChecked(mOrderDirection == DBHelper.LoyaltyCardOrderDirection.Descending);
|
||||
|
||||
|
||||
builder.setSingleChoiceItems(R.array.sort_types_array, currentIndex.get(), (dialog, which) -> currentIndex.set(which));
|
||||
|
||||
builder.setPositiveButton(R.string.sort, (dialog, which) -> {
|
||||
|
||||
setSort(
|
||||
loyaltyCardOrders.get(currentIndex.get()),
|
||||
showReversed.isChecked() ? DBHelper.LoyaltyCardOrderDirection.Descending : DBHelper.LoyaltyCardOrderDirection.Ascending
|
||||
);
|
||||
|
||||
new ListWidget().updateAll(this);
|
||||
|
||||
dialog.dismiss();
|
||||
});
|
||||
|
||||
builder.setNegativeButton(R.string.cancel, (dialog, which) -> dialog.dismiss());
|
||||
|
||||
AlertDialog dialog = builder.create();
|
||||
dialog.show();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if (id == R.id.action_manage_groups) {
|
||||
Intent i = new Intent(getApplicationContext(), ManageGroupsActivity.class);
|
||||
startActivity(i);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (id == R.id.action_import_export) {
|
||||
Intent i = new Intent(getApplicationContext(), ImportExportActivity.class);
|
||||
startActivity(i);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (id == R.id.action_settings) {
|
||||
Intent i = new Intent(getApplicationContext(), SettingsActivity.class);
|
||||
mSettingsLauncher.launch(i);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (id == R.id.action_about) {
|
||||
Intent i = new Intent(getApplicationContext(), AboutActivity.class);
|
||||
startActivity(i);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
return super.onOptionsItemSelected(inputItem);
|
||||
}
|
||||
|
||||
private void setSort(DBHelper.LoyaltyCardOrder order, DBHelper.LoyaltyCardOrderDirection direction) {
|
||||
// Update values
|
||||
mOrder = order;
|
||||
mOrderDirection = direction;
|
||||
|
||||
// Store in Shared Preference to restore next app launch
|
||||
SharedPreferences sortPref = getApplicationContext().getSharedPreferences(
|
||||
getString(R.string.sharedpreference_sort),
|
||||
Context.MODE_PRIVATE);
|
||||
SharedPreferences.Editor sortPrefEditor = sortPref.edit();
|
||||
sortPrefEditor.putString(getString(R.string.sharedpreference_sort_order), order.name());
|
||||
sortPrefEditor.putString(getString(R.string.sharedpreference_sort_direction), direction.name());
|
||||
sortPrefEditor.apply();
|
||||
|
||||
// Update card list
|
||||
updateLoyaltyCardList(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRowLongClicked(int inputPosition) {
|
||||
enableActionMode(inputPosition);
|
||||
}
|
||||
|
||||
private void enableActionMode(int inputPosition) {
|
||||
if (mCurrentActionMode == null) {
|
||||
mCurrentActionMode = startSupportActionMode(mCurrentActionModeCallback);
|
||||
}
|
||||
toggleSelection(inputPosition);
|
||||
}
|
||||
|
||||
private void scaleScreen() {
|
||||
DisplayMetrics displayMetrics = new DisplayMetrics();
|
||||
getWindowManager().getDefaultDisplay().getMetrics(displayMetrics);
|
||||
int screenHeight = displayMetrics.heightPixels;
|
||||
float mediumSizePx = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,MEDIUM_SCALE_FACTOR_DIP,getResources().getDisplayMetrics());
|
||||
boolean shouldScaleSmaller = screenHeight < mediumSizePx;
|
||||
|
||||
binding.include.welcomeIcon.setVisibility(shouldScaleSmaller ? View.GONE : View.VISIBLE);
|
||||
}
|
||||
|
||||
private void toggleSelection(int inputPosition) {
|
||||
mAdapter.toggleSelection(inputPosition);
|
||||
int count = mAdapter.getSelectedItemCount();
|
||||
|
||||
if (count == 0) {
|
||||
mCurrentActionMode.finish();
|
||||
} else {
|
||||
mCurrentActionMode.setTitle(getResources().getQuantityString(R.plurals.selectedCardCount, count, count));
|
||||
|
||||
MenuItem editItem = mCurrentActionMode.getMenu().findItem(R.id.action_edit);
|
||||
MenuItem archiveItem = mCurrentActionMode.getMenu().findItem(R.id.action_archive);
|
||||
MenuItem unarchiveItem = mCurrentActionMode.getMenu().findItem(R.id.action_unarchive);
|
||||
MenuItem starItem = mCurrentActionMode.getMenu().findItem(R.id.action_star);
|
||||
MenuItem unstarItem = mCurrentActionMode.getMenu().findItem(R.id.action_unstar);
|
||||
|
||||
boolean hasStarred = false;
|
||||
boolean hasUnstarred = false;
|
||||
boolean hasArchived = false;
|
||||
boolean hasUnarchived = false;
|
||||
|
||||
for (LoyaltyCard loyaltyCard : mAdapter.getSelectedItems()) {
|
||||
if (loyaltyCard.starStatus == 1) {
|
||||
hasStarred = true;
|
||||
} else {
|
||||
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);
|
||||
editItem.setVisible(true);
|
||||
editItem.setEnabled(true);
|
||||
} else {
|
||||
starItem.setVisible(hasUnstarred);
|
||||
unstarItem.setVisible(hasStarred);
|
||||
|
||||
editItem.setVisible(false);
|
||||
editItem.setEnabled(false);
|
||||
}
|
||||
|
||||
mCurrentActionMode.invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void onRowClicked(int inputPosition) {
|
||||
if (mAdapter.getSelectedItemCount() > 0) {
|
||||
enableActionMode(inputPosition);
|
||||
} else {
|
||||
// FIXME
|
||||
//
|
||||
// There is a really nasty edge case that can happen when someone taps a card but right
|
||||
// after it swipes (very small window, hard to reproduce). The cursor gets replaced and
|
||||
// may not have a card at the ID number that is returned from onRowClicked.
|
||||
//
|
||||
// The proper fix, obviously, would involve makes sure an onFling can't happen while a
|
||||
// click is being processed. Sadly, I have not yet found a way to make that possible.
|
||||
LoyaltyCard loyaltyCard;
|
||||
try {
|
||||
loyaltyCard = mAdapter.getCard(inputPosition);
|
||||
} catch (CursorIndexOutOfBoundsException e) {
|
||||
Log.w(TAG, "Prevented crash from tap + swipe on ID " + inputPosition + ": " + e);
|
||||
return;
|
||||
}
|
||||
|
||||
Intent intent = new Intent(this, LoyaltyCardViewActivity.class);
|
||||
intent.setAction("");
|
||||
final Bundle b = new Bundle();
|
||||
b.putInt(LoyaltyCardViewActivity.BUNDLE_ID, loyaltyCard.id);
|
||||
|
||||
ArrayList<Integer> cardList = new ArrayList<>();
|
||||
for (int i = 0; i < mAdapter.getItemCount(); i++) {
|
||||
cardList.add(mAdapter.getCard(i).id);
|
||||
}
|
||||
|
||||
b.putIntegerArrayList(LoyaltyCardViewActivity.BUNDLE_CARDLIST, cardList);
|
||||
intent.putExtras(b);
|
||||
|
||||
startActivity(intent);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,945 @@
|
||||
package protect.card_locker
|
||||
|
||||
import android.app.SearchManager
|
||||
import android.content.DialogInterface
|
||||
import android.content.Intent
|
||||
import android.database.CursorIndexOutOfBoundsException
|
||||
import android.database.sqlite.SQLiteDatabase
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.util.DisplayMetrics
|
||||
import android.util.Log
|
||||
import android.util.TypedValue
|
||||
import android.view.LayoutInflater
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.widget.Toast
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.activity.result.ActivityResult
|
||||
import androidx.activity.result.ActivityResultCallback
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult
|
||||
import androidx.appcompat.view.ActionMode
|
||||
import androidx.appcompat.widget.SearchView
|
||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.tabs.TabLayout
|
||||
import com.google.android.material.tabs.TabLayout.OnTabSelectedListener
|
||||
import protect.card_locker.DBHelper.LoyaltyCardOrder
|
||||
import protect.card_locker.DBHelper.LoyaltyCardOrderDirection
|
||||
import protect.card_locker.LoyaltyCardCursorAdapter.CardAdapterListener
|
||||
import protect.card_locker.cardview.LoyaltyCardViewActivity
|
||||
import protect.card_locker.databinding.ContentMainBinding
|
||||
import protect.card_locker.databinding.MainActivityBinding
|
||||
import protect.card_locker.databinding.SortingOptionBinding
|
||||
import protect.card_locker.preferences.Settings
|
||||
import protect.card_locker.preferences.SettingsActivity
|
||||
import java.io.UnsupportedEncodingException
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
import androidx.core.content.edit
|
||||
|
||||
class MainActivity : CatimaAppCompatActivity(), CardAdapterListener {
|
||||
private lateinit var binding: MainActivityBinding
|
||||
private lateinit var contentMainBinding: ContentMainBinding
|
||||
private lateinit var mDatabase: SQLiteDatabase
|
||||
private lateinit var mAdapter: LoyaltyCardCursorAdapter
|
||||
private var mCurrentActionMode: ActionMode? = null
|
||||
private var mSearchView: SearchView? = null
|
||||
private var mLoyaltyCardCount = 0
|
||||
@JvmField
|
||||
var mFilter: String = ""
|
||||
private var currentQuery = ""
|
||||
private var finalQuery = ""
|
||||
private var mGroup: Any? = null
|
||||
private var mOrder: LoyaltyCardOrder = LoyaltyCardOrder.Alpha
|
||||
private var mOrderDirection: LoyaltyCardOrderDirection = LoyaltyCardOrderDirection.Ascending
|
||||
private var selectedTab: Int = 0
|
||||
private lateinit var groupsTabLayout: TabLayout
|
||||
private lateinit var mUpdateLoyaltyCardListRunnable: Runnable
|
||||
private lateinit var mBarcodeScannerLauncher: ActivityResultLauncher<Intent>
|
||||
private lateinit var mSettingsLauncher: ActivityResultLauncher<Intent>
|
||||
|
||||
private val mCurrentActionModeCallback: ActionMode.Callback = object : ActionMode.Callback {
|
||||
override fun onCreateActionMode(inputMode: ActionMode, inputMenu: Menu?): Boolean {
|
||||
inputMode.menuInflater.inflate(R.menu.card_longclick_menu, inputMenu)
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onPrepareActionMode(inputMode: ActionMode?, inputMenu: Menu?): Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onActionItemClicked(inputMode: ActionMode, inputItem: MenuItem): Boolean {
|
||||
when (inputItem.itemId) {
|
||||
R.id.action_share -> {
|
||||
try {
|
||||
ImportURIHelper(this@MainActivity).startShareIntent(mAdapter.getSelectedItems())
|
||||
} catch (e: UnsupportedEncodingException) {
|
||||
Toast.makeText(
|
||||
this@MainActivity,
|
||||
R.string.failedGeneratingShareURL,
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
e.printStackTrace()
|
||||
}
|
||||
inputMode.finish()
|
||||
return true
|
||||
}
|
||||
R.id.action_edit -> {
|
||||
require(mAdapter.selectedItemCount == 1) { "Cannot edit more than 1 card at a time" }
|
||||
|
||||
startActivity(
|
||||
Intent(applicationContext, LoyaltyCardEditActivity::class.java).apply {
|
||||
putExtras(Bundle().apply {
|
||||
putInt(
|
||||
LoyaltyCardEditActivity.BUNDLE_ID,
|
||||
mAdapter.getSelectedItems()[0].id
|
||||
)
|
||||
putBoolean(LoyaltyCardEditActivity.BUNDLE_UPDATE, true)
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
inputMode.finish()
|
||||
return true
|
||||
}
|
||||
R.id.action_duplicate -> {
|
||||
require(mAdapter.selectedItemCount == 1) { "Cannot duplicate more than 1 card at a time" }
|
||||
|
||||
startActivity(
|
||||
Intent(applicationContext, LoyaltyCardEditActivity::class.java).apply {
|
||||
putExtras(Bundle().apply {
|
||||
putInt(
|
||||
LoyaltyCardEditActivity.BUNDLE_ID,
|
||||
mAdapter.getSelectedItems()[0].id
|
||||
)
|
||||
putBoolean(LoyaltyCardEditActivity.BUNDLE_DUPLICATE_ID, true)
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
inputMode.finish()
|
||||
return true
|
||||
}
|
||||
R.id.action_delete -> {
|
||||
MaterialAlertDialogBuilder(this@MainActivity).apply {
|
||||
// The following may seem weird, but it is necessary to give translators enough flexibility.
|
||||
// For example, in Russian, Android's plural quantity "one" actually refers to "any number ending on 1 but not ending in 11".
|
||||
// So while in English the extra non-plural form seems unnecessary duplication, it is necessary to give translators enough flexibility.
|
||||
// In here, we use the plain string when meaning exactly 1, and otherwise use the plural forms
|
||||
if (mAdapter.selectedItemCount == 1) {
|
||||
setTitle(R.string.deleteTitle)
|
||||
setMessage(R.string.deleteConfirmation)
|
||||
} else {
|
||||
setTitle(
|
||||
getResources().getQuantityString(
|
||||
R.plurals.deleteCardsTitle,
|
||||
mAdapter.selectedItemCount,
|
||||
mAdapter.selectedItemCount
|
||||
)
|
||||
)
|
||||
setMessage(
|
||||
getResources().getQuantityString(
|
||||
R.plurals.deleteCardsConfirmation,
|
||||
mAdapter.selectedItemCount,
|
||||
mAdapter.selectedItemCount
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
setPositiveButton(
|
||||
R.string.confirm
|
||||
) { dialog, _ ->
|
||||
for (loyaltyCard in mAdapter.getSelectedItems()) {
|
||||
Log.d(TAG, "Deleting card: " + loyaltyCard.id)
|
||||
|
||||
DBHelper.deleteLoyaltyCard(mDatabase, this@MainActivity, loyaltyCard.id)
|
||||
}
|
||||
val tab = groupsTabLayout.getTabAt(selectedTab)
|
||||
mGroup = tab?.tag
|
||||
|
||||
updateLoyaltyCardList(true)
|
||||
dialog.dismiss()
|
||||
}
|
||||
|
||||
setNegativeButton(R.string.cancel) { dialog, _ ->
|
||||
dialog.dismiss()
|
||||
}
|
||||
}.create().show()
|
||||
|
||||
return true
|
||||
}
|
||||
R.id.action_archive -> {
|
||||
for (loyaltyCard in mAdapter.getSelectedItems()) {
|
||||
Log.d(TAG, "Archiving card: " + loyaltyCard.id)
|
||||
DBHelper.updateLoyaltyCardArchiveStatus(mDatabase, loyaltyCard.id, 1)
|
||||
updateLoyaltyCardList(false)
|
||||
inputMode.finish()
|
||||
invalidateOptionsMenu()
|
||||
}
|
||||
return true
|
||||
}
|
||||
R.id.action_unarchive -> {
|
||||
for (loyaltyCard in mAdapter.getSelectedItems()) {
|
||||
Log.d(TAG, "Unarchiving card: " + loyaltyCard.id)
|
||||
DBHelper.updateLoyaltyCardArchiveStatus(mDatabase, loyaltyCard.id, 0)
|
||||
updateLoyaltyCardList(false)
|
||||
inputMode.finish()
|
||||
invalidateOptionsMenu()
|
||||
}
|
||||
return true
|
||||
}
|
||||
R.id.action_star -> {
|
||||
for (loyaltyCard in mAdapter.getSelectedItems()) {
|
||||
Log.d(TAG, "Starring card: " + loyaltyCard.id)
|
||||
DBHelper.updateLoyaltyCardStarStatus(mDatabase, loyaltyCard.id, 1)
|
||||
updateLoyaltyCardList(false)
|
||||
inputMode.finish()
|
||||
}
|
||||
return true
|
||||
}
|
||||
R.id.action_unstar -> {
|
||||
for (loyaltyCard in mAdapter.getSelectedItems()) {
|
||||
Log.d(TAG, "Unstarring card: " + loyaltyCard.id)
|
||||
DBHelper.updateLoyaltyCardStarStatus(mDatabase, loyaltyCard.id, 0)
|
||||
updateLoyaltyCardList(false)
|
||||
inputMode.finish()
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onDestroyActionMode(inputMode: ActionMode?) {
|
||||
mAdapter.clearSelections()
|
||||
mCurrentActionMode = null
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate(inputSavedInstanceState: Bundle?) {
|
||||
installSplashScreen()
|
||||
super.onCreate(inputSavedInstanceState)
|
||||
|
||||
// Delete old cache files
|
||||
// These could be temporary images for the cropper, temporary images in LoyaltyCard toBundle/writeParcel/ etc.
|
||||
Thread {
|
||||
val twentyFourHoursAgo = System.currentTimeMillis() - (1000 * 60 * 60 * 24)
|
||||
val tempFiles = cacheDir.listFiles()
|
||||
|
||||
if (tempFiles == null) {
|
||||
Log.e(
|
||||
TAG,
|
||||
"getCacheDir().listFiles() somehow returned null, this should never happen... Skipping cache cleanup..."
|
||||
)
|
||||
return@Thread
|
||||
}
|
||||
for (file in tempFiles) {
|
||||
if (file.lastModified() < twentyFourHoursAgo) {
|
||||
if (!file.delete()) {
|
||||
Log.w(TAG, "Failed to delete cache file " + file.path)
|
||||
}
|
||||
}
|
||||
}
|
||||
}.start()
|
||||
|
||||
// We should extract the share intent after we called the super.onCreate as it may need to spawn a dialog window and the app needs to be initialized to not crash
|
||||
extractIntentFields(intent)
|
||||
|
||||
binding = MainActivityBinding.inflate(layoutInflater)
|
||||
setContentView(binding.getRoot())
|
||||
Utils.applyWindowInsets(binding.getRoot())
|
||||
setSupportActionBar(binding.toolbar)
|
||||
groupsTabLayout = binding.groups
|
||||
contentMainBinding = ContentMainBinding.bind(binding.include.getRoot())
|
||||
|
||||
mDatabase = DBHelper(this).writableDatabase
|
||||
|
||||
mUpdateLoyaltyCardListRunnable = Runnable {
|
||||
updateLoyaltyCardList(false)
|
||||
}
|
||||
|
||||
groupsTabLayout.addOnTabSelectedListener(object : OnTabSelectedListener {
|
||||
override fun onTabSelected(tab: TabLayout.Tab) {
|
||||
selectedTab = tab.position
|
||||
Log.d("onTabSelected", "Tab Position " + tab.position)
|
||||
mGroup = tab.tag
|
||||
updateLoyaltyCardList(false)
|
||||
// Store active tab in Shared Preference to restore next app launch
|
||||
applicationContext.getSharedPreferences(
|
||||
getString(R.string.sharedpreference_active_tab),
|
||||
MODE_PRIVATE
|
||||
).edit {
|
||||
putInt(
|
||||
getString(R.string.sharedpreference_active_tab),
|
||||
tab.position
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onTabUnselected(tab: TabLayout.Tab?) {
|
||||
}
|
||||
|
||||
override fun onTabReselected(tab: TabLayout.Tab?) {
|
||||
}
|
||||
})
|
||||
|
||||
mAdapter = LoyaltyCardCursorAdapter(this, null, this, mUpdateLoyaltyCardListRunnable)
|
||||
contentMainBinding.list.setAdapter(mAdapter)
|
||||
registerForContextMenu(contentMainBinding.list)
|
||||
|
||||
mBarcodeScannerLauncher = registerForActivityResult(
|
||||
StartActivityForResult(),
|
||||
ActivityResultCallback registerForActivityResult@{ result: ActivityResult? ->
|
||||
// Exit early if the user cancelled the scan (pressed back/home)
|
||||
if (result == null || result.resultCode != RESULT_OK) {
|
||||
return@registerForActivityResult
|
||||
}
|
||||
|
||||
startActivity(
|
||||
Intent(applicationContext, LoyaltyCardEditActivity::class.java).apply {
|
||||
putExtras(result.data!!.extras!!)
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
mSettingsLauncher = registerForActivityResult(
|
||||
StartActivityForResult()
|
||||
) { result: ActivityResult? ->
|
||||
if (result?.resultCode == RESULT_OK) {
|
||||
val intent = result.data
|
||||
if (intent != null && intent.getBooleanExtra(RESTART_ACTIVITY_INTENT, false)) {
|
||||
recreate()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
|
||||
override fun handleOnBackPressed() {
|
||||
if (mSearchView != null && !mSearchView!!.isIconified) {
|
||||
mSearchView!!.isIconified = true
|
||||
} else {
|
||||
finish()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
if (mCurrentActionMode != null) {
|
||||
mAdapter.clearSelections()
|
||||
mCurrentActionMode!!.finish()
|
||||
}
|
||||
|
||||
if (mSearchView != null && !mSearchView!!.isIconified) {
|
||||
mFilter = mSearchView!!.query.toString()
|
||||
}
|
||||
// Start of active tab logic
|
||||
updateTabGroups(groupsTabLayout)
|
||||
|
||||
// Restore selected tab from Shared Preference
|
||||
selectedTab = applicationContext.getSharedPreferences(
|
||||
getString(R.string.sharedpreference_active_tab),
|
||||
MODE_PRIVATE
|
||||
).getInt(getString(R.string.sharedpreference_active_tab), 0)
|
||||
|
||||
// Restore sort preferences from Shared Preferences
|
||||
mOrder = Utils.getLoyaltyCardOrder(this)
|
||||
mOrderDirection = Utils.getLoyaltyCardOrderDirection(this)
|
||||
|
||||
mGroup = null
|
||||
|
||||
if (groupsTabLayout.tabCount != 0) {
|
||||
var tab = groupsTabLayout.getTabAt(selectedTab)
|
||||
if (tab == null) {
|
||||
tab = groupsTabLayout.getTabAt(0)
|
||||
}
|
||||
|
||||
groupsTabLayout.selectTab(tab)
|
||||
checkNotNull(tab)
|
||||
mGroup = tab.tag
|
||||
} else {
|
||||
scaleScreen()
|
||||
}
|
||||
|
||||
updateLoyaltyCardList(true)
|
||||
|
||||
// End of active tab logic
|
||||
|
||||
binding.fabAdd.setOnClickListener {
|
||||
mBarcodeScannerLauncher.launch(
|
||||
Intent(applicationContext, ScanActivity::class.java).apply {
|
||||
putExtras(Bundle().apply {
|
||||
if (selectedTab != 0) {
|
||||
putString(
|
||||
LoyaltyCardEditActivity.BUNDLE_ADDGROUP,
|
||||
groupsTabLayout.getTabAt(selectedTab)!!.text.toString()
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
)
|
||||
}
|
||||
binding.fabAdd.bringToFront()
|
||||
|
||||
// Apply column count setting to card list
|
||||
val layoutManager = contentMainBinding.list.layoutManager as GridLayoutManager?
|
||||
if (layoutManager != null) {
|
||||
val settings = Settings(this)
|
||||
layoutManager.setSpanCount(settings.getPreferredColumnCount())
|
||||
}
|
||||
}
|
||||
|
||||
private fun displayCardSetupOptions(menu: Menu, shouldShow: Boolean) {
|
||||
for (id in intArrayOf(R.id.action_search, R.id.action_display_options, R.id.action_sort)) {
|
||||
menu.findItem(id).isVisible = shouldShow
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateLoyaltyCardCount() {
|
||||
mLoyaltyCardCount = DBHelper.getLoyaltyCardCount(mDatabase)
|
||||
}
|
||||
|
||||
private fun updateLoyaltyCardList(updateCount: Boolean) {
|
||||
var group: Group? = null
|
||||
if (mGroup != null) {
|
||||
group = mGroup as Group
|
||||
}
|
||||
|
||||
mAdapter.swapCursor(
|
||||
DBHelper.getLoyaltyCardCursor(
|
||||
mDatabase,
|
||||
mFilter,
|
||||
group,
|
||||
mOrder,
|
||||
mOrderDirection,
|
||||
if (mAdapter.showingArchivedCards()) DBHelper.LoyaltyCardArchiveFilter.All else DBHelper.LoyaltyCardArchiveFilter.Unarchived
|
||||
)
|
||||
)
|
||||
|
||||
if (updateCount) {
|
||||
updateLoyaltyCardCount()
|
||||
// Update menu icons if necessary
|
||||
invalidateOptionsMenu()
|
||||
}
|
||||
|
||||
if (mLoyaltyCardCount > 0) {
|
||||
// We want the cardList to be visible regardless of the filtered match count
|
||||
// to ensure that the noMatchingCardsText doesn't end up being shown below
|
||||
// the keyboard
|
||||
contentMainBinding.helpSection.visibility = View.GONE
|
||||
contentMainBinding.noGroupCardsText.visibility = View.GONE
|
||||
|
||||
if (mAdapter.itemCount > 0) {
|
||||
contentMainBinding.list.visibility = View.VISIBLE
|
||||
contentMainBinding.noMatchingCardsText.visibility = View.GONE
|
||||
} else {
|
||||
contentMainBinding.list.visibility = View.GONE
|
||||
if (!mFilter.isEmpty()) {
|
||||
// Actual Empty Search Result
|
||||
contentMainBinding.noMatchingCardsText.visibility = View.VISIBLE
|
||||
contentMainBinding.noGroupCardsText.visibility = View.GONE
|
||||
} else {
|
||||
// Group Tab with no Group Cards
|
||||
contentMainBinding.noMatchingCardsText.visibility = View.GONE
|
||||
contentMainBinding.noGroupCardsText.visibility = View.VISIBLE
|
||||
}
|
||||
}
|
||||
} else {
|
||||
contentMainBinding.list.visibility = View.GONE
|
||||
contentMainBinding.helpSection.visibility = View.VISIBLE
|
||||
|
||||
contentMainBinding.noMatchingCardsText.visibility = View.GONE
|
||||
contentMainBinding.noGroupCardsText.visibility = View.GONE
|
||||
}
|
||||
|
||||
if (mCurrentActionMode != null) {
|
||||
mCurrentActionMode!!.finish()
|
||||
}
|
||||
|
||||
ListWidget().updateAll(mAdapter.mContext)
|
||||
ShortcutHelper.updateShortcuts(mAdapter.mContext)
|
||||
}
|
||||
|
||||
private fun processParseResultList(
|
||||
parseResultList: MutableList<ParseResult?>,
|
||||
group: String?,
|
||||
closeAppOnNoBarcode: Boolean
|
||||
) {
|
||||
require(!parseResultList.isEmpty()) { "parseResultList may not be empty" }
|
||||
|
||||
Utils.makeUserChooseParseResultFromList(
|
||||
this@MainActivity,
|
||||
parseResultList,
|
||||
object : ParseResultListDisambiguatorCallback {
|
||||
override fun onUserChoseParseResult(parseResult: ParseResult) {
|
||||
val intent =
|
||||
Intent(applicationContext, LoyaltyCardEditActivity::class.java)
|
||||
val bundle = parseResult.toLoyaltyCardBundle(this@MainActivity)
|
||||
if (group != null) {
|
||||
bundle.putString(LoyaltyCardEditActivity.BUNDLE_ADDGROUP, group)
|
||||
}
|
||||
intent.putExtras(bundle)
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
override fun onUserDismissedSelector() {
|
||||
if (closeAppOnNoBarcode) {
|
||||
finish()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun onSharedIntent(intent: Intent) {
|
||||
val receivedAction = intent.action
|
||||
val receivedType = intent.type
|
||||
|
||||
if (receivedAction == null || receivedType == null) {
|
||||
return
|
||||
}
|
||||
|
||||
val parseResultList: MutableList<ParseResult?>?
|
||||
|
||||
// Check for shared text
|
||||
if (receivedAction == Intent.ACTION_SEND && receivedType == "text/plain") {
|
||||
val loyaltyCard = LoyaltyCard()
|
||||
loyaltyCard.setCardId(intent.getStringExtra(Intent.EXTRA_TEXT)!!)
|
||||
parseResultList = mutableListOf(ParseResult(ParseResultType.BARCODE_ONLY, loyaltyCard))
|
||||
} else {
|
||||
// Parse whatever file was sent, regardless of opening or sharing
|
||||
val data: Uri? = when (receivedAction) {
|
||||
Intent.ACTION_VIEW -> {
|
||||
intent.data
|
||||
}
|
||||
Intent.ACTION_SEND -> {
|
||||
intent.getParcelableExtra(Intent.EXTRA_STREAM)
|
||||
}
|
||||
else -> {
|
||||
Log.e(TAG, "Wrong action type to parse intent")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (receivedType.startsWith("image/")) {
|
||||
parseResultList = Utils.retrieveBarcodesFromImage(this, data)
|
||||
} else if (receivedType == "application/pdf") {
|
||||
parseResultList = Utils.retrieveBarcodesFromPdf(this, data)
|
||||
} else if (mutableListOf<String?>(
|
||||
"application/vnd.apple.pkpass",
|
||||
"application/vnd-com.apple.pkpass"
|
||||
).contains(receivedType)
|
||||
) {
|
||||
parseResultList = Utils.retrieveBarcodesFromPkPass(this, data)
|
||||
} else if (receivedType == "application/vnd.espass-espass") {
|
||||
// FIXME: espass is not pkpass
|
||||
// However, several users stated in https://github.com/CatimaLoyalty/Android/issues/2197 that the formats are extremely similar to the point they could rename an .espass file to .pkpass and have it imported
|
||||
// So it makes sense to "unofficially" treat it as a PKPASS for now, even though not completely correct
|
||||
parseResultList = Utils.retrieveBarcodesFromPkPass(this, data)
|
||||
} else if (receivedType == "application/vnd.apple.pkpasses") {
|
||||
parseResultList = Utils.retrieveBarcodesFromPkPasses(this, data)
|
||||
} else {
|
||||
Log.e(TAG, "Wrong mime-type")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Give up if we should parse but there is nothing to parse
|
||||
if (parseResultList == null || parseResultList.isEmpty()) {
|
||||
finish()
|
||||
return
|
||||
}
|
||||
|
||||
processParseResultList(parseResultList, null, true)
|
||||
}
|
||||
|
||||
private fun extractIntentFields(intent: Intent) {
|
||||
onSharedIntent(intent)
|
||||
}
|
||||
|
||||
fun updateTabGroups(groupsTabLayout: TabLayout) {
|
||||
val newGroups = DBHelper.getGroups(mDatabase)
|
||||
|
||||
if (newGroups.isEmpty()) {
|
||||
groupsTabLayout.removeAllTabs()
|
||||
groupsTabLayout.visibility = View.GONE
|
||||
return
|
||||
}
|
||||
|
||||
groupsTabLayout.removeAllTabs()
|
||||
groupsTabLayout.addTab(
|
||||
groupsTabLayout.newTab().apply {
|
||||
setText(R.string.all)
|
||||
tag = null
|
||||
},
|
||||
false
|
||||
)
|
||||
|
||||
for (group in newGroups) {
|
||||
groupsTabLayout.addTab(
|
||||
groupsTabLayout.newTab().apply {
|
||||
text = group._id
|
||||
tag = group
|
||||
},
|
||||
false
|
||||
)
|
||||
}
|
||||
|
||||
groupsTabLayout.visibility = View.VISIBLE
|
||||
}
|
||||
|
||||
// Saving currentQuery to finalQuery for user, this will be used to restore search history, happens when user clicks a card from list
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
super.onSaveInstanceState(outState)
|
||||
finalQuery = currentQuery
|
||||
// Putting the query also into outState for later use in onRestoreInstanceState when rotating screen
|
||||
if (mSearchView != null) {
|
||||
outState.putString(STATE_SEARCH_QUERY, finalQuery)
|
||||
}
|
||||
}
|
||||
|
||||
// Restoring instance state when rotation of screen happens with the goal to restore search query for user
|
||||
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
|
||||
super.onRestoreInstanceState(savedInstanceState)
|
||||
finalQuery = savedInstanceState.getString(STATE_SEARCH_QUERY, "")
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(inputMenu: Menu): Boolean {
|
||||
menuInflater.inflate(R.menu.main_menu, inputMenu)
|
||||
|
||||
displayCardSetupOptions(inputMenu, mLoyaltyCardCount > 0)
|
||||
|
||||
val searchManager = getSystemService(SEARCH_SERVICE) as SearchManager?
|
||||
if (searchManager != null) {
|
||||
val searchMenuItem = inputMenu.findItem(R.id.action_search)
|
||||
mSearchView = searchMenuItem.actionView as SearchView?
|
||||
mSearchView!!.setSearchableInfo(searchManager.getSearchableInfo(componentName))
|
||||
mSearchView!!.setSubmitButtonEnabled(false)
|
||||
mSearchView!!.setOnCloseListener {
|
||||
invalidateOptionsMenu()
|
||||
false
|
||||
}
|
||||
|
||||
/*
|
||||
* On Android 13 and later, pressing Back while the search view is open hides the keyboard
|
||||
* and collapses the search view at the same time.
|
||||
* This brings back the old behavior on Android 12 and lower: pressing Back once
|
||||
* hides the keyboard, press again while keyboard is hidden to collapse the search view.
|
||||
*/
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
searchMenuItem.setOnActionExpandListener(object : MenuItem.OnActionExpandListener {
|
||||
override fun onMenuItemActionExpand(item: MenuItem): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onMenuItemActionCollapse(item: MenuItem): Boolean {
|
||||
if (mSearchView!!.hasFocus()) {
|
||||
mSearchView!!.clearFocus()
|
||||
return false
|
||||
}
|
||||
currentQuery = ""
|
||||
mFilter = ""
|
||||
updateLoyaltyCardList(false)
|
||||
return true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
mSearchView!!.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
|
||||
override fun onQueryTextSubmit(query: String?): Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onQueryTextChange(newText: String): Boolean {
|
||||
mFilter = newText
|
||||
// New logic to ensure search history after coming back from picked card - user will see the last search query
|
||||
if (newText.isEmpty()) {
|
||||
if (!finalQuery.isEmpty()) {
|
||||
// Setting the query text for user after coming back from picked card from finalQuery
|
||||
mSearchView!!.setQuery(finalQuery, false)
|
||||
} else if (!currentQuery.isEmpty()) {
|
||||
// Else if is needed in case user deletes search - expected behaviour is to show all cards
|
||||
currentQuery = ""
|
||||
mSearchView!!.setQuery(currentQuery, false)
|
||||
}
|
||||
} else {
|
||||
// Setting search query each time user changes the text in search to temporary variable to be used later in finalQuery String which will be used to restore search history
|
||||
currentQuery = newText
|
||||
}
|
||||
val currentTab =
|
||||
groupsTabLayout.getTabAt(groupsTabLayout.selectedTabPosition)
|
||||
mGroup = currentTab?.tag
|
||||
|
||||
updateLoyaltyCardList(false)
|
||||
|
||||
return true
|
||||
}
|
||||
})
|
||||
// Check if we came from a picked card back to search, in that case we want to show the search view with previous search query
|
||||
if (!finalQuery.isEmpty()) {
|
||||
// Expand the search view to show the query
|
||||
searchMenuItem.expandActionView()
|
||||
// Setting the query text to empty String due to behaviour of onQueryTextChange after coming back from picked card - onQueryTextChange is called automatically without users interaction
|
||||
finalQuery = ""
|
||||
mSearchView!!.setQuery(currentQuery, false)
|
||||
}
|
||||
}
|
||||
|
||||
return super.onCreateOptionsMenu(inputMenu)
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(inputItem: MenuItem): Boolean {
|
||||
when (inputItem.itemId) {
|
||||
android.R.id.home -> {
|
||||
onBackPressedDispatcher.onBackPressed()
|
||||
}
|
||||
R.id.action_display_options -> {
|
||||
mAdapter.showDisplayOptionsDialog()
|
||||
invalidateOptionsMenu()
|
||||
return true
|
||||
}
|
||||
R.id.action_sort -> {
|
||||
val currentIndex = AtomicInteger()
|
||||
val loyaltyCardOrders = listOf<LoyaltyCardOrder?>(*LoyaltyCardOrder.entries.toTypedArray())
|
||||
for (i in loyaltyCardOrders.indices) {
|
||||
if (mOrder == loyaltyCardOrders[i]) {
|
||||
currentIndex.set(i)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
MaterialAlertDialogBuilder(this@MainActivity).apply {
|
||||
setTitle(R.string.sort_by)
|
||||
|
||||
val sortingOptionBinding = SortingOptionBinding.inflate(LayoutInflater.from(this@MainActivity), null, false)
|
||||
val customLayout: View = sortingOptionBinding.getRoot()
|
||||
setView(customLayout)
|
||||
|
||||
val showReversed = sortingOptionBinding.checkBoxReverse
|
||||
|
||||
showReversed.isChecked = mOrderDirection == LoyaltyCardOrderDirection.Descending
|
||||
|
||||
setSingleChoiceItems(
|
||||
R.array.sort_types_array,
|
||||
currentIndex.get()
|
||||
) { _: DialogInterface?, which: Int ->
|
||||
currentIndex.set(which)
|
||||
}
|
||||
|
||||
setPositiveButton(
|
||||
R.string.sort
|
||||
) { dialog, _ ->
|
||||
setSort(
|
||||
loyaltyCardOrders[currentIndex.get()]!!,
|
||||
if (showReversed.isChecked) LoyaltyCardOrderDirection.Descending else LoyaltyCardOrderDirection.Ascending
|
||||
)
|
||||
ListWidget().updateAll(this@MainActivity)
|
||||
dialog?.dismiss()
|
||||
}
|
||||
|
||||
setNegativeButton(R.string.cancel) { dialog, _ ->
|
||||
dialog.dismiss()
|
||||
}
|
||||
}.create().show()
|
||||
|
||||
return true
|
||||
}
|
||||
R.id.action_manage_groups -> {
|
||||
startActivity(
|
||||
Intent(applicationContext, ManageGroupsActivity::class.java)
|
||||
)
|
||||
return true
|
||||
}
|
||||
R.id.action_import_export -> {
|
||||
startActivity(
|
||||
Intent(applicationContext, ImportExportActivity::class.java)
|
||||
)
|
||||
return true
|
||||
}
|
||||
R.id.action_settings -> {
|
||||
mSettingsLauncher.launch(
|
||||
Intent(applicationContext, SettingsActivity::class.java)
|
||||
)
|
||||
return true
|
||||
}
|
||||
R.id.action_about -> {
|
||||
startActivity(
|
||||
Intent(applicationContext, AboutActivity::class.java)
|
||||
)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return super.onOptionsItemSelected(inputItem)
|
||||
}
|
||||
|
||||
private fun setSort(order: LoyaltyCardOrder, direction: LoyaltyCardOrderDirection) {
|
||||
// Update values
|
||||
mOrder = order
|
||||
mOrderDirection = direction
|
||||
|
||||
// Store in Shared Preference to restore next app launch
|
||||
applicationContext.getSharedPreferences(
|
||||
getString(R.string.sharedpreference_sort),
|
||||
MODE_PRIVATE
|
||||
).edit {
|
||||
putString(
|
||||
getString(R.string.sharedpreference_sort_order),
|
||||
order.name
|
||||
)
|
||||
putString(
|
||||
getString(R.string.sharedpreference_sort_direction),
|
||||
direction.name
|
||||
)
|
||||
}
|
||||
|
||||
// Update card list
|
||||
updateLoyaltyCardList(false)
|
||||
}
|
||||
|
||||
override fun onRowLongClicked(inputPosition: Int) {
|
||||
enableActionMode(inputPosition)
|
||||
}
|
||||
|
||||
private fun enableActionMode(inputPosition: Int) {
|
||||
if (mCurrentActionMode == null) {
|
||||
mCurrentActionMode = startSupportActionMode(mCurrentActionModeCallback)
|
||||
}
|
||||
toggleSelection(inputPosition)
|
||||
}
|
||||
|
||||
private fun scaleScreen() {
|
||||
val displayMetrics = DisplayMetrics()
|
||||
windowManager.defaultDisplay.getMetrics(displayMetrics)
|
||||
val screenHeight = displayMetrics.heightPixels
|
||||
val mediumSizePx = TypedValue.applyDimension(
|
||||
TypedValue.COMPLEX_UNIT_DIP,
|
||||
MEDIUM_SCALE_FACTOR_DIP.toFloat(),
|
||||
getResources().displayMetrics
|
||||
)
|
||||
val shouldScaleSmaller = screenHeight < mediumSizePx
|
||||
|
||||
binding.include.welcomeIcon.visibility = if (shouldScaleSmaller) View.GONE else View.VISIBLE
|
||||
}
|
||||
|
||||
private fun toggleSelection(inputPosition: Int) {
|
||||
mAdapter.toggleSelection(inputPosition)
|
||||
val count = mAdapter.selectedItemCount
|
||||
|
||||
if (count == 0) {
|
||||
mCurrentActionMode!!.finish()
|
||||
} else {
|
||||
mCurrentActionMode!!.title = getResources().getQuantityString(
|
||||
R.plurals.selectedCardCount,
|
||||
count,
|
||||
count
|
||||
)
|
||||
|
||||
val editItem = mCurrentActionMode!!.menu.findItem(R.id.action_edit)
|
||||
val duplicateItem = mCurrentActionMode!!.menu.findItem(R.id.action_duplicate)
|
||||
val archiveItem = mCurrentActionMode!!.menu.findItem(R.id.action_archive)
|
||||
val unarchiveItem = mCurrentActionMode!!.menu.findItem(R.id.action_unarchive)
|
||||
val starItem = mCurrentActionMode!!.menu.findItem(R.id.action_star)
|
||||
val unstarItem = mCurrentActionMode!!.menu.findItem(R.id.action_unstar)
|
||||
|
||||
var hasStarred = false
|
||||
var hasUnstarred = false
|
||||
var hasArchived = false
|
||||
var hasUnarchived = false
|
||||
|
||||
for (loyaltyCard in mAdapter.getSelectedItems()) {
|
||||
if (loyaltyCard.starStatus == 1) {
|
||||
hasStarred = true
|
||||
} else {
|
||||
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.isVisible = hasArchived
|
||||
archiveItem.isVisible = hasUnarchived
|
||||
|
||||
if (count == 1) {
|
||||
starItem.isVisible = !hasStarred
|
||||
unstarItem.isVisible = !hasUnstarred
|
||||
editItem.isVisible = true
|
||||
editItem.isEnabled = true
|
||||
duplicateItem.isVisible = true
|
||||
duplicateItem.isEnabled = true
|
||||
} else {
|
||||
starItem.isVisible = hasUnstarred
|
||||
unstarItem.isVisible = hasStarred
|
||||
|
||||
editItem.isVisible = false
|
||||
editItem.isEnabled = false
|
||||
duplicateItem.isVisible = false
|
||||
duplicateItem.isEnabled = false
|
||||
}
|
||||
|
||||
mCurrentActionMode!!.invalidate()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override fun onRowClicked(inputPosition: Int) {
|
||||
if (mAdapter.selectedItemCount > 0) {
|
||||
enableActionMode(inputPosition)
|
||||
} else {
|
||||
// FIXME
|
||||
//
|
||||
// There is a really nasty edge case that can happen when someone taps a card but right
|
||||
// after it swipes (very small window, hard to reproduce). The cursor gets replaced and
|
||||
// may not have a card at the ID number that is returned from onRowClicked.
|
||||
//
|
||||
// The proper fix, obviously, would involve makes sure an onFling can't happen while a
|
||||
// click is being processed. Sadly, I have not yet found a way to make that possible.
|
||||
val loyaltyCard: LoyaltyCard
|
||||
try {
|
||||
loyaltyCard = mAdapter.getCard(inputPosition)
|
||||
} catch (e: CursorIndexOutOfBoundsException) {
|
||||
Log.w(TAG, "Prevented crash from tap + swipe on ID $inputPosition: $e")
|
||||
return
|
||||
}
|
||||
|
||||
startActivity(
|
||||
Intent(this, LoyaltyCardViewActivity::class.java).apply {
|
||||
action = ""
|
||||
putExtras(Bundle().apply {
|
||||
putInt(LoyaltyCardViewActivity.BUNDLE_ID, loyaltyCard.id)
|
||||
|
||||
val cardList = ArrayList<Int?>()
|
||||
for (i in 0..<mAdapter.itemCount) {
|
||||
cardList.add(mAdapter.getCard(i).id)
|
||||
}
|
||||
|
||||
putIntegerArrayList(LoyaltyCardViewActivity.BUNDLE_CARDLIST, cardList)
|
||||
})
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "Catima"
|
||||
const val RESTART_ACTIVITY_INTENT: String = "restart_activity_intent"
|
||||
|
||||
private const val MEDIUM_SCALE_FACTOR_DIP = 460
|
||||
const val STATE_SEARCH_QUERY: String = "SEARCH_QUERY"
|
||||
}
|
||||
}
|
||||
@@ -1,242 +0,0 @@
|
||||
package protect.card_locker;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.os.Bundle;
|
||||
import android.text.Editable;
|
||||
import android.text.TextWatcher;
|
||||
import android.util.Log;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.widget.EditText;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.activity.OnBackPressedCallback;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
import com.google.android.material.floatingactionbutton.FloatingActionButton;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import protect.card_locker.databinding.ActivityManageGroupBinding;
|
||||
|
||||
public class ManageGroupActivity extends CatimaAppCompatActivity implements ManageGroupCursorAdapter.CardAdapterListener {
|
||||
private ActivityManageGroupBinding binding;
|
||||
private SQLiteDatabase mDatabase;
|
||||
private ManageGroupCursorAdapter mAdapter;
|
||||
|
||||
private final String SAVE_INSTANCE_ADAPTER_STATE = "adapterState";
|
||||
private final String SAVE_INSTANCE_CURRENT_GROUP_NAME = "currentGroupName";
|
||||
|
||||
protected Group mGroup = null;
|
||||
private RecyclerView mCardList;
|
||||
private TextView noGroupCardsText;
|
||||
private EditText mGroupNameText;
|
||||
|
||||
private boolean mGroupNameNotInUse;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle inputSavedInstanceState) {
|
||||
super.onCreate(inputSavedInstanceState);
|
||||
binding = ActivityManageGroupBinding.inflate(getLayoutInflater());
|
||||
setContentView(binding.getRoot());
|
||||
Utils.applyWindowInsetsAndFabOffset(binding.getRoot(), binding.fabSave);
|
||||
Toolbar toolbar = binding.toolbar;
|
||||
setSupportActionBar(toolbar);
|
||||
|
||||
mDatabase = new DBHelper(this).getWritableDatabase();
|
||||
|
||||
noGroupCardsText = binding.include.noGroupCardsText;
|
||||
mCardList = binding.include.list;
|
||||
FloatingActionButton saveButton = binding.fabSave;
|
||||
|
||||
mGroupNameText = binding.editTextGroupName;
|
||||
|
||||
mGroupNameText.addTextChangedListener(new TextWatcher() {
|
||||
@Override
|
||||
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTextChanged(CharSequence s, int start, int before, int count) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterTextChanged(Editable s) {
|
||||
mGroupNameNotInUse = true;
|
||||
mGroupNameText.setError(null);
|
||||
String currentGroupName = mGroupNameText.getText().toString().trim();
|
||||
if (currentGroupName.length() == 0) {
|
||||
mGroupNameText.setError(getResources().getText(R.string.group_name_is_empty));
|
||||
return;
|
||||
}
|
||||
if (!mGroup._id.equals(currentGroupName)) {
|
||||
if (DBHelper.getGroup(mDatabase, currentGroupName) != null) {
|
||||
mGroupNameNotInUse = false;
|
||||
mGroupNameText.setError(getResources().getText(R.string.group_name_already_in_use));
|
||||
} else {
|
||||
mGroupNameNotInUse = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Intent intent = getIntent();
|
||||
String groupId = intent.getStringExtra("group");
|
||||
if (groupId == null) {
|
||||
throw (new IllegalArgumentException("this activity expects a group loaded into it's intent"));
|
||||
}
|
||||
Log.d("groupId", "groupId: " + groupId);
|
||||
mGroup = DBHelper.getGroup(mDatabase, groupId);
|
||||
if (mGroup == null) {
|
||||
throw (new IllegalArgumentException("cannot load group " + groupId + " from database"));
|
||||
}
|
||||
mGroupNameText.setText(mGroup._id);
|
||||
setTitle(getString(R.string.editGroup, mGroup._id));
|
||||
mAdapter = new ManageGroupCursorAdapter(this, null, this, mGroup, null);
|
||||
mCardList.setAdapter(mAdapter);
|
||||
registerForContextMenu(mCardList);
|
||||
|
||||
if (inputSavedInstanceState != null) {
|
||||
mAdapter.importInGroupState(integerArrayToAdapterState(inputSavedInstanceState.getIntegerArrayList(SAVE_INSTANCE_ADAPTER_STATE)));
|
||||
mGroupNameText.setText(inputSavedInstanceState.getString(SAVE_INSTANCE_CURRENT_GROUP_NAME));
|
||||
}
|
||||
|
||||
enableToolbarBackButton();
|
||||
|
||||
saveButton.setOnClickListener(v -> {
|
||||
String currentGroupName = mGroupNameText.getText().toString().trim();
|
||||
if (!currentGroupName.equals(mGroup._id)) {
|
||||
if (currentGroupName.length() == 0) {
|
||||
Toast.makeText(getApplicationContext(), R.string.group_name_is_empty, Toast.LENGTH_SHORT).show();
|
||||
return;
|
||||
}
|
||||
if (!mGroupNameNotInUse) {
|
||||
Toast.makeText(getApplicationContext(), R.string.group_name_already_in_use, Toast.LENGTH_SHORT).show();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
mAdapter.commitToDatabase();
|
||||
if (!currentGroupName.equals(mGroup._id)) {
|
||||
DBHelper.updateGroup(mDatabase, mGroup._id, currentGroupName);
|
||||
}
|
||||
Toast.makeText(getApplicationContext(), R.string.group_updated, Toast.LENGTH_SHORT).show();
|
||||
finish();
|
||||
});
|
||||
// this setText is here because content_main.xml is reused from main activity
|
||||
noGroupCardsText.setText(getResources().getText(R.string.noGiftCardsGroup));
|
||||
updateLoyaltyCardList();
|
||||
|
||||
getOnBackPressedDispatcher().addCallback(this, new OnBackPressedCallback(true) {
|
||||
@Override
|
||||
public void handleOnBackPressed() {
|
||||
leaveWithoutSaving();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private ArrayList<Integer> adapterStateToIntegerArray(HashMap<Integer, Boolean> adapterState) {
|
||||
ArrayList<Integer> ret = new ArrayList<>(adapterState.size() * 2);
|
||||
for (Map.Entry<Integer, Boolean> entry : adapterState.entrySet()) {
|
||||
ret.add(entry.getKey());
|
||||
ret.add(entry.getValue() ? 1 : 0);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
private HashMap<Integer, Boolean> integerArrayToAdapterState(ArrayList<Integer> in) {
|
||||
HashMap<Integer, Boolean> ret = new HashMap<>();
|
||||
if (in.size() % 2 != 0) {
|
||||
throw (new RuntimeException("failed restoring adapterState from integer array list"));
|
||||
}
|
||||
for (int i = 0; i < in.size(); i += 2) {
|
||||
ret.put(in.get(i), in.get(i + 1) == 1);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu inputMenu) {
|
||||
getMenuInflater().inflate(R.menu.card_details_menu, inputMenu);
|
||||
|
||||
return super.onCreateOptionsMenu(inputMenu);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem inputItem) {
|
||||
int id = inputItem.getItemId();
|
||||
|
||||
if (id == R.id.action_display_options) {
|
||||
mAdapter.showDisplayOptionsDialog();
|
||||
invalidateOptionsMenu();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return super.onOptionsItemSelected(inputItem);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSaveInstanceState(@NonNull Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
|
||||
outState.putIntegerArrayList(SAVE_INSTANCE_ADAPTER_STATE, adapterStateToIntegerArray(mAdapter.exportInGroupState()));
|
||||
outState.putString(SAVE_INSTANCE_CURRENT_GROUP_NAME, mGroupNameText.getText().toString());
|
||||
}
|
||||
|
||||
private void updateLoyaltyCardList() {
|
||||
mAdapter.swapCursor(DBHelper.getLoyaltyCardCursor(mDatabase));
|
||||
|
||||
if (mAdapter.getItemCount() == 0) {
|
||||
mCardList.setVisibility(View.GONE);
|
||||
noGroupCardsText.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
mCardList.setVisibility(View.VISIBLE);
|
||||
noGroupCardsText.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
private void leaveWithoutSaving() {
|
||||
if (hasChanged()) {
|
||||
AlertDialog.Builder builder = new MaterialAlertDialogBuilder(ManageGroupActivity.this);
|
||||
builder.setTitle(R.string.leaveWithoutSaveTitle);
|
||||
builder.setMessage(R.string.leaveWithoutSaveConfirmation);
|
||||
builder.setPositiveButton(R.string.confirm, (dialog, which) -> finish());
|
||||
builder.setNegativeButton(R.string.cancel, (dialog, which) -> dialog.dismiss());
|
||||
AlertDialog dialog = builder.create();
|
||||
dialog.show();
|
||||
} else {
|
||||
finish();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onSupportNavigateUp() {
|
||||
getOnBackPressedDispatcher().onBackPressed();
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean hasChanged() {
|
||||
return mAdapter.hasChanged() || !mGroup._id.equals(mGroupNameText.getText().toString().trim());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRowLongClicked(int inputPosition) {
|
||||
mAdapter.toggleSelection(inputPosition);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRowClicked(int inputPosition) {
|
||||
mAdapter.toggleSelection(inputPosition);
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,246 @@
|
||||
package protect.card_locker
|
||||
|
||||
import android.content.DialogInterface
|
||||
import android.database.sqlite.SQLiteDatabase
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.widget.EditText
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.core.widget.doAfterTextChanged
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import protect.card_locker.LoyaltyCardCursorAdapter.CardAdapterListener
|
||||
import protect.card_locker.databinding.ActivityManageGroupBinding
|
||||
import protect.card_locker.preferences.Settings
|
||||
|
||||
class ManageGroupActivity : CatimaAppCompatActivity(), CardAdapterListener {
|
||||
private lateinit var binding: ActivityManageGroupBinding
|
||||
private lateinit var mDatabase: SQLiteDatabase
|
||||
private lateinit var mAdapter: ManageGroupCursorAdapter
|
||||
|
||||
private lateinit var mGroup: Group
|
||||
private lateinit var mCardList: RecyclerView
|
||||
private lateinit var noGroupCardsText: TextView
|
||||
private lateinit var mGroupNameText: EditText
|
||||
|
||||
private var mGroupNameNotInUse = false
|
||||
|
||||
override fun onCreate(inputSavedInstanceState: Bundle?) {
|
||||
super.onCreate(inputSavedInstanceState)
|
||||
binding = ActivityManageGroupBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
Utils.applyWindowInsetsAndFabOffset(binding.root, binding.fabSave)
|
||||
setSupportActionBar(binding.toolbar)
|
||||
|
||||
mDatabase = DBHelper(this).writableDatabase
|
||||
noGroupCardsText = binding.include.noGroupCardsText
|
||||
mCardList = binding.include.list
|
||||
|
||||
mGroupNameText = binding.editTextGroupName
|
||||
mGroupNameText.doAfterTextChanged {
|
||||
mGroupNameNotInUse = true
|
||||
mGroupNameText.error = null
|
||||
val currentGroupName = mGroupNameText.text.trim().toString()
|
||||
if (currentGroupName.isEmpty()) {
|
||||
mGroupNameText.error = getText(R.string.group_name_is_empty)
|
||||
return@doAfterTextChanged
|
||||
}
|
||||
if (mGroup._id != currentGroupName) {
|
||||
if (DBHelper.getGroup(mDatabase, currentGroupName) != null) {
|
||||
mGroupNameNotInUse = false
|
||||
mGroupNameText.error = getText(R.string.group_name_already_in_use)
|
||||
} else {
|
||||
mGroupNameNotInUse = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val groupId = intent.getStringExtra("group")
|
||||
?: throw (IllegalArgumentException("this activity expects a group loaded into it's intent"))
|
||||
Log.d("groupId", "groupId: $groupId")
|
||||
mGroup = DBHelper.getGroup(mDatabase, groupId)
|
||||
?: throw IllegalArgumentException("Cannot load group $groupId from database")
|
||||
mGroupNameText.setText(mGroup._id)
|
||||
setTitle(getString(R.string.editGroup, mGroup._id))
|
||||
mAdapter = ManageGroupCursorAdapter(this, null, this, mGroup, null)
|
||||
mCardList.adapter = mAdapter
|
||||
registerForContextMenu(mCardList)
|
||||
|
||||
if (inputSavedInstanceState != null) {
|
||||
mAdapter.importInGroupState(
|
||||
bundleToAdapterState(
|
||||
adapterStateBundle = inputSavedInstanceState.getBundle(
|
||||
SAVE_INSTANCE_ADAPTER_STATE
|
||||
)
|
||||
)
|
||||
)
|
||||
mGroupNameText.setText(
|
||||
inputSavedInstanceState.getString(
|
||||
SAVE_INSTANCE_CURRENT_GROUP_NAME
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
enableToolbarBackButton()
|
||||
|
||||
binding.fabSave.setOnClickListener { v: View ->
|
||||
val currentGroupName = mGroupNameText.text.trim().toString()
|
||||
if (currentGroupName != mGroup._id) {
|
||||
when {
|
||||
currentGroupName.isEmpty() -> {
|
||||
Toast.makeText(
|
||||
applicationContext,
|
||||
R.string.group_name_is_empty,
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
return@setOnClickListener
|
||||
}
|
||||
|
||||
!mGroupNameNotInUse -> {
|
||||
Toast.makeText(
|
||||
applicationContext,
|
||||
R.string.group_name_already_in_use,
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
return@setOnClickListener
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mAdapter.commitToDatabase()
|
||||
if (currentGroupName != mGroup._id) {
|
||||
DBHelper.updateGroup(mDatabase, mGroup._id, currentGroupName)
|
||||
}
|
||||
Toast.makeText(
|
||||
applicationContext,
|
||||
R.string.group_updated,
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
finish()
|
||||
}
|
||||
// this setText is here because content_main.xml is reused from main activity
|
||||
noGroupCardsText.text = getText(R.string.noGiftCardsGroup)
|
||||
updateLoyaltyCardList()
|
||||
|
||||
onBackPressedDispatcher.addCallback(
|
||||
owner = this,
|
||||
onBackPressedCallback = object : OnBackPressedCallback(true) {
|
||||
override fun handleOnBackPressed() {
|
||||
leaveWithoutSaving()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// Apply column count setting to card list
|
||||
val layoutManager = mCardList.layoutManager as GridLayoutManager?
|
||||
if (layoutManager != null) {
|
||||
val settings = Settings(this)
|
||||
layoutManager.setSpanCount(settings.getPreferredColumnCount())
|
||||
}
|
||||
}
|
||||
|
||||
private fun adapterStateToBundle(adapterState: HashMap<Int, Boolean>): Bundle {
|
||||
val adapterStateBundle = Bundle().apply {
|
||||
for (entry in adapterState.entries) {
|
||||
putBoolean(entry.key.toString(), entry.value)
|
||||
}
|
||||
}
|
||||
return adapterStateBundle
|
||||
}
|
||||
|
||||
private fun bundleToAdapterState(adapterStateBundle: Bundle?): Map<Int, Boolean> {
|
||||
adapterStateBundle ?: return emptyMap()
|
||||
val adapterStateMap = buildMap {
|
||||
for (key in adapterStateBundle.keySet()) {
|
||||
put(key.toInt(), adapterStateBundle.getBoolean(key))
|
||||
}
|
||||
}
|
||||
return adapterStateMap
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(inputMenu: Menu): Boolean {
|
||||
menuInflater.inflate(R.menu.card_details_menu, inputMenu)
|
||||
|
||||
return super.onCreateOptionsMenu(inputMenu)
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(inputItem: MenuItem): Boolean {
|
||||
val id = inputItem.itemId
|
||||
|
||||
if (id == R.id.action_display_options) {
|
||||
mAdapter.showDisplayOptionsDialog()
|
||||
invalidateOptionsMenu()
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
return super.onOptionsItemSelected(inputItem)
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
super.onSaveInstanceState(outState)
|
||||
|
||||
outState.putBundle(
|
||||
SAVE_INSTANCE_ADAPTER_STATE,
|
||||
adapterStateToBundle(mAdapter.exportInGroupState())
|
||||
)
|
||||
outState.putString(SAVE_INSTANCE_CURRENT_GROUP_NAME, mGroupNameText.text.toString())
|
||||
}
|
||||
|
||||
private fun updateLoyaltyCardList() {
|
||||
mAdapter.swapCursor(DBHelper.getLoyaltyCardCursor(mDatabase))
|
||||
|
||||
if (mAdapter.itemCount == 0) {
|
||||
mCardList.visibility = View.GONE
|
||||
noGroupCardsText.visibility = View.VISIBLE
|
||||
} else {
|
||||
mCardList.visibility = View.VISIBLE
|
||||
noGroupCardsText.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
private fun leaveWithoutSaving() {
|
||||
if (hasChanged()) {
|
||||
MaterialAlertDialogBuilder(this@ManageGroupActivity).apply {
|
||||
setTitle(R.string.leaveWithoutSaveTitle)
|
||||
setMessage(R.string.leaveWithoutSaveConfirmation)
|
||||
setPositiveButton(R.string.confirm) { dialog: DialogInterface, _ ->
|
||||
finish()
|
||||
}
|
||||
setNegativeButton(R.string.cancel) { dialog: DialogInterface, _ ->
|
||||
dialog.dismiss()
|
||||
}
|
||||
}.create().show()
|
||||
} else {
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSupportNavigateUp(): Boolean {
|
||||
onBackPressedDispatcher.onBackPressed()
|
||||
return true
|
||||
}
|
||||
|
||||
private fun hasChanged(): Boolean {
|
||||
return mAdapter.hasChanged() || mGroup._id != mGroupNameText.text.trim().toString()
|
||||
}
|
||||
|
||||
override fun onRowLongClicked(inputPosition: Int) {
|
||||
mAdapter.toggleSelection(inputPosition)
|
||||
}
|
||||
|
||||
override fun onRowClicked(inputPosition: Int) {
|
||||
mAdapter.toggleSelection(inputPosition)
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val SAVE_INSTANCE_ADAPTER_STATE = "adapterState"
|
||||
const val SAVE_INSTANCE_CURRENT_GROUP_NAME = "currentGroupName"
|
||||
}
|
||||
}
|
||||
@@ -99,7 +99,7 @@ public class ManageGroupCursorAdapter extends LoyaltyCardCursorAdapter {
|
||||
}
|
||||
}
|
||||
|
||||
public void importInGroupState(HashMap<Integer, Boolean> cardIdInGroupMap) {
|
||||
public void importInGroupState(Map<Integer, Boolean> cardIdInGroupMap) {
|
||||
mInGroupOverlay = new HashMap<>(cardIdInGroupMap);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,247 +0,0 @@
|
||||
package protect.card_locker;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.os.Bundle;
|
||||
import android.text.InputType;
|
||||
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 androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.recyclerview.widget.DefaultItemAnimator;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
import com.google.android.material.floatingactionbutton.FloatingActionButton;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import protect.card_locker.databinding.ManageGroupsActivityBinding;
|
||||
|
||||
public class ManageGroupsActivity extends CatimaAppCompatActivity implements GroupCursorAdapter.GroupAdapterListener {
|
||||
private ManageGroupsActivityBinding binding;
|
||||
private static final String TAG = "Catima";
|
||||
|
||||
private SQLiteDatabase mDatabase;
|
||||
private TextView mHelpText;
|
||||
private RecyclerView mGroupList;
|
||||
GroupCursorAdapter mAdapter;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
binding = ManageGroupsActivityBinding.inflate(getLayoutInflater());
|
||||
setTitle(R.string.groups);
|
||||
setContentView(binding.getRoot());
|
||||
Utils.applyWindowInsets(binding.getRoot());
|
||||
Toolbar toolbar = binding.toolbar;
|
||||
setSupportActionBar(toolbar);
|
||||
enableToolbarBackButton();
|
||||
|
||||
mDatabase = new DBHelper(this).getWritableDatabase();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
|
||||
FloatingActionButton addButton = binding.fabAdd;
|
||||
addButton.setOnClickListener(v -> createGroup());
|
||||
addButton.bringToFront();
|
||||
|
||||
mGroupList = binding.include.list;
|
||||
mHelpText = binding.include.helpText;
|
||||
|
||||
// Init group list
|
||||
RecyclerView.LayoutManager mLayoutManager = new LinearLayoutManager(getApplicationContext());
|
||||
mGroupList.setLayoutManager(mLayoutManager);
|
||||
mGroupList.setItemAnimator(new DefaultItemAnimator());
|
||||
|
||||
mAdapter = new GroupCursorAdapter(this, null, this);
|
||||
mGroupList.setAdapter(mAdapter);
|
||||
|
||||
updateGroupList();
|
||||
}
|
||||
|
||||
private void updateGroupList() {
|
||||
mAdapter.swapCursor(DBHelper.getGroupCursor(mDatabase));
|
||||
|
||||
if (DBHelper.getGroupCount(mDatabase) == 0) {
|
||||
mGroupList.setVisibility(View.GONE);
|
||||
mHelpText.setVisibility(View.VISIBLE);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
mGroupList.setVisibility(View.VISIBLE);
|
||||
mHelpText.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
private void invalidateHomescreenActiveTab() {
|
||||
SharedPreferences activeTabPref = getApplicationContext().getSharedPreferences(
|
||||
getString(R.string.sharedpreference_active_tab),
|
||||
Context.MODE_PRIVATE);
|
||||
SharedPreferences.Editor activeTabPrefEditor = activeTabPref.edit();
|
||||
activeTabPrefEditor.putInt(getString(R.string.sharedpreference_active_tab), 0);
|
||||
activeTabPrefEditor.apply();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
int id = item.getItemId();
|
||||
|
||||
if (id == android.R.id.home) {
|
||||
finish();
|
||||
}
|
||||
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
private void createGroup() {
|
||||
AlertDialog.Builder builder = new MaterialAlertDialogBuilder(this);
|
||||
|
||||
// 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
|
||||
);
|
||||
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);
|
||||
layout.addView(input);
|
||||
|
||||
// Set layout
|
||||
builder.setView(layout);
|
||||
|
||||
// Buttons
|
||||
builder.setPositiveButton(getString(R.string.ok), (dialog, which) -> {
|
||||
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();
|
||||
}
|
||||
|
||||
private String getGroupName(View view) {
|
||||
TextView groupNameTextView = view.findViewById(R.id.name);
|
||||
return (String) groupNameTextView.getText();
|
||||
}
|
||||
|
||||
private void moveGroup(View view, boolean up) {
|
||||
List<Group> groups = DBHelper.getGroups(mDatabase);
|
||||
final String groupName = getGroupName(view);
|
||||
|
||||
int currentIndex = DBHelper.getGroup(mDatabase, groupName).order;
|
||||
int newIndex;
|
||||
|
||||
// Reinsert group in correct position
|
||||
if (up) {
|
||||
newIndex = currentIndex - 1;
|
||||
} else {
|
||||
newIndex = currentIndex + 1;
|
||||
}
|
||||
|
||||
// Don't try to move out of bounds
|
||||
if (newIndex < 0 || newIndex >= groups.size()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Group group = groups.remove(currentIndex);
|
||||
groups.add(newIndex, group);
|
||||
|
||||
// Update database
|
||||
DBHelper.reorderGroups(mDatabase, groups);
|
||||
|
||||
// Update UI
|
||||
updateGroupList();
|
||||
|
||||
// Ordering may have changed, so invalidate
|
||||
invalidateHomescreenActiveTab();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMoveDownButtonClicked(View view) {
|
||||
moveGroup(view, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMoveUpButtonClicked(View view) {
|
||||
moveGroup(view, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEditButtonClicked(View view) {
|
||||
Intent intent = new Intent(this, ManageGroupActivity.class);
|
||||
intent.putExtra("group", getGroupName(view));
|
||||
startActivity(intent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDeleteButtonClicked(View view) {
|
||||
final String groupName = getGroupName(view);
|
||||
|
||||
AlertDialog.Builder builder = new MaterialAlertDialogBuilder(this);
|
||||
builder.setTitle(R.string.deleteConfirmationGroup);
|
||||
builder.setMessage(groupName);
|
||||
|
||||
builder.setPositiveButton(getString(R.string.ok), (dialog, which) -> {
|
||||
DBHelper.deleteGroup(mDatabase, groupName);
|
||||
updateGroupList();
|
||||
// Delete may change ordering, so invalidate
|
||||
invalidateHomescreenActiveTab();
|
||||
});
|
||||
builder.setNegativeButton(getString(R.string.cancel), (dialog, which) -> dialog.cancel());
|
||||
AlertDialog dialog = builder.create();
|
||||
dialog.show();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
package protect.card_locker
|
||||
|
||||
import android.content.DialogInterface
|
||||
import android.content.Intent
|
||||
import android.database.sqlite.SQLiteDatabase
|
||||
import android.os.Bundle
|
||||
import android.text.InputType
|
||||
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 androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.content.edit
|
||||
import androidx.core.widget.doOnTextChanged
|
||||
import androidx.recyclerview.widget.DefaultItemAnimator
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import protect.card_locker.GroupCursorAdapter.GroupAdapterListener
|
||||
import protect.card_locker.databinding.ManageGroupsActivityBinding
|
||||
|
||||
class ManageGroupsActivity : CatimaAppCompatActivity(), GroupAdapterListener {
|
||||
private lateinit var binding: ManageGroupsActivityBinding
|
||||
private lateinit var mDatabase: SQLiteDatabase
|
||||
private lateinit var mHelpText: TextView
|
||||
private lateinit var mGroupList: RecyclerView
|
||||
private lateinit var mAdapter: GroupCursorAdapter
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
binding = ManageGroupsActivityBinding.inflate(layoutInflater)
|
||||
setTitle(R.string.groups)
|
||||
setContentView(binding.root)
|
||||
Utils.applyWindowInsets(binding.root)
|
||||
setSupportActionBar(binding.toolbar)
|
||||
enableToolbarBackButton()
|
||||
|
||||
mDatabase = DBHelper(this).writableDatabase
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
with(binding.fabAdd) {
|
||||
setOnClickListener { v: View ->
|
||||
createGroup()
|
||||
}
|
||||
bringToFront()
|
||||
}
|
||||
|
||||
mGroupList = binding.include.list
|
||||
mHelpText = binding.include.helpText
|
||||
|
||||
// Init group list
|
||||
LinearLayoutManager(applicationContext).apply {
|
||||
mGroupList.layoutManager = this
|
||||
}
|
||||
mGroupList.setItemAnimator(DefaultItemAnimator())
|
||||
mAdapter = GroupCursorAdapter(this, null, this)
|
||||
mGroupList.setAdapter(mAdapter)
|
||||
|
||||
updateGroupList()
|
||||
}
|
||||
|
||||
private fun updateGroupList() {
|
||||
mAdapter.swapCursor(DBHelper.getGroupCursor(mDatabase))
|
||||
|
||||
if (DBHelper.getGroupCount(mDatabase) == 0) {
|
||||
mGroupList.visibility = View.GONE
|
||||
mHelpText.visibility = View.VISIBLE
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
mGroupList.visibility = View.VISIBLE
|
||||
mHelpText.visibility = View.GONE
|
||||
}
|
||||
|
||||
private fun invalidateHomescreenActiveTab() {
|
||||
val activeTabPref = getSharedPreferences(
|
||||
getString(R.string.sharedpreference_active_tab),
|
||||
MODE_PRIVATE
|
||||
)
|
||||
activeTabPref.edit {
|
||||
putInt(getString(R.string.sharedpreference_active_tab), 0)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
if (item.itemId == android.R.id.home) {
|
||||
finish()
|
||||
}
|
||||
|
||||
return super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
private fun createGroup() {
|
||||
val builder: AlertDialog.Builder = MaterialAlertDialogBuilder(this)
|
||||
|
||||
// Header
|
||||
builder.setTitle(R.string.enter_group_name)
|
||||
|
||||
// Layout
|
||||
val layout = LinearLayout(this)
|
||||
layout.orientation = LinearLayout.VERTICAL
|
||||
val params = LinearLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
).apply {
|
||||
val contentPadding =
|
||||
resources.getDimensionPixelSize(R.dimen.alert_dialog_content_padding)
|
||||
leftMargin = contentPadding
|
||||
topMargin = contentPadding / 2
|
||||
rightMargin = contentPadding
|
||||
}
|
||||
|
||||
// EditText with spacing
|
||||
val input = 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: DialogInterface, which: Int ->
|
||||
DBHelper.insertGroup(mDatabase, input.text.trim().toString())
|
||||
updateGroupList()
|
||||
}
|
||||
builder.setNegativeButton(getString(R.string.cancel)) { dialog: DialogInterface, which: Int ->
|
||||
dialog.cancel()
|
||||
}
|
||||
val dialog = builder.create()
|
||||
|
||||
// Now that the dialog exists, we can bind something that affects the OK button
|
||||
input.doOnTextChanged { s: CharSequence?, start: Int, before: Int, count: Int ->
|
||||
val groupName = s?.trim().toString()
|
||||
|
||||
if (groupName.isEmpty()) {
|
||||
input.error = getString(R.string.group_name_is_empty)
|
||||
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false)
|
||||
return@doOnTextChanged
|
||||
}
|
||||
|
||||
if (DBHelper.getGroup(mDatabase, groupName) != null) {
|
||||
input.error = getString(R.string.group_name_already_in_use)
|
||||
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false)
|
||||
return@doOnTextChanged
|
||||
}
|
||||
|
||||
input.error = null
|
||||
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(true)
|
||||
}
|
||||
|
||||
dialog.apply {
|
||||
show()
|
||||
// Disable button (must be done **after** dialog is shown to prevent crash
|
||||
getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false)
|
||||
// Set focus on input field
|
||||
window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE)
|
||||
}
|
||||
|
||||
input.requestFocus()
|
||||
}
|
||||
|
||||
private fun getGroupName(view: View): String {
|
||||
val groupNameTextView = view.findViewById<TextView>(R.id.name)
|
||||
return groupNameTextView.text.toString()
|
||||
}
|
||||
|
||||
private fun moveGroup(view: View, up: Boolean) {
|
||||
val groups = DBHelper.getGroups(mDatabase)
|
||||
val groupName = getGroupName(view)
|
||||
|
||||
val currentIndex = DBHelper.getGroup(mDatabase, groupName).order
|
||||
|
||||
// Reinsert group in correct position
|
||||
val newIndex: Int = if (up) {
|
||||
currentIndex - 1
|
||||
} else {
|
||||
currentIndex + 1
|
||||
}
|
||||
|
||||
// Don't try to move out of bounds
|
||||
if (newIndex < 0 || newIndex >= groups.size) {
|
||||
return
|
||||
}
|
||||
|
||||
val group = groups.removeAt(currentIndex)
|
||||
groups.add(newIndex, group)
|
||||
|
||||
// Update database
|
||||
DBHelper.reorderGroups(mDatabase, groups)
|
||||
|
||||
// Update UI
|
||||
updateGroupList()
|
||||
|
||||
// Ordering may have changed, so invalidate
|
||||
invalidateHomescreenActiveTab()
|
||||
}
|
||||
|
||||
override fun onMoveDownButtonClicked(view: View) {
|
||||
moveGroup(view, false)
|
||||
}
|
||||
|
||||
override fun onMoveUpButtonClicked(view: View) {
|
||||
moveGroup(view, true)
|
||||
}
|
||||
|
||||
override fun onEditButtonClicked(view: View) {
|
||||
Intent(this, ManageGroupActivity::class.java).apply {
|
||||
putExtra("group", getGroupName(view))
|
||||
startActivity(this)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDeleteButtonClicked(view: View) {
|
||||
val groupName = getGroupName(view)
|
||||
|
||||
MaterialAlertDialogBuilder(this).apply {
|
||||
setTitle(R.string.deleteConfirmationGroup)
|
||||
setMessage(groupName)
|
||||
|
||||
setPositiveButton(getString(R.string.ok)) { dialog: DialogInterface, which: Int ->
|
||||
DBHelper.deleteGroup(mDatabase, groupName)
|
||||
updateGroupList()
|
||||
// Delete may change ordering, so invalidate
|
||||
invalidateHomescreenActiveTab()
|
||||
}
|
||||
setNegativeButton(getString(R.string.cancel)) { dialog: DialogInterface, which: Int ->
|
||||
dialog.cancel()
|
||||
}
|
||||
}.create().show()
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,17 @@
|
||||
package protect.card_locker;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.ActivityNotFoundException;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.util.Log;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
|
||||
public class OpenWebLinkHandler {
|
||||
|
||||
private static final String TAG = "Catima";
|
||||
|
||||
public void openBrowser(AppCompatActivity activity, String url) {
|
||||
public void openBrowser(Activity activity, String url) {
|
||||
if (url == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -34,11 +34,6 @@ public class PermissionUtils {
|
||||
* @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;
|
||||
}
|
||||
|
||||
@@ -49,21 +44,14 @@ public class PermissionUtils {
|
||||
* @param activity
|
||||
* @param requestCode
|
||||
*/
|
||||
public static void requestStorageReadPermission(CatimaAppCompatActivity activity, int requestCode) {
|
||||
public static void requestStorageReadPermission(Activity 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);
|
||||
activity.onRequestPermissionsResult(requestCode, permissions, mockedResults);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,21 +62,14 @@ public class PermissionUtils {
|
||||
* @param activity
|
||||
* @param requestCode
|
||||
*/
|
||||
public static void requestCameraPermission(CatimaAppCompatActivity activity, int requestCode) {
|
||||
public static void requestCameraPermission(Activity 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);
|
||||
activity.onRequestPermissionsResult(requestCode, permissions, mockedResults);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,8 @@ import org.json.JSONObject
|
||||
import java.io.FileNotFoundException
|
||||
import java.io.IOException
|
||||
import java.math.BigDecimal
|
||||
import java.nio.charset.Charset
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.text.DateFormat
|
||||
import java.text.ParseException
|
||||
import java.time.ZonedDateTime
|
||||
@@ -40,6 +42,7 @@ class PkpassParser(context: Context, uri: Uri?) {
|
||||
private var cardId: String = context.getString(R.string.noBarcode)
|
||||
private var barcodeId: String? = null
|
||||
private var barcodeType: CatimaBarcode? = null
|
||||
private var barcodeEncoding: Charset = StandardCharsets.ISO_8859_1
|
||||
private var headerColor: Int? = null
|
||||
private val starStatus = 0
|
||||
private val lastUsed: Long = 0
|
||||
@@ -134,6 +137,7 @@ class PkpassParser(context: Context, uri: Uri?) {
|
||||
cardId,
|
||||
barcodeId,
|
||||
barcodeType,
|
||||
barcodeEncoding,
|
||||
headerColor,
|
||||
starStatus,
|
||||
lastUsed,
|
||||
@@ -342,7 +346,6 @@ class PkpassParser(context: Context, uri: Uri?) {
|
||||
else -> throw IllegalArgumentException("No valid barcode type")
|
||||
}
|
||||
|
||||
// FIXME: We probably need to do something with the messageEncoding field
|
||||
try {
|
||||
cardId = barcodeInfo.getString("altText")
|
||||
barcodeId = barcodeInfo.getString("message")
|
||||
@@ -351,6 +354,12 @@ class PkpassParser(context: Context, uri: Uri?) {
|
||||
barcodeId = null
|
||||
}
|
||||
|
||||
try {
|
||||
barcodeEncoding = Charset.forName(barcodeInfo.getString("messageEncoding"))
|
||||
} catch (ignored: Exception) {
|
||||
// If encoding is not defined, null or otherwise invalid, stick to the default
|
||||
}
|
||||
|
||||
// Don't set barcodeId if it's the same as cardId
|
||||
if (cardId == barcodeId) {
|
||||
barcodeId = null
|
||||
|
||||
@@ -1,542 +0,0 @@
|
||||
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;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
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;
|
||||
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.ListAdapter;
|
||||
import android.widget.SimpleAdapter;
|
||||
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.AlertDialog;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
import com.google.zxing.DecodeHintType;
|
||||
import com.google.zxing.ResultPoint;
|
||||
import com.google.zxing.client.android.Intents;
|
||||
import com.journeyapps.barcodescanner.BarcodeCallback;
|
||||
import com.journeyapps.barcodescanner.BarcodeResult;
|
||||
import com.journeyapps.barcodescanner.CaptureManager;
|
||||
import com.journeyapps.barcodescanner.DecoratedBarcodeView;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
|
||||
import protect.card_locker.databinding.CustomBarcodeScannerBinding;
|
||||
import protect.card_locker.databinding.ScanActivityBinding;
|
||||
|
||||
/**
|
||||
* Custom Scannner Activity extending from Activity to display a custom layout form scanner view.
|
||||
* <p>
|
||||
* Based on https://github.com/journeyapps/zxing-android-embedded/blob/0fdfbce9fb3285e985bad9971c5f7c0a7a334e7b/sample/src/main/java/example/zxing/CustomScannerActivity.java
|
||||
* originally licensed under Apache 2.0
|
||||
*/
|
||||
public class ScanActivity extends CatimaAppCompatActivity {
|
||||
private ScanActivityBinding binding;
|
||||
private CustomBarcodeScannerBinding customBarcodeScannerBinding;
|
||||
private static final String TAG = "Catima";
|
||||
|
||||
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 static final int PERMISSION_SCAN_ADD_FROM_PDF = 101;
|
||||
private static final int PERMISSION_SCAN_ADD_FROM_PKPASS = 102;
|
||||
|
||||
private CaptureManager capture;
|
||||
private DecoratedBarcodeView barcodeScannerView;
|
||||
|
||||
private String cardId;
|
||||
private String addGroup;
|
||||
private boolean torch = false;
|
||||
|
||||
private ActivityResultLauncher<Intent> manualAddLauncher;
|
||||
// can't use the pre-made contract because that launches the file manager for image type instead of gallery
|
||||
private ActivityResultLauncher<Intent> photoPickerLauncher;
|
||||
private ActivityResultLauncher<Intent> pdfPickerLauncher;
|
||||
private ActivityResultLauncher<Intent> pkpassPickerLauncher;
|
||||
|
||||
static final String STATE_SCANNER_ACTIVE = "scannerActive";
|
||||
private boolean mScannerActive = true;
|
||||
private boolean mHasError = false;
|
||||
|
||||
private void extractIntentFields(Intent intent) {
|
||||
final Bundle b = intent.getExtras();
|
||||
cardId = b != null ? b.getString(LoyaltyCard.BUNDLE_LOYALTY_CARD_CARD_ID) : null;
|
||||
addGroup = b != null ? b.getString(LoyaltyCardEditActivity.BUNDLE_ADDGROUP) : null;
|
||||
Log.d(TAG, "Scan activity: id=" + cardId);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
binding = ScanActivityBinding.inflate(getLayoutInflater());
|
||||
customBarcodeScannerBinding = CustomBarcodeScannerBinding.bind(binding.zxingBarcodeScanner);
|
||||
setTitle(R.string.scanCardBarcode);
|
||||
setContentView(binding.getRoot());
|
||||
Utils.applyWindowInsets(binding.getRoot());
|
||||
Toolbar toolbar = binding.toolbar;
|
||||
setSupportActionBar(toolbar);
|
||||
enableToolbarBackButton();
|
||||
|
||||
extractIntentFields(getIntent());
|
||||
|
||||
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()));
|
||||
pdfPickerLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> handleActivityResult(Utils.BARCODE_IMPORT_FROM_PDF_FILE, result.getResultCode(), result.getData()));
|
||||
pkpassPickerLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> handleActivityResult(Utils.BARCODE_IMPORT_FROM_PKPASS_FILE, result.getResultCode(), result.getData()));
|
||||
customBarcodeScannerBinding.fabOtherOptions.setOnClickListener(view -> {
|
||||
setScannerActive(false);
|
||||
|
||||
ArrayList<HashMap<String, Object>> list = new ArrayList<>();
|
||||
String[] texts = new String[]{
|
||||
getString(R.string.addWithoutBarcode),
|
||||
getString(R.string.addManually),
|
||||
getString(R.string.addFromImage),
|
||||
getString(R.string.addFromPdfFile),
|
||||
getString(R.string.addFromPkpass)
|
||||
};
|
||||
Object[] icons = new Object[]{
|
||||
R.drawable.baseline_block_24,
|
||||
R.drawable.ic_edit,
|
||||
R.drawable.baseline_image_24,
|
||||
R.drawable.baseline_picture_as_pdf_24,
|
||||
R.drawable.local_activity_24px
|
||||
};
|
||||
String[] columns = new String[]{"text", "icon"};
|
||||
|
||||
for (int i = 0; i < texts.length; i++) {
|
||||
HashMap<String, Object> map = new HashMap<>();
|
||||
map.put(columns[0], texts[i]);
|
||||
map.put(columns[1], icons[i]);
|
||||
list.add(map);
|
||||
}
|
||||
|
||||
ListAdapter adapter = new SimpleAdapter(
|
||||
ScanActivity.this,
|
||||
list,
|
||||
R.layout.alertdialog_row_with_icon,
|
||||
columns,
|
||||
new int[]{R.id.textView, R.id.imageView}
|
||||
);
|
||||
|
||||
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(ScanActivity.this);
|
||||
builder.setTitle(getString(R.string.add_a_card_in_a_different_way));
|
||||
builder.setAdapter(
|
||||
adapter,
|
||||
(dialogInterface, i) -> {
|
||||
switch (i) {
|
||||
case 0:
|
||||
addWithoutBarcode();
|
||||
break;
|
||||
case 1:
|
||||
addManually();
|
||||
break;
|
||||
case 2:
|
||||
addFromImage();
|
||||
break;
|
||||
case 3:
|
||||
addFromPdf();
|
||||
break;
|
||||
case 4:
|
||||
addFromPkPass();
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("Unknown 'Add a card in a different way' dialog option");
|
||||
}
|
||||
}
|
||||
);
|
||||
builder.setOnCancelListener(dialogInterface -> setScannerActive(true));
|
||||
builder.show();
|
||||
});
|
||||
|
||||
// Configure barcodeScanner
|
||||
barcodeScannerView = binding.zxingBarcodeScanner;
|
||||
Intent barcodeScannerIntent = new Intent();
|
||||
Bundle barcodeScannerIntentBundle = new Bundle();
|
||||
barcodeScannerIntentBundle.putBoolean(DecodeHintType.ALSO_INVERTED.name(), Boolean.TRUE);
|
||||
barcodeScannerIntent.putExtras(barcodeScannerIntentBundle);
|
||||
barcodeScannerView.initializeFromIntent(barcodeScannerIntent);
|
||||
|
||||
// Even though we do the actual decoding with the barcodeScannerView
|
||||
// CaptureManager needs to be running to show the camera and scanning bar
|
||||
capture = new CatimaCaptureManager(this, barcodeScannerView, this::onCaptureManagerError);
|
||||
Intent captureIntent = new Intent();
|
||||
Bundle captureIntentBundle = new Bundle();
|
||||
captureIntentBundle.putBoolean(Intents.Scan.BEEP_ENABLED, false);
|
||||
captureIntent.putExtras(captureIntentBundle);
|
||||
capture.initializeFromIntent(captureIntent, savedInstanceState);
|
||||
|
||||
barcodeScannerView.decodeSingle(new BarcodeCallback() {
|
||||
@Override
|
||||
public void barcodeResult(BarcodeResult result) {
|
||||
LoyaltyCard loyaltyCard = new LoyaltyCard();
|
||||
loyaltyCard.setCardId(result.getText());
|
||||
loyaltyCard.setBarcodeType(CatimaBarcode.fromBarcode(result.getBarcodeFormat()));
|
||||
|
||||
returnResult(new ParseResult(ParseResultType.BARCODE_ONLY, loyaltyCard));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void possibleResultPoints(List<ResultPoint> resultPoints) {
|
||||
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
|
||||
if (mScannerActive) {
|
||||
capture.onResume();
|
||||
}
|
||||
|
||||
if (!Utils.deviceHasCamera(this)) {
|
||||
showCameraError(getString(R.string.noCameraFoundGuideText), false);
|
||||
} else if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {
|
||||
showCameraPermissionMissingText();
|
||||
} else {
|
||||
hideCameraError();
|
||||
}
|
||||
|
||||
scaleScreen();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPause() {
|
||||
super.onPause();
|
||||
capture.onPause();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
capture.onDestroy();
|
||||
}
|
||||
|
||||
@Override
|
||||
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
|
||||
public boolean onKeyDown(int keyCode, KeyEvent event) {
|
||||
return barcodeScannerView.onKeyDown(keyCode, event) || super.onKeyDown(keyCode, event);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
if (getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA_FLASH)) {
|
||||
getMenuInflater().inflate(R.menu.scan_menu, menu);
|
||||
}
|
||||
|
||||
barcodeScannerView.setTorchOff();
|
||||
|
||||
return super.onCreateOptionsMenu(menu);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
if (item.getItemId() == android.R.id.home) {
|
||||
setResult(Activity.RESULT_CANCELED);
|
||||
finish();
|
||||
return true;
|
||||
} else if (item.getItemId() == R.id.action_toggle_flashlight) {
|
||||
if (torch) {
|
||||
torch = false;
|
||||
barcodeScannerView.setTorchOff();
|
||||
item.setTitle(R.string.turn_flashlight_on);
|
||||
item.setIcon(R.drawable.ic_flashlight_off_white_24dp);
|
||||
} else {
|
||||
torch = true;
|
||||
barcodeScannerView.setTorchOn();
|
||||
item.setTitle(R.string.turn_flashlight_off);
|
||||
item.setIcon(R.drawable.ic_flashlight_on_white_24dp);
|
||||
}
|
||||
}
|
||||
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
private void setScannerActive(boolean isActive) {
|
||||
if (isActive) {
|
||||
barcodeScannerView.resume();
|
||||
} else {
|
||||
barcodeScannerView.pause();
|
||||
}
|
||||
mScannerActive = isActive;
|
||||
}
|
||||
|
||||
private void returnResult(ParseResult parseResult) {
|
||||
Intent result = new Intent();
|
||||
Bundle bundle = parseResult.toLoyaltyCardBundle(ScanActivity.this);
|
||||
if (addGroup != null) {
|
||||
bundle.putString(LoyaltyCardEditActivity.BUNDLE_ADDGROUP, addGroup);
|
||||
}
|
||||
result.putExtras(bundle);
|
||||
ScanActivity.this.setResult(RESULT_OK, result);
|
||||
finish();
|
||||
}
|
||||
|
||||
private void handleActivityResult(int requestCode, int resultCode, Intent intent) {
|
||||
super.onActivityResult(requestCode, resultCode, intent);
|
||||
|
||||
List<ParseResult> parseResultList = Utils.parseSetBarcodeActivityResult(requestCode, resultCode, intent, this);
|
||||
|
||||
if (parseResultList.isEmpty()) {
|
||||
setScannerActive(true);
|
||||
return;
|
||||
}
|
||||
|
||||
Utils.makeUserChooseParseResultFromList(this, parseResultList, new ParseResultListDisambiguatorCallback() {
|
||||
@Override
|
||||
public void onUserChoseParseResult(ParseResult parseResult) {
|
||||
returnResult(parseResult);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUserDismissedSelector() {
|
||||
setScannerActive(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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) -> {
|
||||
LoyaltyCard loyaltyCard = new LoyaltyCard();
|
||||
loyaltyCard.setCardId(input.getText().toString());
|
||||
returnResult(new ParseResult(ParseResultType.BARCODE_ONLY, loyaltyCard));
|
||||
});
|
||||
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() {
|
||||
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(ScanActivity.this);
|
||||
builder.setTitle(R.string.add_manually_warning_title);
|
||||
builder.setMessage(R.string.add_manually_warning_message);
|
||||
builder.setPositiveButton(R.string.continue_, (dialog, which) -> {
|
||||
Intent i = new Intent(getApplicationContext(), BarcodeSelectorActivity.class);
|
||||
if (cardId != null) {
|
||||
final Bundle b = new Bundle();
|
||||
b.putString(LoyaltyCard.BUNDLE_LOYALTY_CARD_CARD_ID, cardId);
|
||||
i.putExtras(b);
|
||||
}
|
||||
manualAddLauncher.launch(i);
|
||||
});
|
||||
builder.setNegativeButton(R.string.cancel, (dialog, which) -> setScannerActive(true));
|
||||
builder.setOnCancelListener(dialog -> setScannerActive(true));
|
||||
builder.show();
|
||||
}
|
||||
|
||||
public void addFromImage() {
|
||||
PermissionUtils.requestStorageReadPermission(this, PERMISSION_SCAN_ADD_FROM_IMAGE);
|
||||
}
|
||||
|
||||
public void addFromPdf() {
|
||||
PermissionUtils.requestStorageReadPermission(this, PERMISSION_SCAN_ADD_FROM_PDF);
|
||||
}
|
||||
|
||||
public void addFromPkPass() {
|
||||
PermissionUtils.requestStorageReadPermission(this, PERMISSION_SCAN_ADD_FROM_PKPASS);
|
||||
}
|
||||
|
||||
private void addFromImageOrFileAfterPermission(String mimeType, ActivityResultLauncher<Intent> launcher, int chooserText, int errorMessage) {
|
||||
Intent photoPickerIntent = new Intent(Intent.ACTION_PICK);
|
||||
photoPickerIntent.setType(mimeType);
|
||||
Intent contentIntent = new Intent(Intent.ACTION_GET_CONTENT);
|
||||
contentIntent.setType(mimeType);
|
||||
|
||||
Intent chooserIntent = Intent.createChooser(photoPickerIntent, getString(chooserText));
|
||||
chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, new Intent[] { contentIntent });
|
||||
try {
|
||||
launcher.launch(chooserIntent);
|
||||
} catch (ActivityNotFoundException e) {
|
||||
setScannerActive(true);
|
||||
Toast.makeText(getApplicationContext(), errorMessage, Toast.LENGTH_LONG).show();
|
||||
Log.e(TAG, "No activity found to handle intent", e);
|
||||
}
|
||||
}
|
||||
|
||||
public void onCaptureManagerError(String errorMessage) {
|
||||
if (mHasError) {
|
||||
// We're already showing an error, ignore this new error
|
||||
return;
|
||||
}
|
||||
|
||||
showCameraError(errorMessage, false);
|
||||
}
|
||||
|
||||
private void showCameraPermissionMissingText() {
|
||||
showCameraError(getString(R.string.noCameraPermissionDirectToSystemSetting), true);
|
||||
}
|
||||
|
||||
private void showCameraError(String message, boolean setOnClick) {
|
||||
customBarcodeScannerBinding.cameraErrorLayout.cameraErrorMessage.setText(message);
|
||||
|
||||
setCameraErrorState(true, setOnClick);
|
||||
}
|
||||
|
||||
private void hideCameraError() {
|
||||
setCameraErrorState(false, false);
|
||||
}
|
||||
|
||||
private void setCameraErrorState(boolean visible, boolean setOnClick) {
|
||||
mHasError = visible;
|
||||
|
||||
customBarcodeScannerBinding.cameraErrorLayout.cameraErrorClickableArea.setOnClickListener(visible && setOnClick ? v -> {
|
||||
navigateToSystemPermissionSetting();
|
||||
} : null);
|
||||
customBarcodeScannerBinding.cardInputContainer.setBackgroundColor(visible ? obtainThemeAttribute(com.google.android.material.R.attr.colorSurface) : Color.TRANSPARENT);
|
||||
customBarcodeScannerBinding.cameraErrorLayout.getRoot().setVisibility(visible ? View.VISIBLE : View.GONE);
|
||||
}
|
||||
|
||||
private void scaleScreen() {
|
||||
DisplayMetrics displayMetrics = new DisplayMetrics();
|
||||
getWindowManager().getDefaultDisplay().getMetrics(displayMetrics);
|
||||
int screenHeight = displayMetrics.heightPixels;
|
||||
float mediumSizePx = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,MEDIUM_SCALE_FACTOR_DIP,getResources().getDisplayMetrics());
|
||||
boolean shouldScaleSmaller = screenHeight < mediumSizePx;
|
||||
|
||||
customBarcodeScannerBinding.cameraErrorLayout.cameraErrorIcon.setVisibility(shouldScaleSmaller ? View.GONE : View.VISIBLE);
|
||||
customBarcodeScannerBinding.cameraErrorLayout.cameraErrorTitle.setVisibility(shouldScaleSmaller ? View.GONE : View.VISIBLE);
|
||||
}
|
||||
|
||||
private int obtainThemeAttribute(int attribute) {
|
||||
TypedValue typedValue = new TypedValue();
|
||||
getTheme().resolveAttribute(attribute, typedValue, true);
|
||||
return typedValue.data;
|
||||
}
|
||||
|
||||
private void navigateToSystemPermissionSetting() {
|
||||
Intent permissionIntent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, Uri.fromParts("package", getPackageName(), null));
|
||||
permissionIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
startActivity(permissionIntent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
||||
|
||||
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()) {
|
||||
if (granted) {
|
||||
hideCameraError();
|
||||
} else {
|
||||
showCameraPermissionMissingText();
|
||||
}
|
||||
} else if (requestCode == PERMISSION_SCAN_ADD_FROM_IMAGE || requestCode == PERMISSION_SCAN_ADD_FROM_PDF || requestCode == PERMISSION_SCAN_ADD_FROM_PKPASS) {
|
||||
if (granted) {
|
||||
if (requestCode == PERMISSION_SCAN_ADD_FROM_IMAGE) {
|
||||
addFromImageOrFileAfterPermission("image/*", photoPickerLauncher, R.string.addFromImage, R.string.failedLaunchingPhotoPicker);
|
||||
} else if (requestCode == PERMISSION_SCAN_ADD_FROM_PDF) {
|
||||
addFromImageOrFileAfterPermission("application/pdf", pdfPickerLauncher, R.string.addFromPdfFile, R.string.failedLaunchingFileManager);
|
||||
} else {
|
||||
addFromImageOrFileAfterPermission("application/*", pkpassPickerLauncher, R.string.addFromPkpass, R.string.failedLaunchingFileManager);
|
||||
}
|
||||
} else {
|
||||
setScannerActive(true);
|
||||
Toast.makeText(this, R.string.storageReadPermissionRequired, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,591 @@
|
||||
package protect.card_locker
|
||||
|
||||
|
||||
import android.Manifest
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
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
|
||||
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.ListAdapter
|
||||
import android.widget.SimpleAdapter
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.widget.doOnTextChanged
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.zxing.DecodeHintType
|
||||
import com.google.zxing.ResultPoint
|
||||
import com.journeyapps.barcodescanner.BarcodeCallback
|
||||
import com.journeyapps.barcodescanner.BarcodeResult
|
||||
import com.journeyapps.barcodescanner.CaptureManager
|
||||
import com.journeyapps.barcodescanner.DecoratedBarcodeView
|
||||
import protect.card_locker.databinding.CustomBarcodeScannerBinding
|
||||
import protect.card_locker.databinding.ScanActivityBinding
|
||||
|
||||
/**
|
||||
* Custom Scannner Activity extending from Activity to display a custom layout form scanner view.
|
||||
* <p>
|
||||
* Based on https://github.com/journeyapps/zxing-android-embedded/blob/0fdfbce9fb3285e985bad9971c5f7c0a7a334e7b/sample/src/main/java/example/zxing/CustomScannerActivity.java
|
||||
* originally licensed under Apache 2.0
|
||||
*/
|
||||
class ScanActivity : CatimaAppCompatActivity() {
|
||||
private lateinit var binding: ScanActivityBinding
|
||||
private lateinit var customBarcodeScannerBinding: CustomBarcodeScannerBinding
|
||||
|
||||
companion object {
|
||||
private const val TAG = "Catima"
|
||||
|
||||
private const val MEDIUM_SCALE_FACTOR_DIP = 460
|
||||
private const val COMPAT_SCALE_FACTOR_DIP = 320
|
||||
|
||||
private const val PERMISSION_SCAN_ADD_FROM_IMAGE = 100
|
||||
private const val PERMISSION_SCAN_ADD_FROM_PDF = 101
|
||||
private const val PERMISSION_SCAN_ADD_FROM_PKPASS = 102
|
||||
|
||||
private const val STATE_SCANNER_ACTIVE = "scannerActive"
|
||||
}
|
||||
|
||||
private lateinit var capture: CaptureManager
|
||||
private lateinit var barcodeScannerView: DecoratedBarcodeView
|
||||
private var cardId: String? = null
|
||||
private var addGroup: String? = null
|
||||
private var torch = false
|
||||
|
||||
private lateinit var manualAddLauncher: ActivityResultLauncher<Intent>
|
||||
// can't use the pre-made contract because that launches the file manager for image type instead of gallery
|
||||
private lateinit var photoPickerLauncher: ActivityResultLauncher<Intent>
|
||||
private lateinit var pdfPickerLauncher: ActivityResultLauncher<Intent>
|
||||
private lateinit var pkpassPickerLauncher: ActivityResultLauncher<Intent>
|
||||
|
||||
private var mScannerActive = true
|
||||
private var mHasError = false
|
||||
|
||||
private fun extractIntentFields(intent: Intent) {
|
||||
val b = intent.extras
|
||||
cardId = b?.getString(LoyaltyCard.BUNDLE_LOYALTY_CARD_CARD_ID)
|
||||
addGroup = b?.getString(LoyaltyCardEditActivity.BUNDLE_ADDGROUP)
|
||||
Log.d(TAG, "Scan activity: id=$cardId")
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = ScanActivityBinding.inflate(layoutInflater)
|
||||
customBarcodeScannerBinding = CustomBarcodeScannerBinding.bind(binding.zxingBarcodeScanner)
|
||||
setTitle(R.string.scanCardBarcode)
|
||||
setContentView(binding.root)
|
||||
Utils.applyWindowInsets(binding.root)
|
||||
setSupportActionBar(binding.toolbar)
|
||||
enableToolbarBackButton()
|
||||
|
||||
extractIntentFields(intent)
|
||||
|
||||
manualAddLauncher =
|
||||
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||
handleActivityResult(
|
||||
Utils.SELECT_BARCODE_REQUEST,
|
||||
result.resultCode,
|
||||
result.data
|
||||
)
|
||||
}
|
||||
photoPickerLauncher =
|
||||
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||
handleActivityResult(
|
||||
Utils.BARCODE_IMPORT_FROM_IMAGE_FILE,
|
||||
result.resultCode,
|
||||
result.data
|
||||
)
|
||||
}
|
||||
pdfPickerLauncher =
|
||||
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||
handleActivityResult(
|
||||
Utils.BARCODE_IMPORT_FROM_PDF_FILE,
|
||||
result.resultCode,
|
||||
result.data
|
||||
)
|
||||
}
|
||||
pkpassPickerLauncher =
|
||||
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||
handleActivityResult(
|
||||
Utils.BARCODE_IMPORT_FROM_PKPASS_FILE,
|
||||
result.resultCode,
|
||||
result.data
|
||||
)
|
||||
}
|
||||
|
||||
customBarcodeScannerBinding.fabOtherOptions.setOnClickListener {
|
||||
setScannerActive(false)
|
||||
|
||||
val list: ArrayList<HashMap<String, Any>> = arrayListOf()
|
||||
val texts = arrayOf(
|
||||
getString(R.string.addWithoutBarcode),
|
||||
getString(R.string.addManually),
|
||||
getString(R.string.addFromImage),
|
||||
getString(R.string.addFromPdfFile),
|
||||
getString(R.string.addFromPkpass)
|
||||
)
|
||||
val icons = arrayOf(
|
||||
R.drawable.baseline_block_24,
|
||||
R.drawable.ic_edit,
|
||||
R.drawable.baseline_image_24,
|
||||
R.drawable.baseline_picture_as_pdf_24,
|
||||
R.drawable.local_activity_24px
|
||||
)
|
||||
val columns = arrayOf("text", "icon")
|
||||
|
||||
for (i in 0 until texts.size) {
|
||||
val map: HashMap<String, Any> = hashMapOf()
|
||||
map.put(columns[0], texts[i])
|
||||
map.put(columns[1], icons[i])
|
||||
list.add(map)
|
||||
}
|
||||
|
||||
val adapter: ListAdapter = SimpleAdapter(
|
||||
this,
|
||||
list,
|
||||
R.layout.alertdialog_row_with_icon,
|
||||
columns,
|
||||
intArrayOf(R.id.textView, R.id.imageView)
|
||||
)
|
||||
|
||||
val builder = MaterialAlertDialogBuilder(this).apply {
|
||||
setTitle(getString(R.string.add_a_card_in_a_different_way))
|
||||
setAdapter(adapter) { _, i ->
|
||||
when (i) {
|
||||
0 -> addWithoutBarcode()
|
||||
1 -> addManually()
|
||||
2 -> addFromImage()
|
||||
3 -> addFromPdf()
|
||||
4 -> addFromPkPass()
|
||||
else -> throw IllegalArgumentException(
|
||||
"Unknown 'Add a card in a different way' dialog option: $i"
|
||||
)
|
||||
}
|
||||
}
|
||||
setOnCancelListener { _ -> setScannerActive(true) }
|
||||
}
|
||||
builder.show()
|
||||
}
|
||||
|
||||
// Configure barcodeScanner
|
||||
barcodeScannerView = binding.zxingBarcodeScanner
|
||||
|
||||
val barcodeScannerIntent = Intent().apply {
|
||||
val barcodeScannerIntentBundle = Bundle().apply {
|
||||
putBoolean(DecodeHintType.ALSO_INVERTED.name, true)
|
||||
}
|
||||
putExtras(barcodeScannerIntentBundle)
|
||||
}
|
||||
barcodeScannerView.initializeFromIntent(barcodeScannerIntent)
|
||||
|
||||
// Even though we do the actual decoding with the barcodeScannerView
|
||||
// CaptureManager needs to be running to show the camera and scanning bar
|
||||
capture = CatimaCaptureManager(this, barcodeScannerView, this::onCaptureManagerError)
|
||||
val captureIntent = Intent().apply {
|
||||
val captureIntentBundle = Bundle().apply {
|
||||
putBoolean(DecodeHintType.ALSO_INVERTED.name, false)
|
||||
}
|
||||
putExtras(captureIntentBundle)
|
||||
}
|
||||
capture.initializeFromIntent(captureIntent, savedInstanceState)
|
||||
|
||||
barcodeScannerView.decodeSingle(object : BarcodeCallback {
|
||||
override fun barcodeResult(result: BarcodeResult) {
|
||||
val loyaltyCard = LoyaltyCard().apply {
|
||||
setCardId(result.text)
|
||||
setBarcodeType(CatimaBarcode.fromBarcode(result.barcodeFormat))
|
||||
}
|
||||
|
||||
returnResult(ParseResult(ParseResultType.BARCODE_ONLY, loyaltyCard))
|
||||
}
|
||||
|
||||
override fun possibleResultPoints(resultPoints: List<ResultPoint?>?) {}
|
||||
})
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
if (mScannerActive) {
|
||||
capture.onResume()
|
||||
}
|
||||
|
||||
if (!Utils.deviceHasCamera(this)) {
|
||||
showCameraError(getString(R.string.noCameraFoundGuideText), false)
|
||||
} else if (ContextCompat.checkSelfPermission(
|
||||
this,
|
||||
Manifest.permission.CAMERA
|
||||
) != PackageManager.PERMISSION_GRANTED
|
||||
) {
|
||||
showCameraPermissionMissingText()
|
||||
} else {
|
||||
hideCameraError()
|
||||
}
|
||||
|
||||
scaleScreen()
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
capture.onPause()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
capture.onDestroy()
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(savedInstanceState: Bundle) {
|
||||
super.onSaveInstanceState(savedInstanceState)
|
||||
capture.onSaveInstanceState(savedInstanceState)
|
||||
|
||||
savedInstanceState.putBoolean(STATE_SCANNER_ACTIVE, mScannerActive)
|
||||
}
|
||||
|
||||
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
|
||||
super.onRestoreInstanceState(savedInstanceState)
|
||||
|
||||
mScannerActive = savedInstanceState.getBoolean(STATE_SCANNER_ACTIVE)
|
||||
}
|
||||
|
||||
override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean {
|
||||
return barcodeScannerView.onKeyDown(keyCode, event) || super.onKeyDown(keyCode, event)
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||
if (packageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA_FLASH)) {
|
||||
menuInflater.inflate(R.menu.scan_menu, menu)
|
||||
}
|
||||
|
||||
barcodeScannerView.setTorchOff()
|
||||
|
||||
return super.onCreateOptionsMenu(menu)
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
if (item.itemId == android.R.id.home) {
|
||||
setResult(RESULT_CANCELED)
|
||||
finish()
|
||||
return true
|
||||
} else if (item.itemId == R.id.action_toggle_flashlight) {
|
||||
if (torch) {
|
||||
torch = false
|
||||
barcodeScannerView.setTorchOff()
|
||||
item.setTitle(R.string.turn_flashlight_on)
|
||||
item.setIcon(R.drawable.ic_flashlight_off_white_24dp)
|
||||
} else {
|
||||
torch = true
|
||||
barcodeScannerView.setTorchOn()
|
||||
item.setTitle(R.string.turn_flashlight_off)
|
||||
item.setIcon(R.drawable.ic_flashlight_on_white_24dp)
|
||||
}
|
||||
}
|
||||
|
||||
return super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
private fun setScannerActive(isActive: Boolean) {
|
||||
if (isActive) {
|
||||
barcodeScannerView.resume()
|
||||
} else {
|
||||
barcodeScannerView.pause()
|
||||
}
|
||||
mScannerActive = isActive
|
||||
}
|
||||
|
||||
private fun returnResult(parseResult: ParseResult) {
|
||||
val bundle = parseResult.toLoyaltyCardBundle(this).apply {
|
||||
addGroup?.let { putString(LoyaltyCardEditActivity.BUNDLE_ADDGROUP, it) }
|
||||
}
|
||||
val result = Intent().apply { putExtras(bundle) }
|
||||
this.setResult(RESULT_OK, result)
|
||||
finish()
|
||||
}
|
||||
|
||||
private fun handleActivityResult(requestCode: Int, resultCode: Int, intent: Intent?) {
|
||||
super.onActivityResult(resultCode, resultCode, intent)
|
||||
|
||||
val parseResultList: List<ParseResult> =
|
||||
Utils.parseSetBarcodeActivityResult(requestCode, resultCode, intent, this)
|
||||
|
||||
if (parseResultList.isEmpty()) {
|
||||
setScannerActive(true)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
Utils.makeUserChooseParseResultFromList(
|
||||
this,
|
||||
parseResultList,
|
||||
object : ParseResultListDisambiguatorCallback {
|
||||
override fun onUserChoseParseResult(parseResult: ParseResult) {
|
||||
returnResult(parseResult)
|
||||
}
|
||||
|
||||
override fun onUserDismissedSelector() {
|
||||
setScannerActive(true)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun addWithoutBarcode() {
|
||||
val builder: AlertDialog.Builder = MaterialAlertDialogBuilder(this).apply {
|
||||
setOnCancelListener { dialogInterface -> setScannerActive(true) }
|
||||
// Header
|
||||
setTitle(R.string.addWithoutBarcode)
|
||||
}
|
||||
|
||||
// Layout
|
||||
val layout = LinearLayout(this).apply {
|
||||
orientation = LinearLayout.VERTICAL
|
||||
}
|
||||
val contentPadding = resources.getDimensionPixelSize(R.dimen.alert_dialog_content_padding)
|
||||
val params = LinearLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
).apply {
|
||||
leftMargin = contentPadding
|
||||
topMargin = contentPadding / 2
|
||||
rightMargin = contentPadding
|
||||
}
|
||||
|
||||
// Description
|
||||
val currentTextview = TextView(this).apply {
|
||||
text = getString(R.string.enter_card_id)
|
||||
layoutParams = params
|
||||
}
|
||||
layout.addView(currentTextview)
|
||||
|
||||
//EditText with spacing
|
||||
val input = EditText(this).apply {
|
||||
inputType = InputType.TYPE_CLASS_TEXT
|
||||
layoutParams = params
|
||||
}
|
||||
layout.addView(input)
|
||||
|
||||
// Set layout
|
||||
builder.setView(layout).apply {
|
||||
|
||||
setPositiveButton(getString(R.string.ok)) { _, _ ->
|
||||
val loyaltyCard = LoyaltyCard()
|
||||
loyaltyCard.cardId = input.text.toString()
|
||||
returnResult(ParseResult(ParseResultType.BARCODE_ONLY, loyaltyCard))
|
||||
}
|
||||
setNegativeButton(getString(R.string.cancel)) { dialog, _ ->
|
||||
dialog.cancel()
|
||||
}
|
||||
}
|
||||
val dialog: AlertDialog = builder.create()
|
||||
|
||||
// Now that the dialog exists, we can bind something that affects the OK button
|
||||
input.doOnTextChanged { text, _, _, _ ->
|
||||
if (text.isNullOrEmpty()) {
|
||||
input.error = getString(R.string.card_id_must_not_be_empty)
|
||||
dialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = false
|
||||
} else {
|
||||
input.error = null
|
||||
dialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = true
|
||||
}
|
||||
}
|
||||
|
||||
dialog.show()
|
||||
|
||||
// Disable button (must be done **after** dialog is shown to prevent crash
|
||||
dialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = false
|
||||
// Set focus on input field
|
||||
dialog.window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE)
|
||||
input.requestFocus()
|
||||
}
|
||||
|
||||
fun addManually() {
|
||||
val builder = MaterialAlertDialogBuilder(this).apply {
|
||||
setTitle(R.string.add_manually_warning_title)
|
||||
setMessage(R.string.add_manually_warning_message)
|
||||
setPositiveButton(R.string.continue_) { _, _ ->
|
||||
val i = Intent(applicationContext, BarcodeSelectorActivity::class.java)
|
||||
if (cardId != null) {
|
||||
val b = Bundle()
|
||||
b.putString(LoyaltyCard.BUNDLE_LOYALTY_CARD_CARD_ID, cardId)
|
||||
i.putExtras(b)
|
||||
}
|
||||
manualAddLauncher.launch(i)
|
||||
}
|
||||
setNegativeButton(R.string.cancel) { _, _ -> setScannerActive(true) }
|
||||
setOnCancelListener { _ -> setScannerActive(true) }
|
||||
}
|
||||
builder.show()
|
||||
}
|
||||
|
||||
fun addFromImage() {
|
||||
PermissionUtils.requestStorageReadPermission(this, PERMISSION_SCAN_ADD_FROM_IMAGE)
|
||||
}
|
||||
|
||||
fun addFromPdf() {
|
||||
PermissionUtils.requestStorageReadPermission(this, PERMISSION_SCAN_ADD_FROM_PDF)
|
||||
}
|
||||
|
||||
fun addFromPkPass() {
|
||||
PermissionUtils.requestStorageReadPermission(this, PERMISSION_SCAN_ADD_FROM_PKPASS)
|
||||
}
|
||||
|
||||
private fun addFromImageOrFileAfterPermission(
|
||||
mimeType: String,
|
||||
launcher: ActivityResultLauncher<Intent>,
|
||||
chooserText: Int,
|
||||
errorMessage: Int
|
||||
) {
|
||||
val photoPickerIntent = Intent(Intent.ACTION_PICK)
|
||||
photoPickerIntent.type = mimeType
|
||||
val contentIntent = Intent(Intent.ACTION_GET_CONTENT)
|
||||
contentIntent.type = mimeType
|
||||
|
||||
val chooserIntent = Intent.createChooser(photoPickerIntent, getString(chooserText))
|
||||
chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, arrayOf(contentIntent))
|
||||
try {
|
||||
launcher.launch(chooserIntent)
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
setScannerActive(true)
|
||||
Toast.makeText(applicationContext, errorMessage, Toast.LENGTH_LONG).show()
|
||||
Log.e(TAG, "No activity found to handle intent", e)
|
||||
}
|
||||
}
|
||||
|
||||
fun onCaptureManagerError(errorMessage: String) {
|
||||
if (mHasError) {
|
||||
// We're already showing an error, ignore this new error
|
||||
return
|
||||
}
|
||||
|
||||
showCameraError(errorMessage, false)
|
||||
}
|
||||
|
||||
private fun showCameraPermissionMissingText() {
|
||||
showCameraError(getString(R.string.noCameraPermissionDirectToSystemSetting), true)
|
||||
}
|
||||
|
||||
private fun showCameraError(message: String, setOnClick: Boolean) {
|
||||
customBarcodeScannerBinding.cameraErrorLayout.cameraErrorMessage.text = message
|
||||
|
||||
setCameraErrorState(true, setOnClick)
|
||||
}
|
||||
|
||||
private fun hideCameraError() {
|
||||
setCameraErrorState(false, false)
|
||||
}
|
||||
|
||||
private fun setCameraErrorState(visible: Boolean, setOnClick: Boolean) {
|
||||
mHasError = visible
|
||||
customBarcodeScannerBinding.cameraErrorLayout.cameraErrorClickableArea.setOnClickListener(
|
||||
if (visible && setOnClick) { _ -> navigateToSystemPermissionSetting() }
|
||||
else null
|
||||
)
|
||||
customBarcodeScannerBinding.cardInputContainer.setBackgroundColor(
|
||||
if (visible) obtainThemeAttribute(com.google.android.material.R.attr.colorSurface)
|
||||
else Color.TRANSPARENT
|
||||
)
|
||||
customBarcodeScannerBinding.cameraErrorLayout.root.visibility =
|
||||
if (visible) View.VISIBLE else View.GONE
|
||||
}
|
||||
|
||||
private fun scaleScreen() {
|
||||
val displayMetrics = DisplayMetrics()
|
||||
windowManager.defaultDisplay.getMetrics(displayMetrics)
|
||||
val screenHeight: Int = displayMetrics.heightPixels
|
||||
val mediumSizePx: Float = TypedValue.applyDimension(
|
||||
TypedValue.COMPLEX_UNIT_DIP,
|
||||
MEDIUM_SCALE_FACTOR_DIP.toFloat(),
|
||||
resources.displayMetrics
|
||||
)
|
||||
val shouldScaleSmaller = screenHeight < mediumSizePx
|
||||
|
||||
customBarcodeScannerBinding.cameraErrorLayout.cameraErrorIcon.visibility =
|
||||
if (shouldScaleSmaller) View.GONE else View.VISIBLE
|
||||
customBarcodeScannerBinding.cameraErrorLayout.cameraErrorTitle.visibility =
|
||||
if (shouldScaleSmaller) View.GONE else View.VISIBLE
|
||||
}
|
||||
|
||||
private fun obtainThemeAttribute(attribute: Int): Int {
|
||||
val typedValue = TypedValue()
|
||||
theme.resolveAttribute(attribute, typedValue, true)
|
||||
return typedValue.data
|
||||
}
|
||||
|
||||
private fun navigateToSystemPermissionSetting() {
|
||||
val permissionIntent = Intent(
|
||||
Settings.ACTION_APPLICATION_DETAILS_SETTINGS,
|
||||
Uri.fromParts("package", getPackageName(), null)
|
||||
)
|
||||
permissionIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
startActivity(permissionIntent)
|
||||
}
|
||||
|
||||
override fun onRequestPermissionsResult(
|
||||
requestCode: Int,
|
||||
permissions: Array<String>,
|
||||
grantResults: IntArray
|
||||
) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
||||
|
||||
val granted =
|
||||
grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED
|
||||
|
||||
if (requestCode == CaptureManager.getCameraPermissionReqCode()) {
|
||||
if (granted) {
|
||||
hideCameraError()
|
||||
} else {
|
||||
showCameraPermissionMissingText()
|
||||
}
|
||||
} else if (requestCode in listOf(
|
||||
PERMISSION_SCAN_ADD_FROM_IMAGE,
|
||||
PERMISSION_SCAN_ADD_FROM_PDF,
|
||||
PERMISSION_SCAN_ADD_FROM_PKPASS
|
||||
)
|
||||
) {
|
||||
if (granted) {
|
||||
if (requestCode == PERMISSION_SCAN_ADD_FROM_IMAGE) {
|
||||
addFromImageOrFileAfterPermission(
|
||||
"image/*",
|
||||
photoPickerLauncher,
|
||||
R.string.addFromImage,
|
||||
R.string.failedLaunchingPhotoPicker
|
||||
)
|
||||
} else if (requestCode == PERMISSION_SCAN_ADD_FROM_PDF) {
|
||||
addFromImageOrFileAfterPermission(
|
||||
"application/pdf",
|
||||
pdfPickerLauncher,
|
||||
R.string.addFromPdfFile,
|
||||
R.string.failedLaunchingFileManager
|
||||
)
|
||||
} else {
|
||||
addFromImageOrFileAfterPermission(
|
||||
"application/*",
|
||||
pkpassPickerLauncher,
|
||||
R.string.addFromPkpass,
|
||||
R.string.failedLaunchingFileManager
|
||||
)
|
||||
}
|
||||
} else {
|
||||
setScannerActive(true)
|
||||
Toast.makeText(this, R.string.storageReadPermissionRequired, Toast.LENGTH_LONG)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,31 +2,35 @@ package protect.card_locker;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.database.Cursor;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
import androidx.core.content.pm.ShortcutInfoCompat;
|
||||
import androidx.core.content.pm.ShortcutManagerCompat;
|
||||
import androidx.core.graphics.ColorUtils;
|
||||
import androidx.core.graphics.drawable.IconCompat;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
class ShortcutHelper {
|
||||
// Android documentation says that no more than 5 shortcuts
|
||||
// are supported. However, that may be too many, as not all
|
||||
// launcher will show all 5. Instead, the number is limited
|
||||
// to 3 here, so that the most recent shortcut has a good
|
||||
// chance of being shown.
|
||||
private static final int MAX_SHORTCUTS = 3;
|
||||
import protect.card_locker.cardview.LoyaltyCardViewActivity;
|
||||
|
||||
public class ShortcutHelper {
|
||||
/**
|
||||
* This variable controls the maximum number of shortcuts available.
|
||||
* It is made public only to make testing easier and should not be
|
||||
* manually modified. We use -1 here as a default value to check if
|
||||
* the value has been set either manually by the test scenario or
|
||||
* automatically in the `updateShortcuts` function.
|
||||
* Its actual value will be set based on the maximum amount of shortcuts
|
||||
* declared by the launcher via `getMaxShortcutCountPerActivity`.
|
||||
*/
|
||||
@VisibleForTesting
|
||||
public static int maxShortcuts = -1;
|
||||
|
||||
// https://developer.android.com/reference/android/graphics/drawable/AdaptiveIconDrawable.html
|
||||
private static final int ADAPTIVE_BITMAP_SCALE = 1;
|
||||
@@ -35,86 +39,45 @@ class ShortcutHelper {
|
||||
private static final int ADAPTIVE_BITMAP_IMAGE_SIZE = ADAPTIVE_BITMAP_VISIBLE_SIZE + 5 * ADAPTIVE_BITMAP_SCALE;
|
||||
|
||||
/**
|
||||
* Add a card to the app shortcuts, and maintain a list of the most
|
||||
* recently used cards. If there is already a shortcut for the card,
|
||||
* the card is marked as the most recently used card. If adding this
|
||||
* card exceeds the max number of shortcuts, then the least recently
|
||||
* used card shortcut is discarded.
|
||||
* Update the dynamic shortcut list with the most recently viewed cards
|
||||
* based on the lastUsed field. Archived cards are excluded from the shortcuts
|
||||
* list. The list keeps at most maxShortcuts number of elements.
|
||||
*/
|
||||
static void updateShortcuts(Context context, LoyaltyCard card) {
|
||||
if (card.archiveStatus == 1) {
|
||||
// Don't add archived card to menu
|
||||
return;
|
||||
public static void updateShortcuts(Context context) {
|
||||
if (maxShortcuts == -1) {
|
||||
maxShortcuts = ShortcutManagerCompat.getMaxShortcutCountPerActivity(context);
|
||||
}
|
||||
|
||||
LinkedList<ShortcutInfoCompat> list = new LinkedList<>(ShortcutManagerCompat.getDynamicShortcuts(context));
|
||||
|
||||
SQLiteDatabase database = new DBHelper(context).getReadableDatabase();
|
||||
|
||||
String shortcutId = Integer.toString(card.id);
|
||||
|
||||
// Sort the shortcuts by rank, so working with the relative order will be easier.
|
||||
// This sorts so that the lowest rank is first.
|
||||
Collections.sort(list, Comparator.comparingInt(ShortcutInfoCompat::getRank));
|
||||
|
||||
Integer foundIndex = null;
|
||||
|
||||
for (int index = 0; index < list.size(); index++) {
|
||||
if (list.get(index).getId().equals(shortcutId)) {
|
||||
// Found the item already
|
||||
foundIndex = index;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (foundIndex != null) {
|
||||
// If the item is already found, then the list needs to be
|
||||
// reordered, so that the selected item now has the lowest
|
||||
// rank, thus letting it survive longer.
|
||||
ShortcutInfoCompat found = list.remove(foundIndex.intValue());
|
||||
list.addFirst(found);
|
||||
} else {
|
||||
// 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;
|
||||
DBHelper dbHelper = new DBHelper(context);
|
||||
SQLiteDatabase database = dbHelper.getReadableDatabase();
|
||||
try (Cursor loyaltyCardCursor = DBHelper.getLoyaltyCardCursor(
|
||||
database,
|
||||
"",
|
||||
null,
|
||||
DBHelper.LoyaltyCardOrder.LastUsed,
|
||||
DBHelper.LoyaltyCardOrderDirection.Ascending,
|
||||
DBHelper.LoyaltyCardArchiveFilter.Unarchived
|
||||
)) {
|
||||
int rank = 0;
|
||||
|
||||
// The ranks are now updated; the order in the list is the rank.
|
||||
for (int index = 0; index < list.size(); index++) {
|
||||
ShortcutInfoCompat prevShortcut = list.get(index);
|
||||
while (rank < maxShortcuts && loyaltyCardCursor.moveToNext()) {
|
||||
int id = loyaltyCardCursor.getInt(loyaltyCardCursor.getColumnIndexOrThrow(DBHelper.LoyaltyCardDbIds.ID));
|
||||
LoyaltyCard loyaltyCard = DBHelper.getLoyaltyCard(context, database, id);
|
||||
|
||||
LoyaltyCard loyaltyCard = DBHelper.getLoyaltyCard(context, database, Integer.parseInt(prevShortcut.getId()));
|
||||
|
||||
// skip outdated cards that no longer exist
|
||||
if (loyaltyCard != null) {
|
||||
ShortcutInfoCompat updatedShortcut = createShortcutBuilder(context, loyaltyCard)
|
||||
.setRank(rank)
|
||||
.build();
|
||||
|
||||
finalList.addLast(updatedShortcut);
|
||||
rank++;
|
||||
|
||||
// trim the list
|
||||
if (rank >= MAX_SHORTCUTS) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
dbHelper.close();
|
||||
}
|
||||
|
||||
ShortcutManagerCompat.setDynamicShortcuts(context, finalList);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the given card id from the app shortcuts, if such a
|
||||
* shortcut exists.
|
||||
*/
|
||||
static void removeShortcut(Context context, int cardId) {
|
||||
ShortcutManagerCompat.removeDynamicShortcuts(context, Collections.singletonList(Integer.toString(cardId)));
|
||||
}
|
||||
|
||||
static @NotNull
|
||||
Bitmap createAdaptiveBitmap(@NotNull Bitmap in, int paddingColor) {
|
||||
Bitmap ret = Bitmap.createBitmap(ADAPTIVE_BITMAP_SIZE, ADAPTIVE_BITMAP_SIZE, Bitmap.Config.ARGB_8888);
|
||||
@@ -130,7 +93,7 @@ class ShortcutHelper {
|
||||
intent.setAction(Intent.ACTION_MAIN);
|
||||
// Prevent instances of the view activity from piling up; if one exists let this
|
||||
// one replace it.
|
||||
intent.setFlags(intent.getFlags() | Intent.FLAG_ACTIVITY_SINGLE_TOP);
|
||||
intent.setFlags(intent.getFlags() | Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
|
||||
final Bundle bundle = new Bundle();
|
||||
bundle.putInt(LoyaltyCardViewActivity.BUNDLE_ID, loyaltyCard.id);
|
||||
intent.putExtras(bundle);
|
||||
@@ -150,4 +113,4 @@ class ShortcutHelper {
|
||||
.setIntent(intent)
|
||||
.setIcon(icon);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
package protect.card_locker;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Typeface;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
import android.view.Window;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.LinearLayout;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.widget.AppCompatImageView;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.core.graphics.ColorUtils;
|
||||
import androidx.core.view.WindowInsetsControllerCompat;
|
||||
|
||||
import com.google.android.material.color.MaterialColors;
|
||||
import com.google.android.material.textview.MaterialTextView;
|
||||
import com.yalantis.ucrop.UCropActivity;
|
||||
|
||||
public class UCropWrapper extends UCropActivity {
|
||||
public static final String UCROP_TOOLBAR_TYPEFACE_STYLE = "ucop_toolbar_typeface_style";
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
Utils.applyWindowInsets(findViewById(android.R.id.content));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onPostCreate(savedInstanceState);
|
||||
boolean darkMode = Utils.isDarkModeEnabled(this);
|
||||
Window window = getWindow();
|
||||
// setup status bar to look like the rest of the app
|
||||
if (Build.VERSION.SDK_INT >= 23) {
|
||||
if (window != null) {
|
||||
View decorView = window.getDecorView();
|
||||
WindowInsetsControllerCompat wic = new WindowInsetsControllerCompat(window, decorView);
|
||||
wic.setAppearanceLightStatusBars(!darkMode);
|
||||
}
|
||||
} else {
|
||||
// icons are always white back then
|
||||
if (window != null && !darkMode) {
|
||||
window.setStatusBarColor(ColorUtils.compositeColors(Color.argb(127, 0, 0, 0), window.getStatusBarColor()));
|
||||
}
|
||||
}
|
||||
|
||||
// find and check views that we wish to color modify
|
||||
// for when we update ucrop or switch to another cropper
|
||||
View check = findViewById(com.yalantis.ucrop.R.id.wrapper_controls);
|
||||
if (check instanceof FrameLayout) {
|
||||
FrameLayout controls = (FrameLayout) check;
|
||||
check = findViewById(com.yalantis.ucrop.R.id.wrapper_states);
|
||||
if (check instanceof LinearLayout) {
|
||||
LinearLayout states = (LinearLayout) check;
|
||||
for (int i = 0; i < controls.getChildCount(); i++) {
|
||||
check = controls.getChildAt(i);
|
||||
if (check instanceof AppCompatImageView) {
|
||||
AppCompatImageView controlsBackgroundImage = (AppCompatImageView) check;
|
||||
// everything gathered and are as expected, now perform color patching
|
||||
Utils.patchColors(this);
|
||||
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();
|
||||
controlsBackgroundImageDrawable.setTint(darkMode ? colorOnSurface : colorSurface);
|
||||
controlsBackgroundImage.setBackgroundDrawable(controlsBackgroundImageDrawable);
|
||||
|
||||
states.setBackgroundColor(darkMode ? colorSurface : colorOnSurface);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// change toolbar font
|
||||
check = findViewById(com.yalantis.ucrop.R.id.toolbar_title);
|
||||
if (check instanceof MaterialTextView) {
|
||||
MaterialTextView toolbarTextview = (MaterialTextView) check;
|
||||
Intent intent = getIntent();
|
||||
int style = intent.getIntExtra(UCROP_TOOLBAR_TYPEFACE_STYLE, -1);
|
||||
if (style != -1) {
|
||||
toolbarTextview.setTypeface(Typeface.defaultFromStyle(style));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
package protect.card_locker
|
||||
|
||||
import android.graphics.Color
|
||||
import android.graphics.Typeface
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.LinearLayout
|
||||
import androidx.appcompat.widget.AppCompatImageView
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.graphics.ColorUtils
|
||||
import androidx.core.view.WindowInsetsControllerCompat
|
||||
import androidx.core.view.children
|
||||
import com.google.android.material.color.MaterialColors
|
||||
import com.google.android.material.textview.MaterialTextView
|
||||
import com.yalantis.ucrop.UCropActivity
|
||||
|
||||
class UCropWrapper : UCropActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
Utils.applyWindowInsets(findViewById(android.R.id.content))
|
||||
}
|
||||
|
||||
override fun onPostCreate(savedInstanceState: Bundle?) {
|
||||
super.onPostCreate(savedInstanceState)
|
||||
val darkMode = Utils.isDarkModeEnabled(this)
|
||||
// setup status bar to look like the rest of the app
|
||||
setupStatusBar(darkMode)
|
||||
// find and check views that we wish to color modify
|
||||
// for when we update ucrop or switch to another cropper
|
||||
checkViews(darkMode)
|
||||
// change toolbar font
|
||||
changeToolbarFont()
|
||||
}
|
||||
|
||||
private fun setupStatusBar(darkMode: Boolean) {
|
||||
if (window == null) {
|
||||
return
|
||||
}
|
||||
|
||||
val decorView = window.decorView
|
||||
val wic = WindowInsetsControllerCompat(window, decorView)
|
||||
wic.isAppearanceLightStatusBars = !darkMode
|
||||
}
|
||||
|
||||
private fun checkViews(darkMode: Boolean) {
|
||||
var view = findViewById<View?>(com.yalantis.ucrop.R.id.wrapper_controls)
|
||||
if (view !is FrameLayout) {
|
||||
return
|
||||
}
|
||||
|
||||
val controls = view
|
||||
view = findViewById(com.yalantis.ucrop.R.id.wrapper_states)
|
||||
if (view !is LinearLayout) {
|
||||
return
|
||||
}
|
||||
val states = view
|
||||
controls.children.firstOrNull { it is AppCompatImageView }?.let {
|
||||
// everything gathered and are as expected, now perform color patching
|
||||
Utils.patchColors(this)
|
||||
val colorSurface = MaterialColors.getColor(
|
||||
this,
|
||||
com.google.android.material.R.attr.colorSurface,
|
||||
ContextCompat.getColor(
|
||||
this,
|
||||
R.color.md_theme_light_surface
|
||||
)
|
||||
)
|
||||
val colorOnSurface = MaterialColors.getColor(
|
||||
this,
|
||||
com.google.android.material.R.attr.colorOnSurface,
|
||||
ContextCompat.getColor(
|
||||
this,
|
||||
R.color.md_theme_light_onSurface
|
||||
)
|
||||
)
|
||||
|
||||
val controlsBackgroundImageDrawable = it.background
|
||||
controlsBackgroundImageDrawable.mutate()
|
||||
controlsBackgroundImageDrawable.setTint(
|
||||
if (darkMode) {
|
||||
colorOnSurface
|
||||
} else {
|
||||
colorSurface
|
||||
}
|
||||
)
|
||||
it.background = controlsBackgroundImageDrawable
|
||||
states.setBackgroundColor(
|
||||
if (darkMode) {
|
||||
colorSurface
|
||||
} else {
|
||||
colorOnSurface
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun changeToolbarFont() {
|
||||
val toolbar = findViewById<View?>(com.yalantis.ucrop.R.id.toolbar_title)
|
||||
if (toolbar !is MaterialTextView) {
|
||||
return
|
||||
}
|
||||
|
||||
val style = intent.getIntExtra(UCROP_TOOLBAR_TYPEFACE_STYLE, -1)
|
||||
if (style != -1) {
|
||||
toolbar.setTypeface(Typeface.defaultFromStyle(style))
|
||||
}
|
||||
}
|
||||
|
||||
internal companion object {
|
||||
const val UCROP_TOOLBAR_TYPEFACE_STYLE: String = "ucop_toolbar_typeface_style"
|
||||
}
|
||||
}
|
||||
@@ -118,7 +118,7 @@ public class Utils {
|
||||
static final double LUMINANCE_MIDPOINT = 0.5;
|
||||
|
||||
static final int BITMAP_SIZE_SMALL = 512;
|
||||
static final int BITMAP_SIZE_BIG = 2048;
|
||||
static final int BITMAP_SIZE_BIG = 1600;
|
||||
|
||||
static public LetterBitmap generateIcon(Context context, LoyaltyCard loyaltyCard, boolean forShortcut) {
|
||||
return generateIcon(context, loyaltyCard.store, loyaltyCard.headerColor, forShortcut);
|
||||
@@ -143,7 +143,7 @@ public class Utils {
|
||||
int pixelSize = context.getResources().getDimensionPixelSize(R.dimen.tileLetterImageSize);
|
||||
|
||||
if (backgroundColor == null) {
|
||||
backgroundColor = LetterBitmap.getDefaultColor(context, store);
|
||||
backgroundColor = LetterBitmap.Companion.getDefaultColor(context, store);
|
||||
}
|
||||
|
||||
return new LetterBitmap(context, store, store,
|
||||
@@ -963,31 +963,9 @@ public class Utils {
|
||||
// replace colors in the current theme
|
||||
public static void patchColors(AppCompatActivity activity) {
|
||||
Settings settings = new Settings(activity);
|
||||
String color = settings.getColor();
|
||||
|
||||
Resources.Theme theme = activity.getTheme();
|
||||
Resources resources = activity.getResources();
|
||||
if (color.equals(resources.getString(R.string.settings_key_pink_theme))) {
|
||||
theme.applyStyle(R.style.pink, true);
|
||||
} else if (color.equals(resources.getString(R.string.settings_key_magenta_theme))) {
|
||||
theme.applyStyle(R.style.magenta, true);
|
||||
} else if (color.equals(resources.getString(R.string.settings_key_violet_theme))) {
|
||||
theme.applyStyle(R.style.violet, true);
|
||||
} else if (color.equals(resources.getString(R.string.settings_key_blue_theme))) {
|
||||
theme.applyStyle(R.style.blue, true);
|
||||
} else if (color.equals(resources.getString(R.string.settings_key_sky_blue_theme))) {
|
||||
theme.applyStyle(R.style.skyblue, true);
|
||||
} else if (color.equals(resources.getString(R.string.settings_key_green_theme))) {
|
||||
theme.applyStyle(R.style.green, true);
|
||||
} else if (color.equals(resources.getString(R.string.settings_key_brown_theme))) {
|
||||
theme.applyStyle(R.style.brown, true);
|
||||
} else if (color.equals(resources.getString(R.string.settings_key_catima_theme))) {
|
||||
// catima theme is AppTheme itself, no dynamic colors nor applyStyle
|
||||
} else {
|
||||
// final catch all in case of invalid theme value from older versions
|
||||
// also handles R.string.settings_key_system_theme
|
||||
DynamicColors.applyToActivityIfAvailable(activity);
|
||||
}
|
||||
DynamicColors.applyToActivityIfAvailable(activity);
|
||||
|
||||
if (isDarkModeEnabled(activity) && settings.getOledDark()) {
|
||||
theme.applyStyle(R.style.DarkBackground, true);
|
||||
@@ -1129,7 +1107,7 @@ public class Utils {
|
||||
}
|
||||
|
||||
public static int getHeaderColor(Context context, LoyaltyCard loyaltyCard) {
|
||||
return loyaltyCard.headerColor != null ? loyaltyCard.headerColor : LetterBitmap.getDefaultColor(context, loyaltyCard.store);
|
||||
return loyaltyCard.headerColor != null ? loyaltyCard.headerColor : LetterBitmap.Companion.getDefaultColor(context, loyaltyCard.store);
|
||||
}
|
||||
|
||||
public static String checksum(InputStream input) throws IOException {
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
package protect.card_locker.async;
|
||||
|
||||
import java.util.concurrent.Callable;
|
||||
|
||||
public interface CompatCallable<T> extends Callable<T> {
|
||||
void onPostExecute(Object result);
|
||||
|
||||
void onPreExecute();
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package protect.card_locker.async
|
||||
|
||||
import java.util.concurrent.Callable
|
||||
|
||||
interface CompatCallable<T> : Callable<T?> {
|
||||
fun onPostExecute(result: Any?)
|
||||
|
||||
fun onPreExecute()
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
package protect.card_locker.cardimageview
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.Bitmap
|
||||
import android.os.Bundle
|
||||
import android.view.WindowManager
|
||||
import android.widget.Toast
|
||||
import androidx.activity.OnBackPressedDispatcher
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.gestures.detectTapGestures
|
||||
import androidx.compose.foundation.gestures.rememberTransformableState
|
||||
import androidx.compose.foundation.gestures.transformable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableFloatStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.asImageBitmap
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.layout.onSizeChanged
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.IntSize
|
||||
import protect.card_locker.CatimaComponentActivity
|
||||
import protect.card_locker.ImageLocationType
|
||||
import protect.card_locker.R
|
||||
import protect.card_locker.Utils
|
||||
import protect.card_locker.compose.CatimaTopAppBar
|
||||
import protect.card_locker.compose.theme.CatimaTheme
|
||||
import protect.card_locker.preferences.Settings
|
||||
|
||||
class LoyaltyCardImageViewActivity : CatimaComponentActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
fixedEdgeToEdge()
|
||||
applyWindowPreferences()
|
||||
|
||||
val imageLocationType = intent.imageLocationType()
|
||||
val loyaltyCardId = intent.getIntExtra(BUNDLE_ID, 0)
|
||||
val bitmap = imageLocationType?.let {
|
||||
Utils.retrieveCardImage(this, loyaltyCardId, it)
|
||||
}
|
||||
|
||||
if (loyaltyCardId == 0 || imageLocationType == null || bitmap == null) {
|
||||
Toast.makeText(this, R.string.failedToRetrieveImageFile, Toast.LENGTH_SHORT).show()
|
||||
finish()
|
||||
return
|
||||
}
|
||||
|
||||
title = getString(imageLocationType.descriptionRes())
|
||||
|
||||
setContent {
|
||||
CatimaTheme {
|
||||
LoyaltyCardImageViewScreen(
|
||||
bitmap = bitmap,
|
||||
contentDescriptionRes = imageLocationType.descriptionRes(),
|
||||
onBackPressedDispatcher = onBackPressedDispatcher
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun applyWindowPreferences() {
|
||||
val settings = Settings(this)
|
||||
val currentWindow = window ?: return
|
||||
|
||||
val attributes = currentWindow.attributes
|
||||
if (settings.useMaxBrightnessDisplayingBarcode()) {
|
||||
attributes.screenBrightness = 1F
|
||||
}
|
||||
|
||||
if (settings.keepScreenOn) {
|
||||
currentWindow.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
||||
}
|
||||
|
||||
currentWindow.attributes = attributes
|
||||
}
|
||||
|
||||
private fun Intent.imageLocationType(): ImageLocationType? {
|
||||
val name = getStringExtra(BUNDLE_IMAGE_LOCATION_TYPE) ?: return null
|
||||
return runCatching { ImageLocationType.valueOf(name) }.getOrNull()
|
||||
}
|
||||
|
||||
private fun ImageLocationType.descriptionRes(): Int {
|
||||
return when (this) {
|
||||
ImageLocationType.icon -> R.string.thumbnailDescription
|
||||
ImageLocationType.front -> R.string.frontImageDescription
|
||||
ImageLocationType.back -> R.string.backImageDescription
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val BUNDLE_ID = "id"
|
||||
const val BUNDLE_IMAGE_LOCATION_TYPE = "imageLocationType"
|
||||
|
||||
@JvmStatic
|
||||
fun createIntent(
|
||||
context: Context,
|
||||
loyaltyCardId: Int,
|
||||
imageLocationType: ImageLocationType
|
||||
): Intent {
|
||||
return Intent(context, LoyaltyCardImageViewActivity::class.java)
|
||||
.putExtra(BUNDLE_ID, loyaltyCardId)
|
||||
.putExtra(BUNDLE_IMAGE_LOCATION_TYPE, imageLocationType.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun LoyaltyCardImageViewScreen(
|
||||
bitmap: Bitmap,
|
||||
@StringRes contentDescriptionRes: Int,
|
||||
onBackPressedDispatcher: OnBackPressedDispatcher,
|
||||
) {
|
||||
var scale by remember { mutableFloatStateOf(1F) }
|
||||
var offset by remember { mutableStateOf(Offset.Zero) }
|
||||
var size by remember { mutableStateOf(IntSize.Zero) }
|
||||
val zoomState = rememberTransformableState { _, zoomChange, panChange, _ ->
|
||||
val nextScale = (scale * zoomChange).coerceIn(1F, MAX_IMAGE_SCALE)
|
||||
scale = nextScale
|
||||
offset = if (nextScale == 1F) {
|
||||
Offset.Zero
|
||||
} else {
|
||||
(offset + panChange).clampTo(size, nextScale)
|
||||
}
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
CatimaTopAppBar(
|
||||
title = stringResource(contentDescriptionRes),
|
||||
onBackPressedDispatcher = onBackPressedDispatcher
|
||||
)
|
||||
}
|
||||
) { innerPadding ->
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(innerPadding)
|
||||
.background(MaterialTheme.colorScheme.background)
|
||||
.testTag("card_image_zoom")
|
||||
) {
|
||||
Image(
|
||||
bitmap = bitmap.asImageBitmap(),
|
||||
contentDescription = stringResource(contentDescriptionRes),
|
||||
contentScale = ContentScale.Fit,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.onSizeChanged { size = it }
|
||||
.pointerInput(bitmap) {
|
||||
detectTapGestures(
|
||||
onDoubleTap = {
|
||||
scale = 1F
|
||||
offset = Offset.Zero
|
||||
}
|
||||
)
|
||||
}
|
||||
.transformable(zoomState)
|
||||
.graphicsLayer {
|
||||
scaleX = scale
|
||||
scaleY = scale
|
||||
translationX = offset.x
|
||||
translationY = offset.y
|
||||
}
|
||||
.testTag("card_image")
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun Offset.clampTo(size: IntSize, scale: Float): Offset {
|
||||
if (size.width == 0 || size.height == 0) {
|
||||
return this
|
||||
}
|
||||
|
||||
val maxX = size.width * (scale - 1F) / 2F
|
||||
val maxY = size.height * (scale - 1F) / 2F
|
||||
return Offset(
|
||||
x = x.coerceIn(-maxX, maxX),
|
||||
y = y.coerceIn(-maxY, maxY)
|
||||
)
|
||||
}
|
||||
|
||||
private const val MAX_IMAGE_SCALE = 5F
|
||||
@@ -0,0 +1,103 @@
|
||||
package protect.card_locker.cardview;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
final class LoyaltyCardImageNavigator {
|
||||
private final List<LoyaltyCardImageType> imageTypes;
|
||||
private int currentIndex;
|
||||
|
||||
LoyaltyCardImageNavigator(List<LoyaltyCardImageType> imageTypes, int currentIndex) {
|
||||
this.imageTypes = new ArrayList<>(imageTypes);
|
||||
this.currentIndex = clampIndex(currentIndex);
|
||||
}
|
||||
|
||||
LoyaltyCardImageType getCurrent() {
|
||||
if (isEmpty()) {
|
||||
return LoyaltyCardImageType.NONE;
|
||||
}
|
||||
return imageTypes.get(currentIndex);
|
||||
}
|
||||
|
||||
boolean isEmpty() {
|
||||
return imageTypes.isEmpty();
|
||||
}
|
||||
|
||||
int size() {
|
||||
return imageTypes.size();
|
||||
}
|
||||
|
||||
boolean remove(LoyaltyCardImageType type) {
|
||||
int removedIndex = imageTypes.indexOf(type);
|
||||
if (removedIndex == -1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
imageTypes.remove(removedIndex);
|
||||
currentIndex = clampIndex(currentIndex);
|
||||
return true;
|
||||
}
|
||||
|
||||
boolean canGoPrevious() {
|
||||
return currentIndex > 0;
|
||||
}
|
||||
|
||||
boolean canGoNext() {
|
||||
return currentIndex < imageTypes.size() - 1;
|
||||
}
|
||||
|
||||
boolean movePrevious() {
|
||||
if (!canGoPrevious()) {
|
||||
return false;
|
||||
}
|
||||
currentIndex--;
|
||||
return true;
|
||||
}
|
||||
|
||||
boolean moveNext(boolean overflow) {
|
||||
if (isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
if (canGoNext()) {
|
||||
currentIndex++;
|
||||
return true;
|
||||
}
|
||||
if (overflow) {
|
||||
currentIndex = 0;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
int getCurrentIndex() {
|
||||
return currentIndex;
|
||||
}
|
||||
|
||||
LoyaltyCardImageType peekNext(boolean overflow) {
|
||||
if (isEmpty()) {
|
||||
return LoyaltyCardImageType.NONE;
|
||||
}
|
||||
|
||||
if (canGoNext()) {
|
||||
return imageTypes.get(currentIndex + 1);
|
||||
}
|
||||
|
||||
return overflow ? imageTypes.get(0) : getCurrent();
|
||||
}
|
||||
|
||||
private int clampIndex(int index) {
|
||||
if (imageTypes.isEmpty()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return Math.max(0, Math.min(index, imageTypes.size() - 1));
|
||||
}
|
||||
}
|
||||
|
||||
enum LoyaltyCardImageType {
|
||||
NONE,
|
||||
ICON,
|
||||
BARCODE,
|
||||
IMAGE_FRONT,
|
||||
IMAGE_BACK
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
package protect.card_locker.cardview;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Color;
|
||||
import android.view.View;
|
||||
import android.view.ViewTreeObserver;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.google.android.material.card.MaterialCardView;
|
||||
import com.google.android.material.color.MaterialColors;
|
||||
|
||||
import java.nio.charset.Charset;
|
||||
|
||||
import protect.card_locker.*;
|
||||
import protect.card_locker.async.TaskHandler;
|
||||
|
||||
final class LoyaltyCardMainImageRenderer {
|
||||
private final Context context;
|
||||
private final TaskHandler tasks = new TaskHandler();
|
||||
private final BarcodeImageWriterResultCallback barcodeCallback;
|
||||
|
||||
LoyaltyCardMainImageRenderer(
|
||||
Context context,
|
||||
BarcodeImageWriterResultCallback barcodeCallback
|
||||
) {
|
||||
this.context = context;
|
||||
this.barcodeCallback = barcodeCallback;
|
||||
}
|
||||
|
||||
void renderCurrent(
|
||||
LoyaltyCardImageType imageType,
|
||||
Bitmap frontImageBitmap,
|
||||
Bitmap backImageBitmap,
|
||||
CatimaBarcode format,
|
||||
Charset barcodeEncoding,
|
||||
String cardIdString,
|
||||
String barcodeIdString,
|
||||
ImageView barcodeRenderTarget,
|
||||
TextView mainImageDescription,
|
||||
MaterialCardView mainCardView,
|
||||
boolean isFullscreen,
|
||||
boolean waitForResize
|
||||
) {
|
||||
if (imageType == LoyaltyCardImageType.NONE) {
|
||||
// With no renderable media left, show the raw card ID instead of an empty card area.
|
||||
barcodeRenderTarget.setVisibility(View.GONE);
|
||||
mainCardView.setCardBackgroundColor(Color.TRANSPARENT);
|
||||
mainImageDescription.setTextColor(
|
||||
MaterialColors.getColor(
|
||||
mainImageDescription,
|
||||
com.google.android.material.R.attr.colorOnSurfaceVariant
|
||||
)
|
||||
);
|
||||
mainImageDescription.setText(cardIdString);
|
||||
return;
|
||||
}
|
||||
|
||||
if (imageType == LoyaltyCardImageType.BARCODE) {
|
||||
barcodeRenderTarget.setBackgroundColor(Color.WHITE);
|
||||
mainCardView.setCardBackgroundColor(Color.WHITE);
|
||||
mainImageDescription.setTextColor(context.getResources().getColor(R.color.md_theme_light_onSurfaceVariant));
|
||||
|
||||
if (waitForResize) {
|
||||
redrawBarcodeAfterResize(
|
||||
barcodeRenderTarget,
|
||||
barcodeIdString,
|
||||
cardIdString,
|
||||
format,
|
||||
barcodeEncoding,
|
||||
!isFullscreen,
|
||||
isFullscreen
|
||||
);
|
||||
} else {
|
||||
drawBarcode(
|
||||
barcodeRenderTarget,
|
||||
barcodeIdString,
|
||||
cardIdString,
|
||||
format,
|
||||
barcodeEncoding,
|
||||
!isFullscreen,
|
||||
isFullscreen
|
||||
);
|
||||
}
|
||||
|
||||
mainImageDescription.setText(cardIdString);
|
||||
barcodeRenderTarget.setContentDescription(
|
||||
context.getString(R.string.barcodeImageDescriptionWithType, format.prettyName())
|
||||
);
|
||||
} else if (imageType == LoyaltyCardImageType.IMAGE_FRONT) {
|
||||
barcodeRenderTarget.setImageBitmap(frontImageBitmap);
|
||||
barcodeRenderTarget.setBackgroundColor(Color.TRANSPARENT);
|
||||
mainCardView.setCardBackgroundColor(Color.TRANSPARENT);
|
||||
mainImageDescription.setTextColor(
|
||||
MaterialColors.getColor(
|
||||
mainImageDescription,
|
||||
com.google.android.material.R.attr.colorOnSurfaceVariant
|
||||
)
|
||||
);
|
||||
mainImageDescription.setText(context.getString(R.string.frontImageDescription));
|
||||
barcodeRenderTarget.setContentDescription(context.getString(R.string.frontImageDescription));
|
||||
} else if (imageType == LoyaltyCardImageType.IMAGE_BACK) {
|
||||
barcodeRenderTarget.setImageBitmap(backImageBitmap);
|
||||
barcodeRenderTarget.setBackgroundColor(Color.TRANSPARENT);
|
||||
mainCardView.setCardBackgroundColor(Color.TRANSPARENT);
|
||||
mainImageDescription.setTextColor(
|
||||
MaterialColors.getColor(
|
||||
mainImageDescription,
|
||||
com.google.android.material.R.attr.colorOnSurfaceVariant
|
||||
)
|
||||
);
|
||||
mainImageDescription.setText(context.getString(R.string.backImageDescription));
|
||||
barcodeRenderTarget.setContentDescription(context.getString(R.string.backImageDescription));
|
||||
} else {
|
||||
throw new IllegalArgumentException("Unknown image type: " + imageType);
|
||||
}
|
||||
|
||||
barcodeRenderTarget.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
private void redrawBarcodeAfterResize(
|
||||
ImageView barcodeRenderTarget,
|
||||
String barcodeIdString,
|
||||
String cardIdString,
|
||||
CatimaBarcode format,
|
||||
Charset barcodeEncoding,
|
||||
boolean addPadding,
|
||||
boolean isFullscreen
|
||||
) {
|
||||
if (format == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Barcode dimensions depend on the final ImageView size, so wait for layout before rendering.
|
||||
barcodeRenderTarget.getViewTreeObserver().addOnGlobalLayoutListener(
|
||||
new ViewTreeObserver.OnGlobalLayoutListener() {
|
||||
@Override
|
||||
public void onGlobalLayout() {
|
||||
barcodeRenderTarget.getViewTreeObserver().removeOnGlobalLayoutListener(this);
|
||||
drawBarcode(
|
||||
barcodeRenderTarget,
|
||||
barcodeIdString,
|
||||
cardIdString,
|
||||
format,
|
||||
barcodeEncoding,
|
||||
addPadding,
|
||||
isFullscreen
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void drawBarcode(
|
||||
ImageView barcodeRenderTarget,
|
||||
String barcodeIdString,
|
||||
String cardIdString,
|
||||
CatimaBarcode format,
|
||||
Charset barcodeEncoding,
|
||||
boolean addPadding,
|
||||
boolean isFullscreen
|
||||
) {
|
||||
// Barcodes are regenerated eagerly because zoom/fullscreen changes affect the output bitmap.
|
||||
tasks.flushTaskList(TaskHandler.TYPE.BARCODE, true, false, false);
|
||||
if (format == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
BarcodeImageWriterTask barcodeWriter = new BarcodeImageWriterTask(
|
||||
context.getApplicationContext(),
|
||||
barcodeRenderTarget,
|
||||
barcodeIdString != null ? barcodeIdString : cardIdString,
|
||||
format,
|
||||
barcodeEncoding,
|
||||
null,
|
||||
false,
|
||||
barcodeCallback,
|
||||
addPadding,
|
||||
isFullscreen
|
||||
);
|
||||
tasks.executeTask(TaskHandler.TYPE.BARCODE, barcodeWriter);
|
||||
}
|
||||
}
|
||||
+377
-576
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,246 @@
|
||||
package protect.card_locker.cardview;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Color;
|
||||
import android.text.InputType;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.Spanned;
|
||||
import android.text.TextUtils;
|
||||
import android.text.method.DigitsKeyListener;
|
||||
import android.text.style.ForegroundColorSpan;
|
||||
import android.text.util.Linkify;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
import com.google.android.material.textfield.TextInputEditText;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.text.DateFormat;
|
||||
import java.text.ParseException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
import protect.card_locker.*;
|
||||
|
||||
final class LoyaltyCardViewDialogs {
|
||||
interface BalanceUpdateListener {
|
||||
void onBalanceUpdated(BigDecimal newBalance);
|
||||
}
|
||||
|
||||
void showInfoDialog(Context context, LoyaltyCard loyaltyCard, List<Group> loyaltyCardGroups) {
|
||||
AlertDialog.Builder infoDialog = new MaterialAlertDialogBuilder(context);
|
||||
|
||||
int dialogContentPadding = context.getResources().getDimensionPixelSize(R.dimen.alert_dialog_content_padding);
|
||||
infoDialog.setTitle(loyaltyCard.store);
|
||||
|
||||
TextView infoTextview = new TextView(context);
|
||||
infoTextview.setPadding(
|
||||
dialogContentPadding,
|
||||
dialogContentPadding / 2,
|
||||
dialogContentPadding,
|
||||
0
|
||||
);
|
||||
infoTextview.setAutoLinkMask(Linkify.EMAIL_ADDRESSES | Linkify.PHONE_NUMBERS | Linkify.WEB_URLS);
|
||||
infoTextview.setTextIsSelectable(true);
|
||||
|
||||
SpannableStringBuilder infoText = new SpannableStringBuilder();
|
||||
if (!loyaltyCard.note.isEmpty()) {
|
||||
infoText.append(loyaltyCard.note);
|
||||
}
|
||||
|
||||
if (!loyaltyCardGroups.isEmpty()) {
|
||||
List<String> groupNames = new ArrayList<>();
|
||||
for (Group group : loyaltyCardGroups) {
|
||||
groupNames.add(group._id);
|
||||
}
|
||||
|
||||
padSpannableString(infoText);
|
||||
infoText.append(context.getString(R.string.groupsList, TextUtils.join(", ", groupNames)));
|
||||
}
|
||||
|
||||
if (hasBalance(loyaltyCard)) {
|
||||
padSpannableString(infoText);
|
||||
infoText.append(context.getString(
|
||||
R.string.balanceSentence,
|
||||
Utils.formatBalance(context, loyaltyCard.balance, loyaltyCard.balanceType)
|
||||
));
|
||||
}
|
||||
|
||||
appendDateInfo(
|
||||
context,
|
||||
infoText,
|
||||
loyaltyCard.validFrom,
|
||||
Utils::isNotYetValid,
|
||||
R.string.validFromSentence,
|
||||
R.string.validFromSentence
|
||||
);
|
||||
appendDateInfo(
|
||||
context,
|
||||
infoText,
|
||||
loyaltyCard.expiry,
|
||||
Utils::hasExpired,
|
||||
R.string.expiryStateSentenceExpired,
|
||||
R.string.expiryStateSentence
|
||||
);
|
||||
|
||||
infoTextview.setText(infoText);
|
||||
infoDialog.setView(infoTextview);
|
||||
infoDialog.setPositiveButton(R.string.ok, (dialogInterface, i) -> dialogInterface.dismiss());
|
||||
infoDialog.create().show();
|
||||
}
|
||||
|
||||
void showBalanceUpdateDialog(
|
||||
Context context,
|
||||
LoyaltyCard loyaltyCard,
|
||||
BalanceUpdateListener balanceUpdateListener
|
||||
) {
|
||||
AlertDialog.Builder builder = new MaterialAlertDialogBuilder(context);
|
||||
builder.setTitle(R.string.updateBalanceTitle);
|
||||
|
||||
FrameLayout container = new FrameLayout(context);
|
||||
FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
);
|
||||
int contentPadding = context.getResources().getDimensionPixelSize(R.dimen.alert_dialog_content_padding);
|
||||
params.leftMargin = contentPadding;
|
||||
params.topMargin = contentPadding / 2;
|
||||
params.rightMargin = contentPadding;
|
||||
|
||||
LinearLayout layout = new LinearLayout(context);
|
||||
layout.setOrientation(LinearLayout.VERTICAL);
|
||||
|
||||
TextView currentTextview = new TextView(context);
|
||||
currentTextview.setText(context.getString(
|
||||
R.string.currentBalanceSentence,
|
||||
Utils.formatBalance(context, loyaltyCard.balance, loyaltyCard.balanceType)
|
||||
));
|
||||
layout.addView(currentTextview);
|
||||
|
||||
final TextInputEditText input = new TextInputEditText(context);
|
||||
input.setInputType(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_FLAG_DECIMAL);
|
||||
input.setKeyListener(DigitsKeyListener.getInstance("0123456789,."));
|
||||
input.setHint(R.string.updateBalanceHint);
|
||||
|
||||
layout.addView(input);
|
||||
layout.setLayoutParams(params);
|
||||
container.addView(layout);
|
||||
builder.setView(container);
|
||||
|
||||
builder.setPositiveButton(R.string.spend, (dialogInterface, i) -> {
|
||||
try {
|
||||
BigDecimal balanceChange = Utils.parseBalance(input.getText().toString(), loyaltyCard.balanceType);
|
||||
BigDecimal newBalance = loyaltyCard.balance.subtract(balanceChange).max(new BigDecimal(0));
|
||||
balanceUpdateListener.onBalanceUpdated(newBalance);
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(
|
||||
R.string.newBalanceSentence,
|
||||
Utils.formatBalance(context, newBalance, loyaltyCard.balanceType)
|
||||
),
|
||||
Toast.LENGTH_LONG
|
||||
).show();
|
||||
} catch (ParseException e) {
|
||||
Toast.makeText(context, R.string.amountParsingFailed, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
});
|
||||
builder.setNegativeButton(R.string.receive, (dialogInterface, i) -> {
|
||||
try {
|
||||
BigDecimal balanceChange = Utils.parseBalance(input.getText().toString(), loyaltyCard.balanceType);
|
||||
BigDecimal newBalance = loyaltyCard.balance.add(balanceChange);
|
||||
balanceUpdateListener.onBalanceUpdated(newBalance);
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(
|
||||
R.string.newBalanceSentence,
|
||||
Utils.formatBalance(context, newBalance, loyaltyCard.balanceType)
|
||||
),
|
||||
Toast.LENGTH_LONG
|
||||
).show();
|
||||
} catch (ParseException e) {
|
||||
Toast.makeText(context, R.string.amountParsingFailed, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
});
|
||||
builder.setNeutralButton(context.getString(R.string.cancel), (dialog, which) -> dialog.cancel());
|
||||
AlertDialog dialog = builder.create();
|
||||
|
||||
// Button state depends on the parsed input, so listeners must be bound after the dialog exists.
|
||||
input.addTextChangedListener(new SimpleTextWatcher() {
|
||||
@Override
|
||||
public void onTextChanged(CharSequence s, int start, int before, int count) {
|
||||
BigDecimal balanceChange;
|
||||
|
||||
try {
|
||||
balanceChange = Utils.parseBalance(s.toString(), loyaltyCard.balanceType);
|
||||
} catch (ParseException e) {
|
||||
input.setError(context.getString(R.string.amountParsingFailed));
|
||||
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false);
|
||||
dialog.getButton(AlertDialog.BUTTON_NEGATIVE).setEnabled(false);
|
||||
return;
|
||||
}
|
||||
|
||||
input.setError(null);
|
||||
boolean hasNonZeroValue = !balanceChange.equals(new BigDecimal(0));
|
||||
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(hasNonZeroValue);
|
||||
dialog.getButton(AlertDialog.BUTTON_NEGATIVE).setEnabled(hasNonZeroValue);
|
||||
}
|
||||
});
|
||||
|
||||
dialog.show();
|
||||
// Touching dialog buttons before show() can crash because they are not created yet.
|
||||
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false);
|
||||
dialog.getButton(AlertDialog.BUTTON_NEGATIVE).setEnabled(false);
|
||||
|
||||
if (dialog.getWindow() != null) {
|
||||
dialog.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE);
|
||||
}
|
||||
// Focus immediately so the keyboard opens on the amount field.
|
||||
input.requestFocus();
|
||||
}
|
||||
|
||||
private boolean hasBalance(LoyaltyCard loyaltyCard) {
|
||||
return !loyaltyCard.balance.equals(new BigDecimal(0));
|
||||
}
|
||||
|
||||
private SpannableStringBuilder padSpannableString(SpannableStringBuilder spannableStringBuilder) {
|
||||
if (spannableStringBuilder.length() > 0) {
|
||||
spannableStringBuilder.append("\n\n");
|
||||
}
|
||||
|
||||
return spannableStringBuilder;
|
||||
}
|
||||
|
||||
private void appendDateInfo(
|
||||
Context context,
|
||||
SpannableStringBuilder infoText,
|
||||
Date date,
|
||||
Predicate<Date> dateCheck,
|
||||
@StringRes int dateCheckTrueString,
|
||||
@StringRes int dateCheckFalseString
|
||||
) {
|
||||
if (date == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
String formattedDate = DateFormat.getDateInstance(DateFormat.LONG).format(date);
|
||||
|
||||
padSpannableString(infoText);
|
||||
if (dateCheck.test(date)) {
|
||||
int start = infoText.length();
|
||||
infoText.append(context.getString(dateCheckTrueString, formattedDate));
|
||||
infoText.setSpan(new ForegroundColorSpan(Color.RED), start, infoText.length(), Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
|
||||
} else {
|
||||
infoText.append(context.getString(dateCheckFalseString, formattedDate));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
package protect.card_locker.compose
|
||||
|
||||
import androidx.activity.compose.LocalActivity
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.hideFromAccessibility
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.unit.dp
|
||||
import protect.card_locker.OpenWebLinkHandler
|
||||
import protect.card_locker.R
|
||||
|
||||
@Composable
|
||||
fun CatimaAboutSection(
|
||||
title: String,
|
||||
message: String,
|
||||
modifier: Modifier = Modifier,
|
||||
onClickUrl: String? = null,
|
||||
onClickDialogText: AnnotatedString? = null,
|
||||
) {
|
||||
val activity = LocalActivity.current
|
||||
|
||||
val openDialog = remember { mutableStateOf(false) }
|
||||
|
||||
Row(
|
||||
modifier = modifier
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp)
|
||||
.clickable {
|
||||
if (onClickDialogText != null) {
|
||||
openDialog.value = true
|
||||
} else if (onClickUrl != null) {
|
||||
OpenWebLinkHandler().openBrowser(activity, onClickUrl)
|
||||
}
|
||||
}
|
||||
.semantics(mergeDescendants = true) {}
|
||||
) {
|
||||
Column(modifier = Modifier.weight(1F)) {
|
||||
Text(
|
||||
text = title,
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
Text(text = message)
|
||||
}
|
||||
Text(modifier = Modifier.align(Alignment.CenterVertically).semantics() { hideFromAccessibility() },
|
||||
text = ">",
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
}
|
||||
if (openDialog.value && onClickDialogText != null) {
|
||||
AlertDialog(
|
||||
icon = {},
|
||||
title = {
|
||||
Text(text = title)
|
||||
},
|
||||
text = {
|
||||
Text(
|
||||
text = onClickDialogText,
|
||||
modifier = Modifier.verticalScroll(rememberScrollState())
|
||||
)
|
||||
},
|
||||
onDismissRequest = {
|
||||
openDialog.value = false
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
openDialog.value = false
|
||||
}
|
||||
) {
|
||||
Text(stringResource(R.string.ok))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
if (onClickUrl != null) {
|
||||
TextButton(
|
||||
onClick = {
|
||||
OpenWebLinkHandler().openBrowser(activity, onClickUrl)
|
||||
}
|
||||
) {
|
||||
Text(stringResource(R.string.view_online))
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package protect.card_locker.compose
|
||||
|
||||
import androidx.activity.OnBackPressedDispatcher
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import protect.card_locker.R
|
||||
import protect.card_locker.preferences.Settings
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun CatimaTopAppBar(title: String, onBackPressedDispatcher: OnBackPressedDispatcher?) {
|
||||
// Use pure black in OLED theme
|
||||
val context = LocalContext.current
|
||||
val settings = Settings(context)
|
||||
val isDarkMode = when (settings.theme) {
|
||||
AppCompatDelegate.MODE_NIGHT_NO -> false
|
||||
AppCompatDelegate.MODE_NIGHT_YES -> true
|
||||
else -> isSystemInDarkTheme()
|
||||
}
|
||||
|
||||
val appBarColors = if (isDarkMode && settings.oledDark) {
|
||||
TopAppBarDefaults.topAppBarColors().copy(containerColor = Color.Black)
|
||||
} else {
|
||||
TopAppBarDefaults.topAppBarColors()
|
||||
}
|
||||
|
||||
TopAppBar(
|
||||
modifier = Modifier.testTag("topbar_catima"),
|
||||
title = { Text(text = title) },
|
||||
colors = appBarColors,
|
||||
navigationIcon = {
|
||||
if (onBackPressedDispatcher != null) {
|
||||
IconButton(onClick = { onBackPressedDispatcher.onBackPressed() }) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||
contentDescription = stringResource(R.string.back)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package protect.card_locker.compose.theme
|
||||
|
||||
import android.os.Build
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.darkColorScheme
|
||||
import androidx.compose.material3.dynamicDarkColorScheme
|
||||
import androidx.compose.material3.dynamicLightColorScheme
|
||||
import androidx.compose.material3.lightColorScheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.colorResource
|
||||
import protect.card_locker.R
|
||||
import protect.card_locker.preferences.Settings
|
||||
|
||||
@Composable
|
||||
fun CatimaTheme(content: @Composable () -> Unit) {
|
||||
val context = LocalContext.current
|
||||
val settings = Settings(context)
|
||||
|
||||
val isDynamicColorSupported = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
|
||||
|
||||
val lightTheme = if (isDynamicColorSupported) {
|
||||
dynamicLightColorScheme(context)
|
||||
} else {
|
||||
lightColorScheme(primary = colorResource(id = R.color.md_theme_light_primary))
|
||||
}
|
||||
|
||||
var darkTheme = if (isDynamicColorSupported) {
|
||||
dynamicDarkColorScheme(context)
|
||||
} else {
|
||||
darkColorScheme(primary = colorResource(id = R.color.md_theme_dark_primary))
|
||||
}
|
||||
|
||||
if (settings.oledDark) {
|
||||
darkTheme = darkTheme.copy(background = Color.Black)
|
||||
}
|
||||
|
||||
val colorScheme = when (settings.theme) {
|
||||
AppCompatDelegate.MODE_NIGHT_NO -> lightTheme
|
||||
AppCompatDelegate.MODE_NIGHT_YES -> darkTheme
|
||||
else -> if (isSystemInDarkTheme()) darkTheme else lightTheme
|
||||
}
|
||||
|
||||
MaterialTheme(
|
||||
colorScheme = colorScheme,
|
||||
content = content
|
||||
)
|
||||
}
|
||||
@@ -32,7 +32,7 @@ public class CardsContentProvider extends ContentProvider {
|
||||
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;
|
||||
public static final int MINOR = 1;
|
||||
}
|
||||
|
||||
private static final int URI_VERSION = 0;
|
||||
@@ -52,6 +52,7 @@ public class CardsContentProvider extends ContentProvider {
|
||||
LoyaltyCardDbIds.CARD_ID,
|
||||
LoyaltyCardDbIds.BARCODE_ID,
|
||||
LoyaltyCardDbIds.BARCODE_TYPE,
|
||||
LoyaltyCardDbIds.BARCODE_ENCODING,
|
||||
LoyaltyCardDbIds.STAR_STATUS,
|
||||
LoyaltyCardDbIds.LAST_USED,
|
||||
LoyaltyCardDbIds.ARCHIVE_STATUS,
|
||||
@@ -76,13 +77,6 @@ public class CardsContentProvider extends ContentProvider {
|
||||
@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");
|
||||
|
||||
@@ -134,6 +134,7 @@ public class CatimaExporter implements Exporter {
|
||||
DBHelper.LoyaltyCardDbIds.CARD_ID,
|
||||
DBHelper.LoyaltyCardDbIds.BARCODE_ID,
|
||||
DBHelper.LoyaltyCardDbIds.BARCODE_TYPE,
|
||||
DBHelper.LoyaltyCardDbIds.BARCODE_ENCODING,
|
||||
DBHelper.LoyaltyCardDbIds.HEADER_COLOR,
|
||||
DBHelper.LoyaltyCardDbIds.STAR_STATUS,
|
||||
DBHelper.LoyaltyCardDbIds.LAST_USED,
|
||||
@@ -154,6 +155,7 @@ public class CatimaExporter implements Exporter {
|
||||
card.cardId,
|
||||
card.barcodeId,
|
||||
card.barcodeType != null ? card.barcodeType.name() : "",
|
||||
card.barcodeEncoding.name(),
|
||||
card.headerColor,
|
||||
card.starStatus,
|
||||
card.lastUsed,
|
||||
|
||||
@@ -20,6 +20,7 @@ import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.StringReader;
|
||||
import java.math.BigDecimal;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Currency;
|
||||
@@ -127,10 +128,10 @@ public class CatimaImporter implements Importer {
|
||||
LoyaltyCard existing = DBHelper.getLoyaltyCard(context, database, card.id);
|
||||
if (existing == null) {
|
||||
DBHelper.insertLoyaltyCard(database, card.id, card.store, card.note, card.validFrom, card.expiry, card.balance, card.balanceType,
|
||||
card.cardId, card.barcodeId, card.barcodeType, card.headerColor, card.starStatus, card.lastUsed, card.archiveStatus);
|
||||
card.cardId, card.barcodeId, card.barcodeType, card.barcodeEncoding, 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);
|
||||
card.cardId, card.barcodeId, card.barcodeType, card.barcodeEncoding, card.headerColor, card.starStatus, card.lastUsed, card.archiveStatus);
|
||||
idMap.put(card.id, (int) newId);
|
||||
}
|
||||
}
|
||||
@@ -458,6 +459,15 @@ public class CatimaImporter implements Importer {
|
||||
barcodeType = CatimaBarcode.fromName(unparsedBarcodeType);
|
||||
}
|
||||
|
||||
// Barcode encoding information is only supported since Catima 2.41.0, so old exports will lack this field
|
||||
// Database migration 19 forcing ISO-8859-1 on all cards, so we should treat imports from old Catima versions the same and force ISO-8859-1 there too
|
||||
// This limits the risk of breaking cards when importing an old database backup
|
||||
Charset barcodeEncoding = StandardCharsets.ISO_8859_1;
|
||||
String unparsedBarcodeEncoding = CSVHelpers.extractString(DBHelper.LoyaltyCardDbIds.BARCODE_ENCODING, record, "");
|
||||
if (!unparsedBarcodeEncoding.isEmpty()) {
|
||||
barcodeEncoding = Charset.forName(unparsedBarcodeEncoding);
|
||||
}
|
||||
|
||||
Integer headerColor = null;
|
||||
try {
|
||||
headerColor = CSVHelpers.extractInt(DBHelper.LoyaltyCardDbIds.HEADER_COLOR, record);
|
||||
@@ -501,6 +511,7 @@ public class CatimaImporter implements Importer {
|
||||
cardId,
|
||||
barcodeId,
|
||||
barcodeType,
|
||||
barcodeEncoding,
|
||||
headerColor,
|
||||
starStatus,
|
||||
lastUsed,
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
package protect.card_locker.importexport;
|
||||
|
||||
public enum DataFormat {
|
||||
Catima,
|
||||
Fidme,
|
||||
VoucherVault;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package protect.card_locker.importexport
|
||||
|
||||
enum class DataFormat {
|
||||
Catima,
|
||||
Fidme,
|
||||
VoucherVault
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
package protect.card_locker.importexport;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
|
||||
/**
|
||||
* Interface for a class which can export the contents of the database
|
||||
* in a given format.
|
||||
*/
|
||||
public interface Exporter {
|
||||
/**
|
||||
* Export the database to the output stream in a given format.
|
||||
*
|
||||
* @throws IOException
|
||||
*/
|
||||
void exportData(Context context, SQLiteDatabase database, OutputStream output, char[] password) throws IOException, InterruptedException;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package protect.card_locker.importexport
|
||||
|
||||
import android.content.Context
|
||||
import android.database.sqlite.SQLiteDatabase
|
||||
import java.io.IOException
|
||||
import java.io.OutputStream
|
||||
|
||||
/**
|
||||
* Interface for a class which can export the contents of the database
|
||||
* in a given format.
|
||||
*/
|
||||
interface Exporter {
|
||||
/**
|
||||
* Export the database to the output stream in a given format.
|
||||
*
|
||||
* @throws IOException, InterruptedException
|
||||
*/
|
||||
@Throws(IOException::class, InterruptedException::class)
|
||||
fun exportData(
|
||||
context: Context,
|
||||
database: SQLiteDatabase,
|
||||
output: OutputStream,
|
||||
password: CharArray
|
||||
)
|
||||
}
|
||||
@@ -160,6 +160,7 @@ public class FidmeImporter implements Importer {
|
||||
cardId,
|
||||
null,
|
||||
barcodeType,
|
||||
StandardCharsets.ISO_8859_1,
|
||||
headerColor,
|
||||
starStatus,
|
||||
Utils.getUnixTime(),
|
||||
@@ -181,7 +182,7 @@ public class FidmeImporter implements Importer {
|
||||
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);
|
||||
card.cardId, card.barcodeId, card.barcodeType, card.barcodeEncoding, card.headerColor, card.starStatus, card.lastUsed, card.archiveStatus);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
package protect.card_locker.importexport;
|
||||
|
||||
public enum ImportExportResultType {
|
||||
Success,
|
||||
GenericFailure,
|
||||
BadPassword;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package protect.card_locker.importexport
|
||||
|
||||
enum class ImportExportResultType {
|
||||
Success,
|
||||
GenericFailure,
|
||||
BadPassword
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
package protect.card_locker.importexport;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
|
||||
import org.json.JSONException;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.text.ParseException;
|
||||
|
||||
import protect.card_locker.FormatException;
|
||||
|
||||
/**
|
||||
* Interface for a class which can import the contents of a stream
|
||||
* into the database.
|
||||
*/
|
||||
public interface Importer {
|
||||
/**
|
||||
* Import data from the input stream in a given format into
|
||||
* the database.
|
||||
*
|
||||
* @throws IOException
|
||||
* @throws FormatException
|
||||
*/
|
||||
void importData(Context context, SQLiteDatabase database, File inputFile, char[] password) throws IOException, FormatException, InterruptedException, JSONException, ParseException;
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package protect.card_locker.importexport
|
||||
|
||||
import android.content.Context
|
||||
import android.database.sqlite.SQLiteDatabase
|
||||
import org.json.JSONException
|
||||
import protect.card_locker.FormatException
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.text.ParseException
|
||||
|
||||
/**
|
||||
* Interface for a class which can import the contents of a stream
|
||||
* into the database.
|
||||
*/
|
||||
interface Importer {
|
||||
/**
|
||||
* Import data from the input stream in a given format into
|
||||
* the database.
|
||||
*
|
||||
* @throws IOException
|
||||
* @throws FormatException
|
||||
* @throws InterruptedException
|
||||
* @throws JSONException
|
||||
* @throws ParseException
|
||||
*/
|
||||
@Throws(
|
||||
IOException::class,
|
||||
FormatException::class,
|
||||
InterruptedException::class,
|
||||
JSONException::class,
|
||||
ParseException::class
|
||||
)
|
||||
fun importData(
|
||||
context: Context,
|
||||
database: SQLiteDatabase,
|
||||
inputFile: File,
|
||||
password: CharArray
|
||||
)
|
||||
}
|
||||
@@ -162,6 +162,7 @@ public class VoucherVaultImporter implements Importer {
|
||||
cardId,
|
||||
null,
|
||||
barcodeType,
|
||||
StandardCharsets.ISO_8859_1,
|
||||
headerColor,
|
||||
0,
|
||||
Utils.getUnixTime(),
|
||||
@@ -186,7 +187,7 @@ public class VoucherVaultImporter implements Importer {
|
||||
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);
|
||||
card.cardId, card.barcodeId, card.barcodeType, card.barcodeEncoding, card.headerColor, card.starStatus, card.lastUsed, card.archiveStatus);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -82,6 +82,10 @@ public class Settings {
|
||||
return getBoolean(R.string.settings_key_disable_lockscreen_while_viewing_card, true);
|
||||
}
|
||||
|
||||
public boolean getDisableNfcWhileViewingCard() {
|
||||
return getBoolean(R.string.settings_key_disable_nfc_while_viewing_card, true);
|
||||
}
|
||||
|
||||
public boolean getAllowContentProviderRead() {
|
||||
return getBoolean(R.string.settings_key_allow_content_provider_read, true);
|
||||
}
|
||||
@@ -90,10 +94,6 @@ public class Settings {
|
||||
return getBoolean(R.string.settings_key_oled_dark, false);
|
||||
}
|
||||
|
||||
public String getColor() {
|
||||
return getString(R.string.setting_key_theme_color, mContext.getResources().getString(R.string.settings_key_system_theme));
|
||||
}
|
||||
|
||||
public int getPreferredColumnCount() {
|
||||
var defaultSymbol = mContext.getResources().getString(R.string.settings_key_automatic_column_count);
|
||||
var defaultColumnCount = mContext.getResources().getInteger(R.integer.main_view_card_columns);
|
||||
|
||||
@@ -1,212 +0,0 @@
|
||||
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 androidx.activity.OnBackPressedCallback;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AppCompatDelegate;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.core.os.LocaleListCompat;
|
||||
import androidx.preference.ListPreference;
|
||||
import androidx.preference.Preference;
|
||||
import androidx.preference.PreferenceFragmentCompat;
|
||||
|
||||
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.BuildConfig;
|
||||
import protect.card_locker.CatimaAppCompatActivity;
|
||||
import protect.card_locker.MainActivity;
|
||||
import protect.card_locker.R;
|
||||
import protect.card_locker.Utils;
|
||||
import protect.card_locker.databinding.SettingsActivityBinding;
|
||||
|
||||
public class SettingsActivity extends CatimaAppCompatActivity {
|
||||
|
||||
private SettingsActivityBinding binding;
|
||||
private final static String RELOAD_MAIN_STATE = "mReloadMain";
|
||||
private SettingsFragment fragment;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
binding = SettingsActivityBinding.inflate(getLayoutInflater());
|
||||
setTitle(R.string.settings);
|
||||
setContentView(binding.getRoot());
|
||||
Utils.applyWindowInsets(binding.getRoot());
|
||||
Toolbar toolbar = binding.toolbar;
|
||||
setSupportActionBar(toolbar);
|
||||
enableToolbarBackButton();
|
||||
|
||||
// Display the fragment as the main content.
|
||||
fragment = new SettingsFragment();
|
||||
getSupportFragmentManager().beginTransaction()
|
||||
.replace(R.id.settings_container, fragment)
|
||||
.commit();
|
||||
|
||||
// restore reload main state
|
||||
if (savedInstanceState != null) {
|
||||
fragment.mReloadMain = savedInstanceState.getBoolean(RELOAD_MAIN_STATE);
|
||||
}
|
||||
|
||||
getOnBackPressedDispatcher().addCallback(this, new OnBackPressedCallback(true) {
|
||||
@Override
|
||||
public void handleOnBackPressed() {
|
||||
finishSettingsActivity();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSaveInstanceState(@NonNull Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
outState.putBoolean(RELOAD_MAIN_STATE, fragment.mReloadMain);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
int id = item.getItemId();
|
||||
|
||||
if (id == android.R.id.home) {
|
||||
finishSettingsActivity();
|
||||
return true;
|
||||
}
|
||||
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
private void finishSettingsActivity() {
|
||||
if (fragment.mReloadMain) {
|
||||
Intent intent = new Intent();
|
||||
intent.putExtra(MainActivity.RESTART_ACTIVITY_INTENT, true);
|
||||
setResult(Activity.RESULT_OK, intent);
|
||||
} else {
|
||||
setResult(Activity.RESULT_OK);
|
||||
}
|
||||
finish();
|
||||
}
|
||||
|
||||
public static class SettingsFragment extends PreferenceFragmentCompat {
|
||||
private static final String DIALOG_FRAGMENT_TAG = "SettingsFragment";
|
||||
|
||||
public boolean mReloadMain;
|
||||
|
||||
@Override
|
||||
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
|
||||
// Load the preferences from an XML resource
|
||||
addPreferencesFromResource(R.xml.preferences);
|
||||
|
||||
// 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))) {
|
||||
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO);
|
||||
} else if (o.toString().equals(getResources().getString(R.string.settings_key_dark_theme))) {
|
||||
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES);
|
||||
} else {
|
||||
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM);
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
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;
|
||||
oledDarkPreference.setOnPreferenceChangeListener((preference, newValue) -> {
|
||||
refreshActivity(true);
|
||||
return 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;
|
||||
});
|
||||
|
||||
// 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);
|
||||
|
||||
// Hide crash reporter settings on builds it's not enabled on
|
||||
Preference crashReporterPreference = findPreference("acra.enable");
|
||||
assert crashReporterPreference != null;
|
||||
crashReporterPreference.setVisible(BuildConfig.useAcraCrashReporter);
|
||||
}
|
||||
|
||||
private void refreshActivity(boolean reloadMain) {
|
||||
mReloadMain = reloadMain || mReloadMain;
|
||||
Activity activity = getActivity();
|
||||
if (activity != null) {
|
||||
activity.recreate();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
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 androidx.activity.OnBackPressedCallback
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.core.os.LocaleListCompat
|
||||
import androidx.preference.ListPreference
|
||||
import androidx.preference.Preference
|
||||
import androidx.preference.PreferenceFragmentCompat
|
||||
import protect.card_locker.BuildConfig
|
||||
import protect.card_locker.CatimaAppCompatActivity
|
||||
import protect.card_locker.MainActivity
|
||||
import protect.card_locker.R
|
||||
import protect.card_locker.Utils
|
||||
import protect.card_locker.databinding.SettingsActivityBinding
|
||||
|
||||
class SettingsActivity : CatimaAppCompatActivity() {
|
||||
|
||||
private lateinit var binding: SettingsActivityBinding
|
||||
private lateinit var fragment: SettingsFragment
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = SettingsActivityBinding.inflate(layoutInflater)
|
||||
setTitle(R.string.settings)
|
||||
setContentView(binding.root)
|
||||
Utils.applyWindowInsets(binding.root)
|
||||
val toolbar = binding.toolbar
|
||||
setSupportActionBar(toolbar)
|
||||
enableToolbarBackButton()
|
||||
|
||||
// Display the fragment as the main content.
|
||||
fragment = SettingsFragment()
|
||||
supportFragmentManager.beginTransaction()
|
||||
.replace(R.id.settings_container, fragment)
|
||||
.commit()
|
||||
|
||||
// restore reload main state
|
||||
if (savedInstanceState != null) {
|
||||
fragment.mReloadMain = savedInstanceState.getBoolean(RELOAD_MAIN_STATE)
|
||||
}
|
||||
|
||||
onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
|
||||
override fun handleOnBackPressed() {
|
||||
finishSettingsActivity()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
super.onSaveInstanceState(outState)
|
||||
outState.putBoolean(RELOAD_MAIN_STATE, fragment.mReloadMain)
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
val id = item.itemId
|
||||
|
||||
if (id == android.R.id.home) {
|
||||
finishSettingsActivity()
|
||||
return true
|
||||
}
|
||||
|
||||
return super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
private fun finishSettingsActivity() {
|
||||
if (fragment.mReloadMain) {
|
||||
val intent = Intent()
|
||||
intent.putExtra(MainActivity.RESTART_ACTIVITY_INTENT, true)
|
||||
setResult(RESULT_OK, intent)
|
||||
} else {
|
||||
setResult(RESULT_OK)
|
||||
}
|
||||
finish()
|
||||
}
|
||||
|
||||
class SettingsFragment : PreferenceFragmentCompat() {
|
||||
var mReloadMain: Boolean = false
|
||||
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
// Load the preferences from an XML resource
|
||||
addPreferencesFromResource(R.xml.preferences)
|
||||
|
||||
// Show pretty names and summaries
|
||||
val themePreference = findPreference<ListPreference>(getString(R.string.settings_key_theme))
|
||||
themePreference!!.setOnPreferenceChangeListener { _, o ->
|
||||
when (o.toString()) {
|
||||
getString(R.string.settings_key_light_theme) -> {
|
||||
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO)
|
||||
}
|
||||
getString(R.string.settings_key_dark_theme) -> {
|
||||
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES)
|
||||
}
|
||||
else -> {
|
||||
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
val oledDarkPreference = findPreference<Preference>(getString(R.string.settings_key_oled_dark))
|
||||
oledDarkPreference!!.setOnPreferenceChangeListener { _, _ ->
|
||||
refreshActivity(true)
|
||||
true
|
||||
}
|
||||
|
||||
val localePreference =
|
||||
findPreference<ListPreference>(getString(R.string.settings_key_locale))!!
|
||||
localePreference.let {
|
||||
val entryValues = it.entryValues
|
||||
val entries = entryValues.map { entry ->
|
||||
if (entry.isEmpty()) {
|
||||
getString(R.string.settings_system_locale)
|
||||
} else {
|
||||
val entryLocale = Utils.stringToLocale(entry.toString())
|
||||
entryLocale.getDisplayName(entryLocale)
|
||||
}
|
||||
}
|
||||
it.entries = entries.toTypedArray()
|
||||
|
||||
// Make locale picker preference in sync with system settings
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
val sysLocale = AppCompatDelegate.getApplicationLocales()[0]
|
||||
if (sysLocale == null) {
|
||||
// Corresponds to "System"
|
||||
it.value = ""
|
||||
} 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.
|
||||
val appLocales = entryValues.map { entry -> Utils.stringToLocale(entry.toString()) }
|
||||
// Get the app locale that best matches the system one
|
||||
val bestMatchLocale = Utils.getBestMatchLocale(appLocales, sysLocale)
|
||||
// Get its index in supported locales
|
||||
val index = appLocales.indexOf(bestMatchLocale)
|
||||
// Set preference value to entry value at that index
|
||||
it.value = entryValues[index].toString()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
localePreference.setOnPreferenceChangeListener { _, newValue ->
|
||||
// See corresponding comment in Utils.updateBaseContextLocale for Android 6- notes
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
|
||||
refreshActivity(true)
|
||||
return@setOnPreferenceChangeListener true
|
||||
}
|
||||
val newLocale = newValue as String
|
||||
// If newLocale is empty, that means "System" was selected
|
||||
AppCompatDelegate.setApplicationLocales(if (newLocale.isEmpty()) LocaleListCompat.getEmptyLocaleList() else LocaleListCompat.create(Utils.stringToLocale(newLocale)))
|
||||
true
|
||||
}
|
||||
|
||||
// Hide crash reporter settings on builds it's not enabled on
|
||||
val crashReporterPreference = findPreference<Preference>("acra.enable")
|
||||
crashReporterPreference!!.isVisible = BuildConfig.useAcraCrashReporter
|
||||
}
|
||||
|
||||
private fun refreshActivity(reloadMain: Boolean) {
|
||||
mReloadMain = reloadMain || mReloadMain
|
||||
activity?.recreate()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val RELOAD_MAIN_STATE = "mReloadMain"
|
||||
}
|
||||
}
|
||||
@@ -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="M2,6h2v12L2,18L2,6zM5,6h1v12L5,18L5,6zM7,6h3v12L7,18L7,6zM11,6h1v12h-1L11,6zM13,6h2v12h-2L13,6zM16,6h2v12h-2L16,6zM19,6h1v12h-1L19,6zM21,6h1v12h-1L21,6z"/>
|
||||
</vector>
|
||||
@@ -1,421 +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"
|
||||
android:fitsSystemWindows="true">
|
||||
|
||||
<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.appbar.AppBarLayout>
|
||||
|
||||
<androidx.core.widget.NestedScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginTop="?attr/actionBarSize"
|
||||
android:paddingVertical="8dp"
|
||||
android:clipToPadding="false">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/version_history"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingVertical="8dp"
|
||||
android:paddingHorizontal="16dp"
|
||||
android:background="?android:selectableItemBackground">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/version_history_main"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:fontFamily="sans-serif-medium"
|
||||
android:paddingStart="2dp"
|
||||
android:paddingEnd="30dp"
|
||||
android:text="@string/version_history"
|
||||
android:textSize="18sp"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/version_history_sub"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingStart="2dp"
|
||||
android:paddingEnd="30dp"
|
||||
android:textSize="16sp"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/version_history_main" />
|
||||
|
||||
<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/credits"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingVertical="8dp"
|
||||
android:paddingHorizontal="16dp"
|
||||
android:background="?android:selectableItemBackground">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/credits_main"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:fontFamily="sans-serif-medium"
|
||||
android:paddingStart="2dp"
|
||||
android:paddingEnd="30dp"
|
||||
android:text="@string/credits"
|
||||
android:textSize="18sp"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/credits_sub"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingStart="2dp"
|
||||
android:paddingEnd="30dp"
|
||||
android:textSize="16sp"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/credits_main" />
|
||||
|
||||
<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/translate"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingVertical="8dp"
|
||||
android:paddingHorizontal="16dp"
|
||||
android:background="?android:selectableItemBackground">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/translate_main"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:fontFamily="sans-serif-medium"
|
||||
android:paddingStart="2dp"
|
||||
android:paddingEnd="30dp"
|
||||
android:text="@string/help_translate_this_app"
|
||||
android:textSize="18sp"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/translate_sub"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingStart="2dp"
|
||||
android:paddingEnd="30dp"
|
||||
android:text="@string/translate_platform"
|
||||
android:textSize="16sp"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/translate_main"/>
|
||||
|
||||
<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/license"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingVertical="8dp"
|
||||
android:paddingHorizontal="16dp"
|
||||
android:background="?android:selectableItemBackground">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/license_main"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:fontFamily="sans-serif-medium"
|
||||
android:paddingStart="2dp"
|
||||
android:paddingEnd="30dp"
|
||||
android:text="@string/license"
|
||||
android:textSize="18sp"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/license_sub"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingStart="2dp"
|
||||
android:paddingEnd="30dp"
|
||||
android:text="@string/app_license"
|
||||
android:textSize="16sp"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/license_main"/>
|
||||
|
||||
<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/repo"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingVertical="8dp"
|
||||
android:paddingHorizontal="16dp"
|
||||
android:background="?android:selectableItemBackground">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/repo_main"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:fontFamily="sans-serif-medium"
|
||||
android:paddingStart="2dp"
|
||||
android:paddingEnd="30dp"
|
||||
android:text="@string/source_repository"
|
||||
android:textSize="18sp"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/repo_sub"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingStart="2dp"
|
||||
android:paddingEnd="30dp"
|
||||
android:text="@string/on_github"
|
||||
android:textSize="16sp"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/repo_main" />
|
||||
|
||||
<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/privacy"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingVertical="8dp"
|
||||
android:paddingHorizontal="16dp"
|
||||
android:background="?android:selectableItemBackground">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/privacy_main"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:fontFamily="sans-serif-medium"
|
||||
android:paddingStart="2dp"
|
||||
android:paddingEnd="30dp"
|
||||
android:text="@string/privacy_policy"
|
||||
android:textSize="18sp"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/privacy_sub"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingStart="2dp"
|
||||
android:paddingEnd="30dp"
|
||||
android:text="@string/and_data_usage"
|
||||
android:textSize="16sp"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/privacy_main" />
|
||||
|
||||
<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/donate"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingVertical="8dp"
|
||||
android:paddingHorizontal="16dp"
|
||||
android:background="?android:selectableItemBackground">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/donate_main"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:fontFamily="sans-serif-medium"
|
||||
android:paddingStart="2dp"
|
||||
android:paddingEnd="30dp"
|
||||
android:text="@string/donate"
|
||||
android:textSize="18sp"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="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"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingVertical="8dp"
|
||||
android:paddingHorizontal="16dp"
|
||||
android:background="?android:selectableItemBackground">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/rate_main"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:fontFamily="sans-serif-medium"
|
||||
android:paddingStart="2dp"
|
||||
android:paddingEnd="30dp"
|
||||
android:text="@string/rate_this_app"
|
||||
android:textSize="18sp"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/rate_sub"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingStart="2dp"
|
||||
android:paddingEnd="30dp"
|
||||
android:text="@string/on_google_play"
|
||||
android:textSize="16sp"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/rate_main" />
|
||||
|
||||
<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/report_error"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingVertical="8dp"
|
||||
android:paddingHorizontal="16dp"
|
||||
android:background="?android:selectableItemBackground">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/report_error_main"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:fontFamily="sans-serif-medium"
|
||||
android:paddingStart="2dp"
|
||||
android:paddingEnd="30dp"
|
||||
android:text="@string/report_error"
|
||||
android:textSize="18sp"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/report_error_sub"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintTop_toBottomOf="@id/report_error_main"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
android:paddingStart="2dp"
|
||||
android:paddingEnd="30dp"
|
||||
android:textSize="16sp"
|
||||
android:text="@string/on_github" />
|
||||
|
||||
<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>
|
||||
</LinearLayout>
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
@@ -109,7 +109,8 @@
|
||||
android:id="@+id/storeNameEdit"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:singleLine="true" />
|
||||
android:singleLine="true"
|
||||
android:inputType="textCapWords" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
</LinearLayout>
|
||||
@@ -191,6 +192,32 @@
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
</LinearLayout>
|
||||
|
||||
<!-- Barcode encoding -->
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingHorizontal="@dimen/inputPadding"
|
||||
android:paddingTop="@dimen/inputPadding"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<!-- Barcode type -->
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/barcodeEncodingView"
|
||||
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.ExposedDropdownMenu"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1.0"
|
||||
android:hint="@string/barcodeEncoding"
|
||||
android:labelFor="@+id/barcodeEncodingField">
|
||||
|
||||
<AutoCompleteTextView
|
||||
android:id="@+id/barcodeEncodingField"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="none"/>
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
</LinearLayout>
|
||||
|
||||
<!-- Barcode -->
|
||||
<LinearLayout android:orientation="horizontal"
|
||||
android:layout_marginTop="10.0dp"
|
||||
@@ -276,6 +303,24 @@
|
||||
android:paddingTop="@dimen/inputPadding"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<!-- Currency -->
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/balanceCurrencyView"
|
||||
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.ExposedDropdownMenu"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1.0"
|
||||
android:hint="@string/currency"
|
||||
android:labelFor="@+id/balanceCurrencyField">
|
||||
|
||||
<AutoCompleteTextView
|
||||
android:id="@+id/balanceCurrencyField"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="none"/>
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<!-- Balance -->
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/balanceView"
|
||||
@@ -294,24 +339,6 @@
|
||||
android:digits="0123456789,." />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<!-- Currency -->
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/balanceCurrencyView"
|
||||
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.ExposedDropdownMenu"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1.0"
|
||||
android:hint="@string/currency"
|
||||
android:labelFor="@+id/balanceCurrencyField">
|
||||
|
||||
<AutoCompleteTextView
|
||||
android:id="@+id/balanceCurrencyField"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="none"/>
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
</LinearLayout>
|
||||
|
||||
<!-- Valid from -->
|
||||
|
||||
@@ -158,10 +158,31 @@
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible"
|
||||
app:layout_constraintTop_toBottomOf="@+id/note"
|
||||
app:layout_constraintBottom_toTopOf="@+id/balance"
|
||||
app:layout_constraintBottom_toTopOf="@+id/cardId"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/cardId"
|
||||
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_card_id_24dp"
|
||||
android:drawablePadding="4dp"
|
||||
android:maxLines="1"
|
||||
android:ellipsize="end"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintTop_toBottomOf="@+id/info_divider"
|
||||
app:layout_constraintBottom_toTopOf="@+id/balance"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
tools:visibility="visible"
|
||||
tools:text="1234567890"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/balance"
|
||||
android:layout_width="match_parent"
|
||||
@@ -174,7 +195,7 @@
|
||||
app:drawableLeftCompat="@drawable/ic_baseline_payments_24"
|
||||
android:drawablePadding="4dp"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintTop_toBottomOf="@+id/info_divider"
|
||||
app:layout_constraintTop_toBottomOf="@+id/cardId"
|
||||
app:layout_constraintBottom_toTopOf="@+id/validFrom"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
|
||||
@@ -40,10 +40,16 @@
|
||||
android:titleCondensed="@string/unarchive"
|
||||
app:showAsAction="never"/>
|
||||
|
||||
<item
|
||||
android:id="@+id/action_duplicate"
|
||||
android:title="@string/duplicateCard"
|
||||
android:titleCondensed="@string/duplicateCard"
|
||||
app:showAsAction="never" />
|
||||
|
||||
<item
|
||||
android:id="@+id/action_delete"
|
||||
android:icon="@drawable/ic_delete_white_24dp"
|
||||
android:title="@string/delete"
|
||||
android:titleCondensed="@string/delete"
|
||||
app:showAsAction="never"/>
|
||||
</menu>
|
||||
</menu>
|
||||
@@ -21,25 +21,25 @@
|
||||
app:showAsAction="always">
|
||||
|
||||
<menu>
|
||||
<item
|
||||
android:id="@+id/action_archive"
|
||||
android:title="@string/archive"
|
||||
app:showAsAction="never"/>
|
||||
|
||||
<item
|
||||
android:id="@+id/action_unarchive"
|
||||
android:title="@string/unarchive"
|
||||
app:showAsAction="never"/>
|
||||
|
||||
<item
|
||||
android:id="@+id/action_duplicate"
|
||||
android:title="@string/duplicateCard"
|
||||
app:showAsAction="never" />
|
||||
|
||||
<item
|
||||
android:id="@+id/action_archive"
|
||||
android:title="@string/archive"
|
||||
app:showAsAction="never"/>
|
||||
<item
|
||||
android:id="@+id/action_unarchive"
|
||||
android:title="@string/unarchive"
|
||||
app:showAsAction="never"/>
|
||||
<item
|
||||
android:id="@+id/action_delete"
|
||||
android:title="@string/delete"
|
||||
app:showAsAction="never"/>
|
||||
|
||||
</menu>
|
||||
|
||||
</item>
|
||||
|
||||
@@ -6,84 +6,101 @@ Allan Nordhøy
|
||||
Heimen Stoffels
|
||||
Oğuz Ersen
|
||||
FC (Fay) Stegerman
|
||||
大王叫我来巡山
|
||||
B o d o
|
||||
StoyanDimitrov
|
||||
SlavekB
|
||||
Katharine Chui
|
||||
大王叫我来巡山
|
||||
Katharine Chui / Katie
|
||||
Silvério Santos
|
||||
mondstern
|
||||
IllusiveMan196
|
||||
B o d o
|
||||
Altonss
|
||||
Silvério Santos
|
||||
Michael Moroni
|
||||
Edgars Andersons
|
||||
Eric
|
||||
Joel A
|
||||
Priit Jõerüüt
|
||||
Максим Горпиніч
|
||||
Edgars Andersons
|
||||
Liner Seven
|
||||
Altonss
|
||||
Fjuro
|
||||
Joel A
|
||||
GitSpoon
|
||||
Michael Moroni
|
||||
Priit Jõerüüt
|
||||
Eric
|
||||
GM
|
||||
laralem
|
||||
Petr Novák
|
||||
Taco
|
||||
GitSpoon
|
||||
Milo Ivir
|
||||
nadiafekihahmed
|
||||
pfaffenrodt
|
||||
Fjuro
|
||||
Vasilis
|
||||
Aayush Gupta
|
||||
Scrambled777
|
||||
josé m
|
||||
ikanakova
|
||||
Giovanni Donisi
|
||||
HudobniVolk
|
||||
Jiri Grönroos
|
||||
Nyatsuki
|
||||
Warder
|
||||
Giovanni Donisi
|
||||
Kachelkaiser
|
||||
Milo Ivir
|
||||
Vasilis
|
||||
Samantaz Fox
|
||||
Jiri Grönroos
|
||||
HudobniVolk
|
||||
109247019824
|
||||
Warder
|
||||
Balázs Meskó
|
||||
Traductor
|
||||
Samantaz Fox
|
||||
mdvhimself
|
||||
AMIR-G98
|
||||
Cliff Heraldo
|
||||
Sergio Paredes
|
||||
Ankit Tiwari
|
||||
Feike
|
||||
Arno-github
|
||||
Jose Delvani
|
||||
mdvhimself
|
||||
Milan Šalka
|
||||
damjang
|
||||
Robin
|
||||
Skrripy
|
||||
huuhaa
|
||||
தமிழ்நேரம்
|
||||
huuhaa
|
||||
Skrripy
|
||||
Govindgopalyadav
|
||||
Gideon
|
||||
waffshappen
|
||||
Marnick L'Eau
|
||||
Горпиніч Максим Олександрович
|
||||
JungHee Lee
|
||||
pacavarn
|
||||
ngocanhtve
|
||||
aradxxx
|
||||
StellarSand
|
||||
Quentin PAGÈS
|
||||
Projjal Moitra
|
||||
109247019824
|
||||
JungHee Lee
|
||||
Aliaksandr Trush
|
||||
e-michalak
|
||||
தமிழ் நேரம்
|
||||
hajertabbane
|
||||
Ziad OUALHADJ
|
||||
Robin Liu
|
||||
Ricky Tigg
|
||||
Renko
|
||||
Denis Shilin
|
||||
Renko
|
||||
Ricky Tigg
|
||||
Robin Liu
|
||||
Ziad OUALHADJ
|
||||
inavleb
|
||||
しいたけ
|
||||
Alexander Ivanov
|
||||
Miha Frangež
|
||||
stavpup
|
||||
mrestivill
|
||||
ezn24
|
||||
ehrt74
|
||||
delvani
|
||||
Virginie
|
||||
Tim Trek
|
||||
Thu-Trang Pham
|
||||
Alì Mortacci
|
||||
Andre Costa
|
||||
arshbeerSingh
|
||||
Augustin LAVILLE
|
||||
Freddo espresso
|
||||
Peter Dave Hello
|
||||
PRATHAMESH BHAGAT
|
||||
Michael Gangolf
|
||||
rudy3
|
||||
echo r"0xX4H" | rev
|
||||
Kim Seohyun
|
||||
Govind S Nair
|
||||
Freddo espresso
|
||||
Augustin LAVILLE
|
||||
arshbeerSingh
|
||||
MisterCosta96
|
||||
Aliaksandr Trush
|
||||
Thomas Bertels
|
||||
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="app_name">Catima</string>
|
||||
<string name="action_search">Soek</string>
|
||||
<string name="action_add">Voeg by</string>
|
||||
<string name="save">Stoor</string>
|
||||
<plurals name="selectedCardCount">
|
||||
<item quantity="one"><xliff:g>%d</xliff:g> geselekteer</item>
|
||||
<item quantity="other"><xliff:g>%d</xliff:g> geselekteer</item>
|
||||
</plurals>
|
||||
</resources>
|
||||
@@ -1,40 +1,40 @@
|
||||
<?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="storeName">اسم</string>
|
||||
<string name="note">مذكرة</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="barcodeType">نوع الباركود</string>
|
||||
<string name="noBarcode">لا يوجد باركود</string>
|
||||
<string name="star">أضف الى المفضلة</string>
|
||||
<string name="unstar">حذف من المفضلة</string>
|
||||
<string name="star">إضافة إلى المفضلة</string>
|
||||
<string name="unstar">إزالة من المفضلة</string>
|
||||
<string name="cancel">إلغاء</string>
|
||||
<string name="save">حفظ</string>
|
||||
<string name="edit">تعديل</string>
|
||||
<string name="delete">مسح</string>
|
||||
<string name="delete">حذف</string>
|
||||
<string name="confirm">تأكيد</string>
|
||||
<string name="deleteConfirmation">مسح هذة البطاقة نهائيا؟</string>
|
||||
<string name="ok">حسنا</string>
|
||||
<string name="share">شارك</string>
|
||||
<string name="sendLabel">ارسل…</string>
|
||||
<string name="editCardTitle">عدل البطاقة</string>
|
||||
<string name="addCardTitle">اضف بطاقة</string>
|
||||
<string name="scanCardBarcode">مسح باركود</string>
|
||||
<string name="deleteConfirmation">حذف هذه البطاقة نهائيًّا؟</string>
|
||||
<string name="ok">حسنًا</string>
|
||||
<string name="share">مشاركة</string>
|
||||
<string name="sendLabel">إرسال…</string>
|
||||
<string name="editCardTitle">تعديل البطاقة</string>
|
||||
<string name="addCardTitle">إضافة بطاقة</string>
|
||||
<string name="scanCardBarcode">مسح الباركود</string>
|
||||
<string name="cardShortcut">اختصار البطاقة</string>
|
||||
<string name="noCardsMessage">اضف بطاقة أولا</string>
|
||||
<string name="noCardsMessage">أضف بطاقة أولًا</string>
|
||||
<string name="barcodeImageDescriptionWithType">صورة <xliff:g>%s</xliff:g> الباركود</string>
|
||||
<string name="noCardExistsError">لا يمكن العثور على هذه البطاقة</string>
|
||||
<string name="failedParsingImportUriError">لا يمكن تحليل الرابط المستورد</string>
|
||||
<string name="noCardExistsError">لم يتمّ العثور على تلك البطاقة</string>
|
||||
<string name="failedParsingImportUriError">لا يمكن تحليل عنوان URL المستورد</string>
|
||||
<string name="importExport">استيراد/تصدير</string>
|
||||
<string name="importExportHelp">انشاء نسخة احتياطية من بياناتك يسمح بنقلها إلى جهاز آخر.</string>
|
||||
<string name="importFailed">تعذر إجراء الاستيراد</string>
|
||||
<string name="exportSuccessfulTitle">متصدر</string>
|
||||
<string name="importExportHelp">تتيح لك عملية النسخ الاحتياطي لبياناتك نقلها إلى جهاز آخر</string>
|
||||
<string name="importFailed">تعذّر إجراء عملية الاستيراد</string>
|
||||
<string name="exportSuccessfulTitle">تمّ التصدير</string>
|
||||
<string name="exportFailedTitle">فشل التصدير</string>
|
||||
<string name="exportFailed">لا يمكن عمل التصدير</string>
|
||||
<string name="exportOptionExplanation">ستتم كتابة البيانات في الموقع الذي تختاره.</string>
|
||||
<string name="exportFailed">تعذّر إجراء عملية التصدير</string>
|
||||
<string name="exportOptionExplanation">ستكون البيانات مكتوبة في المسار الذي تختاره</string>
|
||||
<string name="importOptionFilesystemButton">من نظام الملفات</string>
|
||||
<string name="about">حول</string>
|
||||
<string name="app_copyright_old">بناء على Loyalty Card Keychain
|
||||
@@ -53,7 +53,7 @@
|
||||
<string name="exportSuccessful">تم تصدير البيانات</string>
|
||||
<string name="enter_group_name">أدخل اسم المجموعة</string>
|
||||
<string name="group_edit">تعديل المجموعة</string>
|
||||
<string name="noGroups">اضغط زر + لإضافة مجموعات للتصنيف.</string>
|
||||
<string name="noGroups">اضغط زر + لإضافة مجموعات للتصنيف</string>
|
||||
<string name="noGroupCards">هذه المجموعة فارغة</string>
|
||||
<string name="group_name_already_in_use">اسم المجموعة قيد الاستخدام بالفعل</string>
|
||||
<string name="group_name_is_empty">لا يمكن أن يكون اسم المجموعة فارغًا</string>
|
||||
@@ -71,11 +71,11 @@
|
||||
<string name="privacy_policy">سياسة الخصوصية</string>
|
||||
<string name="accept">قبول</string>
|
||||
<string name="importCatima">الاستيراد من Catima</string>
|
||||
<string name="importCatimaMessage">حدّد ملفك <i>catima.zip</i> تصدير من Catima للاستيراد. \nإنشئها من قائمة الاستيراد / التصدير لتطبيق Catima آخر بالضغط على تصدير هناك أولاً.</string>
|
||||
<string name="importCatimaMessage">حدّد ملفك تصدير من Catima للاستيراد.\nإنشئها من قائمة الاستيراد / التصدير لتطبيق Catima آخر بالضغط على تصدير .</string>
|
||||
<string name="importFidme">الاستيراد من FidMe</string>
|
||||
<string name="importFidmeMessage">حدّد ملفك <i>fidme-export-request-xxxxxx.zip</i> تصدير من FidMe للاستيراد، ثم حدد أنواع الباركود يدويًا بعد ذلك. \nإنشئها من ملف تعريف FidMe الخاص بك عن طريق اختيار حماية البيانات ثم الضغط على استخراج بياناتي أولاً.</string>
|
||||
<string name="importFidmeMessage">حدّد للتصدير من FidMe للاستيراد، ثم حدد أنواع الباركود يدويًا بعد ذلك. \nقم بإنشاءها من ملفك التعريفي في FidMe عن طريق اختيار \"حماية البيانات\" ثم النقر على \"استخراج بياناتي\".</string>
|
||||
<string name="importVoucherVault">الاستيراد من Voucher Vault</string>
|
||||
<string name="importVoucherVaultMessage">حدّد ملفك <i>vouchervault.json</i> تصدير من Voucher Vault للاستيراد. \nإنشئها بالضغط على تصدير في Voucher Vault أولاً.</string>
|
||||
<string name="importVoucherVaultMessage">حدّد ملفك <i>vouchervault.json</i> تصدير من Voucher Vault للاستيراد.\nإنشئها بالضغط على تصدير في Voucher Vault.</string>
|
||||
<string name="barcodeId">قيمة الباركود</string>
|
||||
<string name="sameAsCardId">نفس بطاقة الهوية</string>
|
||||
<string name="setBarcodeId">عيّن قيمة الباركود</string>
|
||||
@@ -101,14 +101,6 @@
|
||||
<string name="settings_locale">لغة</string>
|
||||
<string name="settings_system_locale">النظام</string>
|
||||
<string name="setIcon">تعيين الصورة المصغرة</string>
|
||||
<string name="settings_catima_theme">Catima</string>
|
||||
<string name="settings_pink_theme">زهري</string>
|
||||
<string name="settings_magenta_theme">أرجواني</string>
|
||||
<string name="settings_violet_theme">البنفسجي</string>
|
||||
<string name="settings_blue_theme">أزرق</string>
|
||||
<string name="settings_sky_blue_theme">أزرق سماوي</string>
|
||||
<string name="settings_green_theme">أخضر</string>
|
||||
<string name="settings_brown_theme">بني</string>
|
||||
<string name="app_contributors">أصبح ممكنًا بواسطة: <xliff:g id="app_contributors">%s</xliff:g></string>
|
||||
<string name="sort">فرز</string>
|
||||
<string name="showMoreInfo">اظهر المعلومات</string>
|
||||
@@ -130,21 +122,21 @@
|
||||
<string name="archived">تمت أرشفة البطاقة</string>
|
||||
<string name="unarchived">البطاقة غير مؤرشفة</string>
|
||||
<string name="failedLaunchingPhotoPicker">تعذر العثور على تطبيق معرض مدعوم</string>
|
||||
<string name="noGiftCardsGroup">أنشئ بعض البطاقات، ثم عيّنهم لهذه المجموعة.</string>
|
||||
<string name="deleteTitle">مسح بطاقة</string>
|
||||
<string name="noGiftCardsGroup">أنشئ بعض البطاقات، ثم عيّنها للمجموعة هنا</string>
|
||||
<string name="deleteTitle">حذف البطاقة</string>
|
||||
<plurals name="selectedCardCount">
|
||||
<item quantity="zero"><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>
|
||||
<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="importing">جار الاستيراد…</string>
|
||||
<string name="importing">جارٍ الاستيراد…</string>
|
||||
<string name="exportName">تصدير</string>
|
||||
<string name="exporting">جار التصدير…</string>
|
||||
<string name="importSuccessfulTitle">مستورد</string>
|
||||
<string name="importFailedTitle">فشل بالاستيراد</string>
|
||||
<string name="importSuccessfulTitle">تمّ الاستيراد</string>
|
||||
<string name="importFailedTitle">فشل الاستيراد</string>
|
||||
<string name="sort_by_most_recently_used">الأكثر أستعمالا مؤخرا</string>
|
||||
<string name="license">رخصة</string>
|
||||
<string name="sort_by_name">اسم</string>
|
||||
@@ -158,23 +150,23 @@
|
||||
<item quantity="other"><xliff:g>%1$d</xliff:g> بطاقات ( <xliff:g id="archivedCount">%2$d</xliff:g> مؤرشفة)</item>
|
||||
</plurals>
|
||||
<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="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>
|
||||
<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>
|
||||
<string name="importOptionFilesystemExplanation">اختر ملفًا محددًا من نظام الملفات</string>
|
||||
<string name="about_title_fmt">حول <xliff:g id="app_name">%s</xliff:g></string>
|
||||
<string name="debug_version_fmt">نسخة: <xliff:g id="version">%s</xliff:g></string>
|
||||
<string name="settings_system_theme">النظام</string>
|
||||
@@ -221,15 +213,13 @@
|
||||
<string name="report_error">الإبلاغ عن خطأ</string>
|
||||
<string name="sort_by_expiry">انقضاء</string>
|
||||
<string name="importLoyaltyCardKeychain">الاستيراد من Loyalty Card Keychain</string>
|
||||
<string name="importLoyaltyCardKeychainMessage">حدّد ملفك <i>LoyaltyCardKeychain.csv</i> التصدير من Loyalty Card Keychain للاستيراد. \nإنشئها من قائمة الاستيراد / التصدير في Loyalty Card Keychain بالضغط على تصدير هناك أولاً.</string>
|
||||
<string name="importLoyaltyCardKeychainMessage">حدد تصديرك من سلسلة مفاتيح بطاقة الولاء للتصدير.\nانشئها من قائمة استيراد/تصدير في سلسلة مفاتيح بطاقة الولاء عبر ضغط زر تصدير.</string>
|
||||
<string name="failedGeneratingShareURL">تعذر إنشاء عنوان URL قابل للمشاركة</string>
|
||||
<string name="help_translate_this_app">ساعد في ترجمة هذا التطبيق</string>
|
||||
<string name="on_google_play">على Google Play</string>
|
||||
<string name="settings_theme_color">لون المظهر</string>
|
||||
<string name="previousCard">السابق</string>
|
||||
<string name="nextCard">التالي</string>
|
||||
<string name="failedToRetrieveImageFile">فشل في استخراج ملف الصورة</string>
|
||||
<string name="barcodeLongPressMessage">يمكن فتح صور فقط في تطبيق معرض الصور</string>
|
||||
<string name="failedToOpenUrl">ثبت متصفح ويب أولاً</string>
|
||||
<string name="welcome">مرحبًا بك في كاتيما</string>
|
||||
<string name="updateBalanceTitle">كم أنفقت أو استلمت؟</string>
|
||||
@@ -237,22 +227,21 @@
|
||||
<string name="importCards">استيراد البطاقات</string>
|
||||
<string name="newBalanceSentence">الرصيد الجديد: <xliff:g>%s</xliff:g></string>
|
||||
<string name="cameraPermissionDeniedTitle">تعذر الوصول إلى الكاميرا</string>
|
||||
<string name="noCameraPermissionDirectToSystemSetting">لمسح الباركود، ستحتاج Catima إلى الوصول إلى الكاميرا. اضغط هنا لتغيير إعدادات الأذونات.</string>
|
||||
<string name="noCameraPermissionDirectToSystemSetting">لمسح الباركود، ستحتاج كاتيما إلى الوصول إلى الكاميرا. اضغط هنا لتغيير إعدادات الأذونات.</string>
|
||||
<string name="updateBalance">تحديث الرصيد</string>
|
||||
<string name="updateBalanceHint">أدخل المبلغ</string>
|
||||
<string name="storageReadPermissionRequired">الصلاحيه للوصل للتخزين مطلوبة لهذا الاجراء…</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="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>
|
||||
@@ -268,7 +257,6 @@
|
||||
<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>
|
||||
@@ -289,7 +277,7 @@
|
||||
<string name="spend">انفق</string>
|
||||
<string name="receive">استلم</string>
|
||||
<string name="amountParsingFailed">كمية غير صحيحة</string>
|
||||
<string name="add_manually_warning_message">في بعض المتاجر قيمة الباركود تختلف عن الرقم الموجود على البطاقة. لهذا السبب إدخال الباركود يدوياً لن ينجح دائماً. من المستحسن فحص الباركود بكاميرا بدلا من ذالك. هل انت مُصِر على الاستكمال؟</string>
|
||||
<string name="add_manually_warning_message">بالنسبة لبعض البطاقات، تختلف قيمة الرمز الشريطي عن الرقم المكتوب عليها. لذا، قد لا ينجح إدخال الرمز الشريطي يدويًا دائمًا. يُنصح بمسح الرمز الشريطي باستخدام الكاميرا. هل ما زلت ترغب في المتابعة؟</string>
|
||||
<string name="addFromPdfFile">تحديد ملف PDF</string>
|
||||
<string name="errorReadingFile">لا يمكن قراءة الملف</string>
|
||||
<string name="failedLaunchingFileManager">لم يتم العثور على مدير ملفات مدعوم</string>
|
||||
@@ -300,7 +288,7 @@
|
||||
<string name="exportCancelled">تم الغاء الاستخراج</string>
|
||||
<string name="useFrontImage">استخدام صورة أمامية</string>
|
||||
<string name="useBackImage">استخدم صورة خلفية</string>
|
||||
<string name="addFromPkpass">اختر ملف الدفتر (.pkpass)</string>
|
||||
<string name="addFromPkpass">اختر ملف الدفتر الحساب [Passbook] (.pkpass. / pkpasses)</string>
|
||||
<string name="unsupportedFile">هذا الملف غير مدعوم</string>
|
||||
<string name="generic_error_please_retry">حدث خطأ ما</string>
|
||||
<string name="settings_use_volume_keys_navigation">بدّل البطاقات باستخدام أزرار الصوت</string>
|
||||
@@ -322,4 +310,21 @@
|
||||
<string name="card_list_widget_name">قائمة البطاقات</string>
|
||||
<string name="cardWithNumber">البطاقة <xliff:g>%d</xliff:g></string>
|
||||
<string name="cardWithNumberAndLocale">البطاقة <xliff:g>%d</xliff:g> (<xliff:g>%s</xliff:g>)</string>
|
||||
<string name="card_list_widget_empty">بعد أن تضيف بطاقات الولاء في كاتيما، ستظهر هنا. وإذا كنت تملك البطاقات، تأكد من عدم وجودهن جميعا في الأرشفة.</string>
|
||||
<string name="pleaseDoNotRotateTheDevice">نرجو منك عدم تدوير الجهاز، أو سيُلغى هذا الإجراء</string>
|
||||
<string name="acra_catima_has_crashed">نأسف، لقد توقف <xliff:g id="app_name">%s</xliff:g>. ساعدنا في إصلاح هذه المشكلة من خلال إرسال تقرير للأخطاء.</string>
|
||||
<string name="acra_explain_crash">بقدر الإمكان، نرجو منك أن تضيف مزيدا من المعلومات عمَّ كنت تفعل هنا:</string>
|
||||
<string name="acra_crash_email_subject">تقرير الأخطاء لـ<xliff:g id="app_name">%s</xliff:g></string>
|
||||
<string name="copy_value">قيمة النسخ</string>
|
||||
<string name="copied_to_clipboard">تم النسخ إلى الحافظة</string>
|
||||
<string name="nothing_to_copy">لم يتم العثور على قيمة</string>
|
||||
<string name="barcodeEncoding">طريقة ترميز الرمز الشريطي \"الباركود\"</string>
|
||||
<string name="back">رجوع</string>
|
||||
<string name="pref_enable_acra">إخطار بإرسال تقارير الأخطاء</string>
|
||||
<string name="pref_enable_acra_summary">عند التفعيل, سيتم سؤالك للابلاغ بالاعطال عندما تحدث. لا ولن يتم الابلاغ بالاعطال تلفائيا.</string>
|
||||
<string name="settings_disable_nfc_while_viewing_card">وقف NFC</string>
|
||||
<string name="settings_disable_nfc_while_viewing_card_summary">وقف NFC لمنع NFC من العمل أثناء عرض بطاقة</string>
|
||||
<string name="nfc_blocked_while_viewing_card">NFC موقوف</string>
|
||||
<string name="change_settings">تغير الاعدادات</string>
|
||||
<string name="nfc_block_system_error">فشل في ايقاف NFC</string>
|
||||
</resources>
|
||||
@@ -10,7 +10,7 @@
|
||||
<string name="action_add">Дадаць</string>
|
||||
<string name="app_name">Catima</string>
|
||||
<string name="action_search">Пошук</string>
|
||||
<string name="noGiftCards">Націсніце кнопку + плюс, каб дадаць картку, або імпартуйце праз меню ⋮.</string>
|
||||
<string name="noGiftCards">Націсніце кнопку + плюс, каб дадаць картку, або імпартуйце праз меню ⋮</string>
|
||||
<string name="storeName">Назва</string>
|
||||
<string name="star">Дадаць у абраныя</string>
|
||||
<string name="unstar">Выдаліць з абраных</string>
|
||||
@@ -37,7 +37,7 @@
|
||||
<string name="storageReadPermissionRequired">Для гэтага дзеяння патрабуецца дазвол на чытанне сховішча…</string>
|
||||
<string name="cameraPermissionRequired">Для гэтага дзеяння патрабуецца дазвол на доступ да камеры…</string>
|
||||
<string name="permissionReadCardsLabel">Чытаць карты Catima</string>
|
||||
<string name="exportOptionExplanation">Даныя будуць запісаны ў выбранае вамі месца.</string>
|
||||
<string name="exportOptionExplanation">Даныя будуць запісаны ў выбранае вамі месца</string>
|
||||
<string name="importOptionFilesystemExplanation">Выберыце пэўны файл з файлавай сістэмы.</string>
|
||||
<string name="importOptionFilesystemButton">З файлавай сістэмы</string>
|
||||
<string name="about">Аб праграме</string>
|
||||
@@ -65,7 +65,7 @@
|
||||
<string name="importExport">Імпарт/Экспарт</string>
|
||||
<string name="importOptionFilesystemTitle">Імпарт з файлавай сістэмы</string>
|
||||
<string name="app_copyright_fmt" tools:ignore="PluralsCandidate">Аўтарскае права © 2019–<xliff:g>%d</xliff:g> Sylvia van Os і суаўтары</string>
|
||||
<string name="importExportHelp">Рэзервовае капіяванне даных дазваляе перанесці іх на іншую прыладу.</string>
|
||||
<string name="importExportHelp">Рэзервовае капіяванне даных дазваляе перанесці іх на іншую прыладу</string>
|
||||
<string name="failedParsingImportUriError">Не атрымалася разабраць URI імпарту</string>
|
||||
<string name="exportFailedTitle">Не ўдалося экспартаваць</string>
|
||||
<string name="exportFailed">Немагчыма выканаць экспарт</string>
|
||||
@@ -158,15 +158,11 @@
|
||||
<string name="settings_oled_dark">Чысты чорны фон для цёмнай тэмы</string>
|
||||
<string name="selectColor">Выбраць колер</string>
|
||||
<string name="setIcon">Задаць мініяцюру</string>
|
||||
<string name="settings_theme_color">Колер тэмы</string>
|
||||
<string name="settings_catima_theme">Catima</string>
|
||||
<string name="settings_magenta_theme">Пурпурны</string>
|
||||
<string name="app_contributors">Стала магчымым дзякуючы: <xliff:g id="app_contributors">%s</xliff:g></string>
|
||||
<string name="sort">Сартаваць</string>
|
||||
<string name="showMoreInfo">Паказаць інфармацыю</string>
|
||||
<string name="updateBalance">Абнавіць баланс</string>
|
||||
<string name="failedToRetrieveImageFile">Не ўдалося атрымаць файл відарысу</string>
|
||||
<string name="barcodeLongPressMessage">У галерэі можна адкрываць толькі відарысы</string>
|
||||
<string name="sort_by_name">Назва</string>
|
||||
<string name="sort_by_most_recently_used">Частата выкарыстання</string>
|
||||
<string name="sort_by_valid_from">Дзейнічае ад</string>
|
||||
@@ -206,7 +202,6 @@
|
||||
<string name="validFromSentence">Дзейнічае з: <xliff:g>%s</xliff:g></string>
|
||||
<string name="height">Вышыня</string>
|
||||
<string name="switchToFrontImage">Пераключыцца на пярэдні відарыс</string>
|
||||
<string name="openBackImageInGalleryApp">Адкрыць задні відарыс у галерэі</string>
|
||||
<string name="setBarcodeHeight">Задаць вышыню штрыхкода</string>
|
||||
<string name="donate">Ахвяраваць</string>
|
||||
<string name="icon_header_click_text">Доўгі націск для рэдагавання мініяцюры</string>
|
||||
@@ -275,8 +270,6 @@
|
||||
<string name="switchToBackImage">Пераключыцца на задні відарыс</string>
|
||||
<string name="importFidmeMessage">Каб імпартаваць, выберыце файл <i>fidme-export-request-xxxxxx.zip</i> з FidMe, а потым уручную выберыце тыпы штрыхкодаў. \nСтварыце яго з вашага профілю FidMe, выбраўшы \"Абарону даных\", а затым націснуўшы \"Выняць мае даныя\".</string>
|
||||
<string name="importCatimaMessage">Каб імпартаваць, выберыце файл <i>catima.zip</i> з Catima. \nСтварыце яго з меню \"Імпарт/Экспарт\" іншай праграмы Catima, спачатку націснуўшы там \"Экспарт\".</string>
|
||||
<string name="settings_sky_blue_theme">Нябесна-блакітны</string>
|
||||
<string name="settings_brown_theme">Карычневы</string>
|
||||
<string name="switchToBarcode">Пераключыцца на штрыхкод</string>
|
||||
<string name="settings_locale">Мова</string>
|
||||
<plurals name="groupCardCountWithArchived">
|
||||
@@ -288,16 +281,11 @@
|
||||
<string name="unarchived">Карта разархівавана</string>
|
||||
<string name="updateBarcodeQuestionText">Вы змянілі ID. Вы хочаце таксама абнавіць штрыхкод, каб выкарыстоўваць тое ж значэнне?</string>
|
||||
<string name="no">Не</string>
|
||||
<string name="settings_pink_theme">Ружовы</string>
|
||||
<string name="settings_system_locale">Сістэма</string>
|
||||
<string name="settings_violet_theme">Фіялетавы</string>
|
||||
<string name="multipleBarcodesFoundPleaseChooseOne">Які са знойдзеных штрыхкодаў вы хочаце выкарыстоўваць?</string>
|
||||
<string name="settings_blue_theme">Сіні</string>
|
||||
<string name="settings_green_theme">Зялёны</string>
|
||||
<string name="report_error">Паведаміць пра памылку</string>
|
||||
<string name="failedLaunchingPhotoPicker">Не атрымалася знайсці праграму для галерэі, якая падтрымліваецца</string>
|
||||
<string name="unsupportedFile">Гэты файл не падтрымліваецца</string>
|
||||
<string name="openFrontImageInGalleryApp">Адкрыць пярэдні відарыс у галерэі</string>
|
||||
<string name="field_must_not_be_empty">Поле не павінна быць пустым</string>
|
||||
<string name="add_manually_warning_title">Рэкамендуецца сканаванне</string>
|
||||
<string name="errorReadingFile">Не атрымалася прачытаць файл</string>
|
||||
@@ -306,4 +294,19 @@
|
||||
<string name="generic_error_please_retry">На жаль, нешта пайшло не так, паспрабуйце яшчэ раз...</string>
|
||||
<string name="setBarcodeWidth">Задаць шырыню штрыхкода</string>
|
||||
<string name="app_license">Свабоднае копілефт праграмнае забеспячэнне, ліцэнзаванае паводле GPLv3+</string>
|
||||
<string name="cardWithNumber">Карта <xliff:g>%d</xliff:g></string>
|
||||
<string name="cardWithNumberAndLocale">Карта <xliff:g>%d</xliff:g> (<xliff:g>%s</xliff:g>)</string>
|
||||
<string name="pleaseDoNotRotateTheDevice">Калі ласка, не паварочвайце прыладу, бо гэта адменіць дзеянне</string>
|
||||
<string name="acra_explain_crash">Калі магчыма, дадайце больш падрабязную інфармацыю пра тое, што вы тут рабілі:</string>
|
||||
<string name="acra_crash_email_subject">Справаздача аб збоі <xliff:g id="app_name">%s</xliff:g></string>
|
||||
<string name="pref_enable_acra">Запытваць дазвол на адпраўку справаздач аб збоях</string>
|
||||
<string name="pref_enable_acra_summary">Калі гэта ўключана, вам будзе прапанавана паведаміць пра збой, калі ён адбудзецца. Справаздачы аб збоях ніколі не адпраўляюцца аўтаматычна.</string>
|
||||
<string name="card_list_widget_name">Спіс карт</string>
|
||||
<string name="card_list_widget_empty">Пасля таго, як вы дадасце некалькі картак лаяльнасці ў Catima, яны з\'явяцца тут. Калі ў вас ёсць карты, пераканайцеся, што яны не ўсе заархіваваны.</string>
|
||||
<string name="acra_catima_has_crashed">Прабачце, але ў праграме <xliff:g id="app_name">%s</xliff:g> адбыўся збой. Калі ласка, дапамажыце нам выправіць гэту праблему, даслаўшы нам справаздачу аб памылцы.</string>
|
||||
<string name="back">Назад</string>
|
||||
<string name="copy_value">Скапіяваць значэнне</string>
|
||||
<string name="copied_to_clipboard">Скапіявана ў буфер абмену</string>
|
||||
<string name="nothing_to_copy">Значэнне не знойдзенае</string>
|
||||
<string name="barcodeEncoding">Кадыяванне штрых-кодаў</string>
|
||||
</resources>
|
||||
@@ -143,15 +143,6 @@
|
||||
<item quantity="other">Желаете ли тези <xliff:g>%d</xliff:g> карти да бъдат премахнати\?</item>
|
||||
</plurals>
|
||||
<string name="app_contributors">Осъществено от: <xliff:g id="app_contributors">%s</xliff:g></string>
|
||||
<string name="settings_brown_theme">Кафяво</string>
|
||||
<string name="settings_green_theme">Зелено</string>
|
||||
<string name="settings_sky_blue_theme">Небесносиньо</string>
|
||||
<string name="settings_blue_theme">Синьо</string>
|
||||
<string name="settings_violet_theme">Виолетово</string>
|
||||
<string name="settings_magenta_theme">Цикламено</string>
|
||||
<string name="settings_pink_theme">Розово</string>
|
||||
<string name="settings_catima_theme">Catima</string>
|
||||
<string name="settings_theme_color">Цвят на темата</string>
|
||||
<string name="settings_system_locale">Система</string>
|
||||
<string name="settings_locale">Език</string>
|
||||
<string name="noGroupCards">Групата е празна</string>
|
||||
@@ -207,7 +198,6 @@
|
||||
<string name="nextCard">Следваща</string>
|
||||
<string name="failedToOpenUrl">Първо инсталирайте уеб браузър</string>
|
||||
<string name="welcome">Добре дошли при Катима</string>
|
||||
<string name="barcodeLongPressMessage">В приложението галерия могат да бъдат отваряни само изображения</string>
|
||||
<string name="failedToRetrieveImageFile">Не е възможно извличане на изображение</string>
|
||||
<string name="noCameraPermissionDirectToSystemSetting">За да сканирате щрихкодове с Catima е необходим достъп до камерата. За да промените разрешението докоснете тук.</string>
|
||||
<string name="updateBalanceTitle">Колко е похарчено или получено?</string>
|
||||
@@ -227,8 +217,6 @@
|
||||
<string name="switchToFrontImage">Показване на предната страна</string>
|
||||
<string name="switchToBackImage">Показване на задната страна</string>
|
||||
<string name="switchToBarcode">Показване на щрихкода</string>
|
||||
<string name="openFrontImageInGalleryApp">Отваряне на изображението на предната страна в приложение за преглед за изображения</string>
|
||||
<string name="openBackImageInGalleryApp">Отваряне на изображението на задната страна в приложение за преглед за изображения</string>
|
||||
<string name="setBarcodeHeight">Задаване на височина на щрихкода</string>
|
||||
<string name="donate">Даряване</string>
|
||||
<string name="icon_header_click_text">Задръжте, за да промените миниатюрата</string>
|
||||
@@ -299,4 +287,20 @@
|
||||
<string name="card_list_widget_empty">Когато добавите карти в Catima те ще се покажат тук. Ако имате карти уверете се, че са извън архива.</string>
|
||||
<string name="cardWithNumber">Карта <xliff:g>%d</xliff:g></string>
|
||||
<string name="cardWithNumberAndLocale">Карта <xliff:g>%d</xliff:g> (<xliff:g>%s</xliff:g>)</string>
|
||||
<string name="pleaseDoNotRotateTheDevice">Не завъртайте устройството, защото това ще прекъсне действието</string>
|
||||
<string name="acra_catima_has_crashed">За съжаление <xliff:g id="app_name">%s</xliff:g> се срина. Помогнете ни да оправим проблема като ни изпратите доклад за грешката.</string>
|
||||
<string name="acra_crash_email_subject">Доклад за срив на <xliff:g id="app_name">%s</xliff:g></string>
|
||||
<string name="pref_enable_acra">Питане преди изпращане на доклад за срив</string>
|
||||
<string name="pref_enable_acra_summary">Когато е отметнато, при срив ще ви бъде предложено да докладвате за него. Докладите никога не се изпращат автоматично.</string>
|
||||
<string name="acra_explain_crash">Ако е възможно добавете подробности за вашите действия:</string>
|
||||
<string name="copy_value">Копиране на стойността</string>
|
||||
<string name="copied_to_clipboard">Копирано</string>
|
||||
<string name="nothing_to_copy">Няма стойност</string>
|
||||
<string name="back">Назад</string>
|
||||
<string name="barcodeEncoding">Кодиране на щрихкода</string>
|
||||
<string name="settings_disable_nfc_while_viewing_card">Пауза на NFC</string>
|
||||
<string name="settings_disable_nfc_while_viewing_card_summary">Спира NFC, за да бъдат предотвратени безконтактни плащания при преглед на карта</string>
|
||||
<string name="nfc_blocked_while_viewing_card">NFC е на пауза</string>
|
||||
<string name="change_settings">Настройки</string>
|
||||
<string name="nfc_block_system_error">Грешка при спиране на NFC</string>
|
||||
</resources>
|
||||
@@ -50,15 +50,6 @@
|
||||
<string name="turn_flashlight_off">টর্চলাইট বন্ধ করুন</string>
|
||||
<string name="settings_locale">লোকেল</string>
|
||||
<string name="settings_system_locale">সিস্টেম লোকেল</string>
|
||||
<string name="settings_theme_color">থিম রঙ</string>
|
||||
<string name="settings_catima_theme">কটিমা থিম</string>
|
||||
<string name="settings_pink_theme">গোলাপী থিম</string>
|
||||
<string name="settings_magenta_theme">ম্যাজেন্টা থিম</string>
|
||||
<string name="settings_violet_theme">ভায়োলেট থিম</string>
|
||||
<string name="settings_blue_theme">নীল থিম</string>
|
||||
<string name="settings_sky_blue_theme">আকাশী নীল থিম</string>
|
||||
<string name="settings_green_theme">সবুজ থিম</string>
|
||||
<string name="settings_brown_theme">বাদামী থিম</string>
|
||||
<string name="sort">সাজান</string>
|
||||
<string name="sort_by_name">নামের দ্বারা সাজান</string>
|
||||
<string name="sort_by_most_recently_used">সর্বাধিক সম্প্রতি ব্যবহৃত দ্বারা সাজান</string>
|
||||
@@ -202,7 +193,6 @@
|
||||
<string name="settings_oled_dark">অন্ধকার থিমের জন্য খাঁটি কালো পটভূমি</string>
|
||||
<string name="setIcon">আইকন সেট করুন</string>
|
||||
<string name="updateBalance">ব্যালেন্স আপডেট করুন</string>
|
||||
<string name="barcodeLongPressMessage">গ্যালারি অ্যাপে শুধুমাত্র ছবি খোলা যাবে</string>
|
||||
<string name="translate_platform">Weblate-এ</string>
|
||||
<string name="on_google_play">Google Play-তে</string>
|
||||
<string name="shortcutSelectCard">একটি কার্ড নির্বাচন করুন</string>
|
||||
@@ -234,8 +224,6 @@
|
||||
<string name="switchToBackImage">পিছনের ছবিটিতে সুইচ করুন</string>
|
||||
<string name="switchToFrontImage">সামনের ছবিটিতে সুইচ করুন</string>
|
||||
<string name="switchToBarcode">বারকোডে সুইচ করুন</string>
|
||||
<string name="openFrontImageInGalleryApp">সামনের ছবিটি গ্যালারি অ্যাপে খুলুন</string>
|
||||
<string name="openBackImageInGalleryApp">পিছনের ছবিটি গ্যালারি অ্যাপে খুলুন</string>
|
||||
<string name="setBarcodeHeight">বারকোডের উচ্চতা সেট করুন</string>
|
||||
<string name="icon_header_click_text">দীর্ঘক্ষন টাচ করে থাম্বনেইল এডিট করবেন</string>
|
||||
<string name="show_balance">ব্যালান্স দেখান</string>
|
||||
|
||||
@@ -2,10 +2,6 @@
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="settings_locale">ভাষা</string>
|
||||
<string name="action_search">খুঁজুন</string>
|
||||
<string name="settings_pink_theme">গুলাপি</string>
|
||||
<string name="settings_blue_theme">নীল</string>
|
||||
<string name="settings_green_theme">সবুজ</string>
|
||||
<string name="settings_brown_theme">বাদামি</string>
|
||||
<string name="save">সংরক্ষণ</string>
|
||||
<string name="cardId">কার্ড আইডি</string>
|
||||
<string name="barcodeType">বারকোডের ধরন</string>
|
||||
@@ -19,7 +15,6 @@
|
||||
<string name="all">সকল</string>
|
||||
<string name="never">কখনো না</string>
|
||||
<string name="currency">মুদ্রা</string>
|
||||
<string name="settings_violet_theme">বেগুনি</string>
|
||||
<string name="no">না</string>
|
||||
<string name="nextCard">পরবর্তী</string>
|
||||
<string name="action_add">যুক্ত করুন</string>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2" xmlns:tools="http://schemas.android.com/tools">
|
||||
<string name="save">Snimi</string>
|
||||
<string name="cancel">Odustani</string>
|
||||
<string name="unstar">Ukloni iz omiljenih</string>
|
||||
@@ -8,15 +8,15 @@
|
||||
<string name="note">Zabilješka</string>
|
||||
<string name="storeName">Ime</string>
|
||||
<string name="noMatchingGiftCards">Nema rezultata. Pokušaj promijeniti pretragu.</string>
|
||||
<string name="noGiftCards">Kliknite + Plus dugme da dodate kartu ili uvezete nešto iz menija prvo.</string>
|
||||
<string name="noGiftCards">Kliknite na dugme + plus da biste dodali karticu ili je uvezite iz ⋮ menija</string>
|
||||
<string name="action_add">Dodaj</string>
|
||||
<string name="all">Sve</string>
|
||||
<string name="noGroupCards">Ova grupa ne sadrži nikakve karte</string>
|
||||
<string name="noGroups">Kliknite + Plus dugme da dodate grupe za kategorizaciju prvo.</string>
|
||||
<string name="noGroupCards">Ova grupa je prazna</string>
|
||||
<string name="noGroups">Kliknite na dugme + plus da biste dodali grupe za kategorizaciju</string>
|
||||
<string name="groups">Grupe</string>
|
||||
<string name="enter_group_name">Unesite ime grupe</string>
|
||||
<string name="exportSuccessful">Podaci o kartama izvožen</string>
|
||||
<string name="importSuccessful">Uvezeni podaci o kartama</string>
|
||||
<string name="exportSuccessful">Izvezeni podaci</string>
|
||||
<string name="importSuccessful">Uvezeni podaci</string>
|
||||
<string name="intent_import_card_from_url_share_text">Želim podijeliti čestitku s tobom</string>
|
||||
<string name="settings_disable_lockscreen_while_viewing_card">Spriječi zaključavanje ekrana</string>
|
||||
<string name="settings_keep_screen_on">Zadrži ekran</string>
|
||||
@@ -28,47 +28,38 @@
|
||||
<string name="importLoyaltyCardKeychain">Uvezi iz Loyalty Card Keychain</string>
|
||||
<string name="importVoucherVault">Uvezi iz trezora vaučer</string>
|
||||
<string name="barcodeId">Barcode vrijednost</string>
|
||||
<string name="sameAsCardId">Isto kao i kartica</string>
|
||||
<string name="sameAsCardId">Isto kao i ID</string>
|
||||
<string name="setBarcodeId">Postavi vrijednost za bar kod</string>
|
||||
<string name="unsupportedBarcodeType">Ovaj bar kod još nije prikazan. Ona može biti podržana u kasnijoj verziji app.</string>
|
||||
<string name="wrongValueForBarcodeType">Izabrana vrijednost nije izvršna</string>
|
||||
<string name="wrongValueForBarcodeType">Vrijednost nije važeća za odabrani tip barkoda</string>
|
||||
<string name="intent_import_card_from_url_share_multiple_text">Želim podijeliti karte s tobom</string>
|
||||
<string name="frontImageDescription">Slika kartice</string>
|
||||
<string name="backImageDescription">Slika pozadine kartice</string>
|
||||
<string name="frontImageDescription">Prednja slika</string>
|
||||
<string name="backImageDescription">Stražnja slika</string>
|
||||
<string name="photos">Slike</string>
|
||||
<string name="setFrontImage">Postavi naslovnu sliku</string>
|
||||
<string name="setBackImage">Vrati sliku</string>
|
||||
<string name="removeImage">Ukloni sliku</string>
|
||||
<string name="takePhoto">Slikaj</string>
|
||||
<string name="updateBarcodeQuestionTitle">Ažurirati bar kod vrijednost\?</string>
|
||||
<string name="updateBarcodeQuestionText">Promijenio si auto. Da li želite ažurirati bar kod koristiti istu vrijednost\?</string>
|
||||
<string name="updateBarcodeQuestionText">Promijenili ste ID. Želite li ažurirati i barkod da koristi istu vrijednost?</string>
|
||||
<string name="yes">Da</string>
|
||||
<string name="no">Ne.</string>
|
||||
<string name="passwordRequired">Upišite šifru</string>
|
||||
<string name="failedGeneratingShareURL">Ne mogu generisati oštriji URL. Molim vas, prijavite ovo.</string>
|
||||
<string name="passwordRequired">Unesite lozinku</string>
|
||||
<string name="failedGeneratingShareURL">Nije moguće generirati URL za dijeljenje</string>
|
||||
<string name="turn_flashlight_on">Upali lampu</string>
|
||||
<string name="turn_flashlight_off">Ugasi lampu</string>
|
||||
<string name="settings_locale">Jezik</string>
|
||||
<string name="settings_system_locale">Sistem</string>
|
||||
<string name="settings_theme_color">Boja teme</string>
|
||||
<string name="settings_catima_theme">Catima</string>
|
||||
<string name="settings_pink_theme">Ružičasto</string>
|
||||
<string name="settings_magenta_theme">Ljubičasto</string>
|
||||
<string name="settings_violet_theme">Ljubičasto</string>
|
||||
<string name="settings_blue_theme">Plavo</string>
|
||||
<string name="settings_sky_blue_theme">Nebo plavo</string>
|
||||
<string name="settings_green_theme">Zeleno</string>
|
||||
<string name="settings_brown_theme">Braun</string>
|
||||
<string name="sort">Poništi sortiranje</string>
|
||||
<string name="sort_by_name">Ime</string>
|
||||
<string name="sort_by_most_recently_used">Nedavno Korišten</string>
|
||||
<string name="sort_by_most_recently_used">Najnovije korišteno</string>
|
||||
<string name="sort_by_expiry">Sajam</string>
|
||||
<string name="reverse">Rikverc</string>
|
||||
<string name="reverse">...obrnutim redoslijedom</string>
|
||||
<string name="sort_by">Sortiraj</string>
|
||||
<string name="noCardExistsError">Nisam mogao pronaći tu karticu</string>
|
||||
<string name="noCardExistsError">Nisam mogao/la pronaći tu karticu</string>
|
||||
<string name="noCardsMessage">Dodaj prvo kartu</string>
|
||||
<string name="addCardTitle">Dodaj Kartu</string>
|
||||
<string name="editCardTitle">Izmijeni Karticu</string>
|
||||
<string name="addCardTitle">Dodaj karticu</string>
|
||||
<string name="editCardTitle">Uredi karticu</string>
|
||||
<string name="sendLabel">Pošalji…</string>
|
||||
<string name="share">Podijeli</string>
|
||||
<string name="deleteConfirmation">Izbriši trajno ovu karticu\?</string>
|
||||
@@ -81,25 +72,236 @@
|
||||
<string name="exportFailed">Nisam uspio izvršiti izvoz</string>
|
||||
<string name="app_copyright_old">Bazirano na Loyalty Card privjesku
|
||||
\ncopyright © 2016-2020 Branden Archer</string>
|
||||
<string name="noGiftCardsGroup">Kreirajte kartice, i dodajte ih ovdje u grupu.</string>
|
||||
<string name="importExportHelp">Backupovanje vaših podataka omogućava njihov prenos na drugi uređaj.</string>
|
||||
<string name="noGiftCardsGroup">Napravite nekoliko kartica, a zatim ih dodijelite grupi ovdje</string>
|
||||
<string name="importExportHelp">Pravljenje sigurnosne kopije podataka omogućava njihovo premještanje na drugi uređaj</string>
|
||||
<string name="importSuccessfulTitle">Uvezeno</string>
|
||||
<string name="exportFailedTitle">Izvoz neuspješan</string>
|
||||
<string name="scanCardBarcode">Skeniraj barkod</string>
|
||||
<string name="cardShortcut">Prečica kartice</string>
|
||||
<string name="failedParsingImportUriError">Nisam uspio parsirati URI za uvoz</string>
|
||||
<string name="importExport">Uvoz/Izvoz</string>
|
||||
<string name="failedParsingImportUriError">Nemogu analizirati URI za uvoz</string>
|
||||
<string name="importExport">Uvoz/izvoz</string>
|
||||
<string name="exportName">Izvoz</string>
|
||||
<string name="importFailedTitle">Uvoz neuspješan</string>
|
||||
<string name="importFailed">Nisam uspio odraditi uvoz</string>
|
||||
<string name="exportSuccessfulTitle">Izvezeno</string>
|
||||
<string name="importing">Uvozim…</string>
|
||||
<string name="exporting">Izvozim…</string>
|
||||
<string name="exportOptionExplanation">Ovi podaci će biti zapisani na lokaciju po Vašoj želji.</string>
|
||||
<string name="exportOptionExplanation">Podaci će biti zapisani na lokaciju po vašem izboru</string>
|
||||
<string name="importOptionFilesystemTitle">Uvoz iz file sistema</string>
|
||||
<string name="importOptionFilesystemExplanation">Izaberite specifični file iz file sistema.</string>
|
||||
<string name="importOptionFilesystemExplanation">Odaberite određenu datoteku iz datotečnog sistema</string>
|
||||
<string name="importOptionFilesystemButton">Iz file sistema</string>
|
||||
<string name="about">O</string>
|
||||
<string name="noBarcode">Nema barkoda</string>
|
||||
<string name="deleteTitle">Obriši karticu</string>
|
||||
</resources>
|
||||
<string name="app_name">Catima</string>
|
||||
<plurals name="selectedCardCount">
|
||||
<item quantity="one"><xliff:g>%d</xliff:g> odabran</item>
|
||||
<item quantity="few"><xliff:g>%d</xliff:g> odabranih</item>
|
||||
<item quantity="other"><xliff:g>%d</xliff:g> odabrano</item>
|
||||
</plurals>
|
||||
<string name="cameraPermissionRequired">Za ovu radnju potrebna je dozvola za pristup kameri…</string>
|
||||
<plurals name="deleteCardsTitle">
|
||||
<item quantity="one">Obriši <xliff:g>%d</xliff:g> karticu</item>
|
||||
<item quantity="few">Obriši <xliff:g>%d</xliff:g> kartice</item>
|
||||
<item quantity="other">Obriši <xliff:g>%d</xliff:g> kartica</item>
|
||||
</plurals>
|
||||
<plurals name="deleteCardsConfirmation">
|
||||
<item quantity="one">Trajno izbrisati ovu <xliff:g>%d</xliff:g> karticu?</item>
|
||||
<item quantity="few">Trajno izbrisati ove <xliff:g>%d</xliff:g> kartice?</item>
|
||||
<item quantity="other">Trajno izbrisati ovih <xliff:g>%d</xliff:g> kartica?</item>
|
||||
</plurals>
|
||||
<plurals name="groupCardCount">
|
||||
<item quantity="one"><xliff:g>%d</xliff:g> kartica</item>
|
||||
<item quantity="few"><xliff:g>%d</xliff:g> kartice</item>
|
||||
<item quantity="other"><xliff:g>%d</xliff:g> kartica</item>
|
||||
</plurals>
|
||||
<plurals name="balancePoints">
|
||||
<item quantity="one"><xliff:g>%s</xliff:g> poen</item>
|
||||
<item quantity="few"><xliff:g>%s</xliff:g> poena</item>
|
||||
<item quantity="other"><xliff:g>%s</xliff:g> poena</item>
|
||||
</plurals>
|
||||
<plurals name="groupCardCountWithArchived">
|
||||
<item quantity="one"><xliff:g>%1$d</xliff:g> kartica (<xliff:g id="archivedCount">%2$d</xliff:g> archived)</item>
|
||||
<item quantity="few"><xliff:g>%1$d</xliff:g> kartice (<xliff:g id="archivedCount">%2$d</xliff:g> archived)</item>
|
||||
<item quantity="other"><xliff:g>%1$d</xliff:g> kartica (<xliff:g id="archivedCount">%2$d</xliff:g> archived)</item>
|
||||
</plurals>
|
||||
<string name="barcodeImageDescriptionWithType">Slika <xliff:g>%s</xliff:g> barkoda</string>
|
||||
<string name="storageReadPermissionRequired">Za ovu radnju potrebna je dozvola za čitanje pohrane…</string>
|
||||
<string name="permissionReadCardsLabel">Čitajte Catima kartice</string>
|
||||
<string name="permissionReadCardsDescription">da pročita vaše Catima kartice i sve njihove detalje, uključujući bilješke i slike</string>
|
||||
<string name="cameraPermissionDeniedTitle">Nije bilo moguće pristupiti kameri</string>
|
||||
<string name="noCameraPermissionDirectToSystemSetting">Za skeniranje barkodova, Catima će trebati pristup vašoj kameri. Dodirnite ovdje da promijenite postavke dozvola.</string>
|
||||
<string name="app_copyright_fmt" tools:ignore="PluralsCandidate">Autorska prava © 2019–<xliff:g>%d</xliff:g> Sylvia van Os i saradnici</string>
|
||||
<string name="app_copyright_short">Autorska prava © Sylvia van Os i saradnici</string>
|
||||
<string name="app_license">Slobodni softver zaštićen autorskim pravom, licenciran pod GPLv3+</string>
|
||||
<string name="about_title_fmt">O aplikaciji <xliff:g id="app_name">%s</xliff:g></string>
|
||||
<string name="debug_version_fmt">Verzija: <xliff:g id="version">%s</xliff:g></string>
|
||||
<string name="app_libraries">Biblioteke trećih strana: <xliff:g id="app_libraries_list">%s</xliff:g></string>
|
||||
<string name="app_resources">Resursi trećih strana: <xliff:g id="app_resources_list">%s</xliff:g></string>
|
||||
<string name="selectBarcodeTitle">Odaberite barkod</string>
|
||||
<string name="thumbnailDescription">Sličica</string>
|
||||
<string name="settings">Postavke</string>
|
||||
<string name="failedOpeningFileManager">Nije moguće otvoriti upravitelj datoteka</string>
|
||||
<string name="moveUp">Pomakni se prema gore</string>
|
||||
<string name="moveDown">Pomakni se prema dolje</string>
|
||||
<string name="leaveWithoutSaveTitle">Izlaz</string>
|
||||
<string name="leaveWithoutSaveConfirmation">Napustiti bez spremanja?</string>
|
||||
<string name="addManually">Ručno unesite barkod</string>
|
||||
<string name="addFromImage">Odaberite sliku iz galerije</string>
|
||||
<string name="groupsList">Grupe: <xliff:g>%s</xliff:g></string>
|
||||
<string name="editGroup">Grupa za uređivanje: <xliff:g>%s</xliff:g></string>
|
||||
<string name="expiryStateSentence">Ističe: <xliff:g>%s</xliff:g></string>
|
||||
<string name="expiryStateSentenceExpired">Isteklo: <xliff:g>%s</xliff:g></string>
|
||||
<string name="balanceSentence">Stanje: <xliff:g>%s</xliff:g></string>
|
||||
<string name="card">Kartica</string>
|
||||
<string name="editBarcode">Uredi barkod</string>
|
||||
<string name="expiryDate">Datum isteka</string>
|
||||
<string name="never">Nikad</string>
|
||||
<string name="chooseExpiryDate">Odaberite datum isteka</string>
|
||||
<string name="moveBarcodeToTopOfScreen">Pomaknite barkod na vrh ekrana</string>
|
||||
<string name="noBarcodeFound">Nije pronađen barkod</string>
|
||||
<string name="errorReadingImage">Nisam mogao/la pročitati sliku</string>
|
||||
<string name="balance">Stanje</string>
|
||||
<string name="currency">Valuta</string>
|
||||
<string name="points">Bodovi</string>
|
||||
<string name="balanceParsingFailed">Nevažeći saldo</string>
|
||||
<string name="chooseImportType">Uvoz podataka iz</string>
|
||||
<string name="app_loyalty_card_keychain">Privjesak za ključeve s karticom vjernosti</string>
|
||||
<string name="privacy_policy">Politika privatnosti</string>
|
||||
<string name="accept">Prihvati</string>
|
||||
<string name="importFidme">Uvoz iz FidMe-a</string>
|
||||
<string name="importFidmeMessage">Odaberite izvoz iz FidMe-a za uvoz i nakon toga ručno odaberite vrste barkodova. \nKreirajte ga iz svog FidMe profila odabirom Zaštita podataka, a zatim pritiskom na Izdvoji moje podatke.</string>
|
||||
<string name="importLoyaltyCardKeychainMessage">Odaberite svoj izvoz iz Privjeska za ključeve s karticom vjernosti za uvoz.\nKreirajte ga iz menija Uvoz/Izvoz u Privjesku za ključeve s karticom vjernosti pritiskom na Izvoz.</string>
|
||||
<string name="importVoucherVaultMessage">Odaberite izvoz iz Voucher Vaulta za uvoz. \nKreirajte ga pritiskom na Izvoz u Voucher Vaultu.</string>
|
||||
<string name="switchToBarcode">Prebaci na barkod</string>
|
||||
<string name="switchToBackImage">Prebaci na sliku iz pozadine</string>
|
||||
<string name="switchToFrontImage">Prebaci na prednju sliku</string>
|
||||
<string name="height">Visina</string>
|
||||
<string name="validFromSentence">Važi od: <xliff:g>%s</xliff:g></string>
|
||||
<string name="chooseValidFromDate">Odaberite datum važenja od</string>
|
||||
<string name="anyDate">Bilo koji datum</string>
|
||||
<string name="validFromDate">Vrijedi od</string>
|
||||
<string name="newBalanceSentence">Novo stanje: <xliff:g>%s</xliff:g></string>
|
||||
<string name="currentBalanceSentence">Trenutno stanje: <xliff:g>%s</xliff:g></string>
|
||||
<string name="updateBalanceHint">Unesite iznos</string>
|
||||
<string name="updateBalanceTitle">Koliko ste potrošili ili primili?</string>
|
||||
<string name="importCards">Uvezi kartice</string>
|
||||
<string name="welcome">Dobrodošli u Catima</string>
|
||||
<string name="failedToOpenUrl">Prvo instalirajte web preglednik</string>
|
||||
<string name="nextCard">Sljedeće</string>
|
||||
<string name="previousCard">Prethodno</string>
|
||||
<string name="failedLaunchingPhotoPicker">Nemogu pronaći podržani alat za odabir slika</string>
|
||||
<string name="unarchived">Kartica je vraćena iz arhive</string>
|
||||
<string name="archived">Kartica je arhivirana</string>
|
||||
<string name="unarchive">Vrati iz arhive</string>
|
||||
<string name="archive">Arhiva</string>
|
||||
<string name="duplicateCard">Duplikat</string>
|
||||
<string name="include_if_asking_support">Ako želite zatražiti podršku, uključite sljedeće informacije:</string>
|
||||
<string name="starred">Označeno zvjezdicom</string>
|
||||
<string name="options">Opcije</string>
|
||||
<string name="shortcutSelectCard">Odaberite karticu</string>
|
||||
<string name="translate_platform">na Weblateu</string>
|
||||
<string name="report_error">Prijavi grešku</string>
|
||||
<string name="on_google_play">na Google Play</string>
|
||||
<string name="rate_this_app">Ocijenite ovu aplikaciju</string>
|
||||
<string name="and_data_usage">i korištenje podataka</string>
|
||||
<string name="on_github">na GitHubu</string>
|
||||
<string name="source_repository">Izvorni repozitorij</string>
|
||||
<string name="license">Licenca</string>
|
||||
<string name="help_translate_this_app">Pomozite u prevođenju ove aplikacije</string>
|
||||
<string name="credits">Zasluge</string>
|
||||
<string name="version_history">Historija verzija</string>
|
||||
<string name="sort_by_valid_from">Vrijedi od</string>
|
||||
<string name="failedToRetrieveImageFile">Neuspjevam preuzimanje slikovne datoteke</string>
|
||||
<string name="updateBalance">Ažuriraj stanje</string>
|
||||
<string name="showMoreInfo">Prikaži informacije</string>
|
||||
<string name="app_contributors">Omogućeno zahvaljujući: <xliff:g id="app_contributors">%s</xliff:g></string>
|
||||
<string name="setIcon">Postavi sličicu</string>
|
||||
<string name="selectColor">Odaberite boju</string>
|
||||
<string name="settings_oled_dark_summary">Smanjuje potrošnju baterije na OLED ekranima</string>
|
||||
<string name="settings_oled_dark">Čisto crna pozadina za tamnu temu</string>
|
||||
<string name="exportPasswordHint">Unesite lozinku</string>
|
||||
<string name="exportPassword">Postavite lozinku za zaštitu izvoza (opcionalno)</string>
|
||||
<string name="back">Nazad</string>
|
||||
<string name="barcodeEncoding">Kodiranje barkoda</string>
|
||||
<string name="nothing_to_copy">Nije pronađena vrijednost</string>
|
||||
<string name="settings_dark_theme">Tamno</string>
|
||||
<string name="settings_display_barcode_max_brightness">Posvijetli ekran</string>
|
||||
<string name="settings_display_barcode_max_brightness_summary">Neophodno za rad nekih skenera</string>
|
||||
<string name="settings_keep_screen_on_summary">Onemogućava istek vremena ekrana tokom pregleda kartice</string>
|
||||
<string name="settings_disable_lockscreen_while_viewing_card_summary">Onemogućava zaključavanje ekrana tokom pregleda kartice</string>
|
||||
<string name="settings_allow_content_provider_read_title">Dozvoli drugim aplikacijama pristup mojim podacima</string>
|
||||
<string name="settings_allow_content_provider_read_summary">Aplikacije će i dalje morati tražiti dozvolu za pristup</string>
|
||||
<string name="settings_use_volume_keys_navigation">Prebacivanje kartica pomoću tipki za jačinu zvuka</string>
|
||||
<string name="settings_use_volume_keys_navigation_summary">Koristite tipke za jačinu zvuka da promijenite koja se kartica prikazuje</string>
|
||||
<string name="group_edit">Uredi grupu</string>
|
||||
<string name="group_name_already_in_use">Naziv grupe se već koristi</string>
|
||||
<string name="group_name_is_empty">Naziv grupe ne smije biti prazan</string>
|
||||
<string name="group_updated">Grupa ažurirana</string>
|
||||
<string name="deleteConfirmationGroup">Izbrisati grupu?</string>
|
||||
<string name="setBarcodeHeight">Postavi visinu barkoda</string>
|
||||
<string name="donate">Donirajte</string>
|
||||
<string name="icon_header_click_text">Dugo pritisnite za uređivanje sličice</string>
|
||||
<string name="show_name_below_image_thumbnail">Prikaži ime ispod sličice slike</string>
|
||||
<string name="show_note">Prikaži bilješku</string>
|
||||
<string name="show_balance">Prikaži stanje</string>
|
||||
<string name="show_validity">Prikaži validnost</string>
|
||||
<string name="settings_category_title_cards">Prikaz kartice</string>
|
||||
<string name="settings_category_title_cards_overview">Pregled kartica</string>
|
||||
<string name="settings_column_count_portrait">Kolone u portretnom načinu rada</string>
|
||||
<string name="settings_column_count_landscape">Kolone u pejzažnom načinu rada</string>
|
||||
<string name="settings_automatic_column_count">Automatski</string>
|
||||
<string name="settings_column_count_1">1</string>
|
||||
<string name="settings_column_count_2">2</string>
|
||||
<string name="settings_column_count_3">3</string>
|
||||
<string name="settings_column_count_4">4</string>
|
||||
<string name="settings_column_count_5">5</string>
|
||||
<string name="settings_column_count_6">6</string>
|
||||
<string name="settings_column_count_7">7</string>
|
||||
<string name="settings_category_title_general">Općenito</string>
|
||||
<string name="settings_category_title_privacy">Privatnost</string>
|
||||
<string name="action_display_options">Opcije prikaza</string>
|
||||
<string name="show_archived_cards">Prikaži arhivirane kartice</string>
|
||||
<string name="view_online">Pogledajte online</string>
|
||||
<string name="action_more_options">Više opcija</string>
|
||||
<string name="addWithoutBarcode">Dodajte karticu bez barkoda</string>
|
||||
<string name="enter_card_id">Unesite ID broj ili tekst sa vaše kartice</string>
|
||||
<string name="card_id_must_not_be_empty">ID kartice ne smije biti prazan</string>
|
||||
<string name="add_a_card_in_a_different_way">Dodajte karticu na drugačiji način</string>
|
||||
<string name="field_must_not_be_empty">Polje ne smije biti prazno</string>
|
||||
<string name="manually_enter_barcode_instructions">Unesite ID broj ili tekst sa vaše kartice i pritisnite barkod koji izgleda kao onaj na vašoj kartici.</string>
|
||||
<string name="add_manually_warning_title">Preporučuje se skeniranje</string>
|
||||
<string name="add_manually_warning_message">Za neke kartice, vrijednost barkoda se razlikuje od broja napisanog na kartici. Zbog toga, ručni unos barkoda možda neće uvijek funkcionirati. Preporučuje se skeniranje barkoda kamerom. Želite li i dalje nastaviti?</string>
|
||||
<string name="continue_">Nastavi</string>
|
||||
<string name="spend">Potroši</string>
|
||||
<string name="receive">Primi</string>
|
||||
<string name="amountParsingFailed">Nevažeći iznos</string>
|
||||
<string name="addFromPdfFile">Odaberite PDF datoteku</string>
|
||||
<string name="errorReadingFile">Nisam mogao pročitati datoteku</string>
|
||||
<string name="failedLaunchingFileManager">Nije pronađen podržani upravitelj datoteka</string>
|
||||
<string name="multipleBarcodesFoundPleaseChooseOne">Koji od pronađenih barkodova želite koristiti?</string>
|
||||
<string name="pageWithNumber">Stranica <xliff:g>%d</xliff:g></string>
|
||||
<string name="noCameraFoundGuideText">Izgleda da vaš uređaj nema kameru. Ako je ima, pokušajte ponovo pokrenuti uređaj. U suprotnom, koristite dugme Više opcija ispod da biste dodali barkod na drugi način.</string>
|
||||
<string name="importCancelled">Uvoz otkazan</string>
|
||||
<string name="exportCancelled">Izvoz otkazan</string>
|
||||
<string name="useFrontImage">Koristi prednju sliku</string>
|
||||
<string name="useBackImage">Koristi sliku sa zadnje strane</string>
|
||||
<string name="addFromPkpass">Odaberite Passbook datoteku (.pkpass / .pkpasses)</string>
|
||||
<string name="unsupportedFile">Ova datoteka nije podržana</string>
|
||||
<string name="generic_error_please_retry">Došlo je do greške</string>
|
||||
<string name="width">Širina</string>
|
||||
<string name="card_list_widget_name">Lista kartica</string>
|
||||
<string name="setBarcodeWidth">Postavi širinu barkoda</string>
|
||||
<string name="card_list_widget_empty">Nakon što dodate neke kartice vjernosti u Catima, one će se pojaviti ovdje. Ako imate kartice, provjerite da nisu sve arhivirane.</string>
|
||||
<string name="cardWithNumber">Kartica <xliff:g>%d</xliff:g></string>
|
||||
<string name="cardWithNumberAndLocale">Kartica <xliff:g>%d</xliff:g> (<xliff:g>%s</xliff:g>)</string>
|
||||
<string name="pleaseDoNotRotateTheDevice">Molimo vas da ne rotirate uređaj, jer će to prekinuti radnju</string>
|
||||
<string name="acra_catima_has_crashed">Žao nam je, ali aplikacija <xliff:g id="app_name">%s</xliff:g> se srušila. Molimo vas da nam pomognete da riješimo ovaj problem slanjem izvještaja o grešci.</string>
|
||||
<string name="acra_explain_crash">Ako je moguće, molimo vas da dodate više detalja o tome šta ste ovdje radili:</string>
|
||||
<string name="acra_crash_email_subject">Izvještaj o padu aplikacije <xliff:g id="app_name">%s</xliff:g></string>
|
||||
<string name="pref_enable_acra">Zatraži slanje izvještaja o padu programa</string>
|
||||
<string name="pref_enable_acra_summary">Kada je omogućeno, bit ćete upitani da prijavite pad sistema kada se dogodi. Izvještaji o padu sistema se nikada ne šalju automatski.</string>
|
||||
<string name="copy_value">Kopiraj vrijednost</string>
|
||||
<string name="copied_to_clipboard">Kopirano u međuspremnik</string>
|
||||
<string name="importCatimaMessage">Odaberite izvoz iz Catima za uvoz. \nKreirajte ga iz menija Uvoz/izvoz druge Catima aplikacije pritiskom na Izvoz.</string>
|
||||
</resources>
|
||||
Loaded 100 of 1335 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user