Compare commits
451 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f4d4e3d6fb | ||
|
|
929633e4dd | ||
|
|
eec7359603 | ||
|
|
4168ec3b43 | ||
|
|
6568ebb01c | ||
|
|
99e2a75d46 | ||
|
|
43ae42c7c5 | ||
|
|
a7aa3e9e0e | ||
|
|
5a9f0a44fd | ||
|
|
4a1858e47b | ||
|
|
d8d8a59707 | ||
|
|
b027beea35 | ||
|
|
b4d0651e99 | ||
|
|
b40380dff6 | ||
|
|
b1a0a98004 | ||
|
|
044d363f47 | ||
|
|
35d659be31 | ||
|
|
8bc1e2d321 | ||
|
|
b8811ba053 | ||
|
|
cbaf172e9d | ||
|
|
78c831cb68 | ||
|
|
279e775fb6 | ||
|
|
e5b30c9528 | ||
|
|
eb9732658f | ||
|
|
5feb59612d | ||
|
|
09bd9b3882 | ||
|
|
beb619000c | ||
|
|
72425dd39e | ||
|
|
a93d240d9a | ||
|
|
d380e284b1 | ||
|
|
cbbb434aae | ||
|
|
6dc8490b5e | ||
|
|
b2c57258b3 | ||
|
|
fe0ae4049b | ||
|
|
0517a7514e | ||
|
|
39544ac853 | ||
|
|
f46e1d09ba | ||
|
|
6421f09eab | ||
|
|
45663065f9 | ||
|
|
67328724fa | ||
|
|
55373e82a5 | ||
|
|
acdb5d6fe7 | ||
|
|
d9461c476a | ||
|
|
deee5f6aa2 | ||
|
|
efd2b1ffe1 | ||
|
|
a87d8bbfe4 | ||
|
|
a1d5275063 | ||
|
|
e327306955 | ||
|
|
9469ae37e1 | ||
|
|
9e2d65b2cd | ||
|
|
d509c06815 | ||
|
|
70faa7636a | ||
|
|
4072bc7607 | ||
|
|
eced502985 | ||
|
|
47441dbb9a | ||
|
|
e7729d9763 | ||
|
|
df40b72f77 | ||
|
|
1a1c028565 | ||
|
|
5cd77c3a25 | ||
|
|
369631d00c | ||
|
|
350031624c | ||
|
|
846f4d4904 | ||
|
|
5081eb2dce | ||
|
|
e964fda54a | ||
|
|
aaa4fc1ef3 | ||
|
|
93331e1a27 | ||
|
|
3251a6266b | ||
|
|
d3f8399cbe | ||
|
|
7c805128a7 | ||
|
|
a40c4841da | ||
|
|
768ac795ff | ||
|
|
e8460d52ec | ||
|
|
03a7efb52e | ||
|
|
48b60d8b4d | ||
|
|
562b830e5a | ||
|
|
ac810a0c6f | ||
|
|
27a90615a9 | ||
|
|
f894427247 | ||
|
|
8b7df8dabe | ||
|
|
d66903f972 | ||
|
|
7834a93394 | ||
|
|
572378de85 | ||
|
|
26e0c50a13 | ||
|
|
cd638a96f3 | ||
|
|
4e043edb64 | ||
|
|
1067d09773 | ||
|
|
d347cdde3e | ||
|
|
b704a7492e | ||
|
|
4c28d5d181 | ||
|
|
47e50de063 | ||
|
|
3777abc2a3 | ||
|
|
01554381b2 | ||
|
|
09ca9c47ab | ||
|
|
6751befe5d | ||
|
|
91661f1059 | ||
|
|
afe47f1b84 | ||
|
|
9bcbfc6d81 | ||
|
|
89a40a789d | ||
|
|
e60814d6f3 | ||
|
|
ed8028a22b | ||
|
|
bb106f185e | ||
|
|
18c4dd4dc9 | ||
|
|
72672e99c2 | ||
|
|
73067d1fe8 | ||
|
|
5e6a4c8184 | ||
|
|
310228fb5e | ||
|
|
5aef382b68 | ||
|
|
9fa78a4ea8 | ||
|
|
400867b03f | ||
|
|
b06c6bc94d | ||
|
|
d6c48bdf6e | ||
|
|
4d11391f8a | ||
|
|
8b0490fdf3 | ||
|
|
6ec23d976b | ||
|
|
11ed56ee11 | ||
|
|
4ded73f78e | ||
|
|
588f8ef677 | ||
|
|
3d70095862 | ||
|
|
410309ebd2 | ||
|
|
b9e646a25d | ||
|
|
81c1ec9199 | ||
|
|
4498d08afb | ||
|
|
963789db25 | ||
|
|
902fbc505d | ||
|
|
9889678e53 | ||
|
|
ac551ed93f | ||
|
|
ec63931396 | ||
|
|
d29d6ddf4a | ||
|
|
38aac76144 | ||
|
|
30de0a8266 | ||
|
|
0016b40256 | ||
|
|
aa3588dbfe | ||
|
|
bf7ddc023d | ||
|
|
e50cf66bca | ||
|
|
85c30185e8 | ||
|
|
5bce259445 | ||
|
|
8504109399 | ||
|
|
e182857e1b | ||
|
|
981c0b9ca6 | ||
|
|
925f62b633 | ||
|
|
cceb1207ae | ||
|
|
2e633b19dc | ||
|
|
b8b1074a46 | ||
|
|
bea65793f0 | ||
|
|
252efabdc6 | ||
|
|
018efaeebb | ||
|
|
56522b42e2 | ||
|
|
122a80f29c | ||
|
|
9363a31a77 | ||
|
|
21f3c58e32 | ||
|
|
205f7bb59d | ||
|
|
1cc0f11a5d | ||
|
|
d2168f4d91 | ||
|
|
ba46e2a0b8 | ||
|
|
6fb102d9b5 | ||
|
|
83b3d5d31c | ||
|
|
c93b7527ad | ||
|
|
07bfc95bf9 | ||
|
|
d34554ac07 | ||
|
|
63754798ef | ||
|
|
9ca4b7fbe1 | ||
|
|
4d6021ad8c | ||
|
|
f76df0d252 | ||
|
|
60a172813f | ||
|
|
01025e862a | ||
|
|
7e9c7db813 | ||
|
|
d8b121f503 | ||
|
|
79a143ebaf | ||
|
|
0b78f36784 | ||
|
|
a4c24c6436 | ||
|
|
f59f9ddec8 | ||
|
|
d21f2d12c9 | ||
|
|
30971b7e85 | ||
|
|
7ab9e0f8b0 | ||
|
|
6e63543cd1 | ||
|
|
d2de5db792 | ||
|
|
a1bb6e3bed | ||
|
|
e937fc60a7 | ||
|
|
1213ee99da | ||
|
|
7dadce600b | ||
|
|
bb1eae6f79 | ||
|
|
cf871e9606 | ||
|
|
0b92a12694 | ||
|
|
7636c3648c | ||
|
|
f62352cf05 | ||
|
|
8fbbfb137e | ||
|
|
afb960257e | ||
|
|
b9e152e3c4 | ||
|
|
bd1d33867d | ||
|
|
eba1ed63a6 | ||
|
|
74fbdc7a5e | ||
|
|
ef61aaeac6 | ||
|
|
b5ee7d7a2d | ||
|
|
f51ad0295a | ||
|
|
d0d3289efa | ||
|
|
353d8a7ecd | ||
|
|
d221969b5e | ||
|
|
97553e9253 | ||
|
|
11b839dabe | ||
|
|
bc5252b2ef | ||
|
|
5a6b7944b1 | ||
|
|
a9794daca5 | ||
|
|
aee59550e4 | ||
|
|
dcc5e5921c | ||
|
|
12df426dea | ||
|
|
9b4085e955 | ||
|
|
a4f7d4e131 | ||
|
|
70b313021c | ||
|
|
d1bdab5c66 | ||
|
|
237405279f | ||
|
|
810fa5f621 | ||
|
|
64b709266b | ||
|
|
f9a66ba3a0 | ||
|
|
bfd36bae9f | ||
|
|
15116ab673 | ||
|
|
d49d019f63 | ||
|
|
ba9d1f3891 | ||
|
|
01596d5637 | ||
|
|
9bd71d5694 | ||
|
|
6d1e7ee3bb | ||
|
|
0ce71039f1 | ||
|
|
a03b89bc93 | ||
|
|
156b720102 | ||
|
|
c13b5dda18 | ||
|
|
ffa39000f7 | ||
|
|
40de4a8dc4 | ||
|
|
db22703ec0 | ||
|
|
8b59ef152a | ||
|
|
01bd91ff66 | ||
|
|
b558d2178a | ||
|
|
efc8e6ae33 | ||
|
|
aaf481a82c | ||
|
|
d03d96b194 | ||
|
|
1c92787254 | ||
|
|
bc808d3045 | ||
|
|
b265fadec3 | ||
|
|
d2f5cd05b5 | ||
|
|
2dba2c63a8 | ||
|
|
3fffcdbd67 | ||
|
|
6c53b8e9ca | ||
|
|
8ac2fe575c | ||
|
|
b4a992abf8 | ||
|
|
45c71f01c2 | ||
|
|
9f914d4943 | ||
|
|
3f25f99236 | ||
|
|
d54f574558 | ||
|
|
0af1935d20 | ||
|
|
9a37d917f7 | ||
|
|
d4f62435ae | ||
|
|
0baf1ba348 | ||
|
|
4a568d02d3 | ||
|
|
1dc30ebaad | ||
|
|
4fbedbc30c | ||
|
|
1675ebf1dc | ||
|
|
2d6dfdcfdf | ||
|
|
bacb5b97ec | ||
|
|
940be02851 | ||
|
|
34f8c830fd | ||
|
|
ee7f5c0498 | ||
|
|
9a5c3bc661 | ||
|
|
db9ec7daea | ||
|
|
16b683fe9d | ||
|
|
0e10c644d8 | ||
|
|
fd283a5e06 | ||
|
|
e406320f17 | ||
|
|
40d36ec8fb | ||
|
|
99a3849942 | ||
|
|
26a6a786dc | ||
|
|
55a5884852 | ||
|
|
c20ce1e555 | ||
|
|
a90899d41e | ||
|
|
413d479ea0 | ||
|
|
8ff452cfd2 | ||
|
|
54c279ec89 | ||
|
|
000d7722da | ||
|
|
0bb0df21e4 | ||
|
|
0dcfc11c53 | ||
|
|
58f8a6a929 | ||
|
|
6a80c633a8 | ||
|
|
031bfd94e0 | ||
|
|
45443abaf9 | ||
|
|
b81da59bcc | ||
|
|
86f3530d95 | ||
|
|
863a378bf0 | ||
|
|
a9d81b8321 | ||
|
|
46865f349b | ||
|
|
1d1109d665 | ||
|
|
c11e995684 | ||
|
|
184af4d272 | ||
|
|
d7356704ea | ||
|
|
66573fd2da | ||
|
|
36ff2333b7 | ||
|
|
9325f33da6 | ||
|
|
9e40d9b6b3 | ||
|
|
4679051df1 | ||
|
|
27cbad1b44 | ||
|
|
ba24b49009 | ||
|
|
c408beb68c | ||
|
|
5b7590e4e1 | ||
|
|
574832d09b | ||
|
|
8029be7b2b | ||
|
|
390a2e1b5a | ||
|
|
803555f94e | ||
|
|
6488dc0fe3 | ||
|
|
ddc385c39d | ||
|
|
6382c54d1a | ||
|
|
af515d307a | ||
|
|
e1edeb364e | ||
|
|
18b95e96eb | ||
|
|
5960857568 | ||
|
|
6426a99be4 | ||
|
|
1f76809300 | ||
|
|
5346726ae1 | ||
|
|
3828454a26 | ||
|
|
7995c2ba1a | ||
|
|
9ee4184122 | ||
|
|
319ba195e4 | ||
|
|
a21e9f142c | ||
|
|
22fbbd3341 | ||
|
|
191e22db65 | ||
|
|
ceba99d11f | ||
|
|
dd1840ce5c | ||
|
|
b1608d6a9f | ||
|
|
aee03170dc | ||
|
|
419e12c0eb | ||
|
|
6c2ce61ddc | ||
|
|
fc342b4197 | ||
|
|
a6536307ad | ||
|
|
8a517e8161 | ||
|
|
34f314dad8 | ||
|
|
2e49e36e0c | ||
|
|
a7afdae83c | ||
|
|
c705136362 | ||
|
|
afa76d8f54 | ||
|
|
cf20d03fb3 | ||
|
|
bb866819db | ||
|
|
c9dd994120 | ||
|
|
eabd988540 | ||
|
|
af91097559 | ||
|
|
16793d1c0b | ||
|
|
6f4673c9fb | ||
|
|
bb99321209 | ||
|
|
baa42c8c69 | ||
|
|
6c011dd9ac | ||
|
|
9bf245a392 | ||
|
|
412afcda1e | ||
|
|
c8bc0864b2 | ||
|
|
4d59e6c48d | ||
|
|
70cf42f144 | ||
|
|
b3fc66fa58 | ||
|
|
aa7315fafe | ||
|
|
5325f1955e | ||
|
|
ee30180137 | ||
|
|
6bddeb2f75 | ||
|
|
3c69990b0d | ||
|
|
43d5478ae3 | ||
|
|
30a54ae22a | ||
|
|
9ff6b87092 | ||
|
|
9763d9fb73 | ||
|
|
0b92686dad | ||
|
|
cb1c834057 | ||
|
|
98a0487d0c | ||
|
|
2df165061d | ||
|
|
fdd7ca03cf | ||
|
|
29e996a5fe | ||
|
|
3c1c8baaa0 | ||
|
|
6c186c2abb | ||
|
|
9deb0b94af | ||
|
|
97d54e87f2 | ||
|
|
0e329b3c31 | ||
|
|
bd337d6da8 | ||
|
|
dc7f199e0c | ||
|
|
b54e14def2 | ||
|
|
f724e4c164 | ||
|
|
c389a02d69 | ||
|
|
b7db58e7d0 | ||
|
|
eefd26fa06 | ||
|
|
8d4a122cb5 | ||
|
|
13ea10d0b0 | ||
|
|
4366eab380 | ||
|
|
0c61d36cb3 | ||
|
|
6bc5e77bbd | ||
|
|
d8ddee0667 | ||
|
|
5e8312e816 | ||
|
|
8e10022158 | ||
|
|
b209b670a5 | ||
|
|
ecdec0f773 | ||
|
|
5c90107a7b | ||
|
|
6a8a9878d7 | ||
|
|
3400d10cc6 | ||
|
|
05e171170b | ||
|
|
cb967a8d8d | ||
|
|
5835da2931 | ||
|
|
2e9339b8aa | ||
|
|
bbba4b2e11 | ||
|
|
d9659ecc9d | ||
|
|
c558278962 | ||
|
|
c137cb9428 | ||
|
|
1d2f54c69b | ||
|
|
d4a377fe18 | ||
|
|
9844113568 | ||
|
|
c49f26db95 | ||
|
|
9744dfe6ff | ||
|
|
55dfd1fe09 | ||
|
|
0a3c81154c | ||
|
|
70a9689bb2 | ||
|
|
54f6d8d26f | ||
|
|
c0622ddaec | ||
|
|
12578eab3e | ||
|
|
5dd57577af | ||
|
|
d883c84b4e | ||
|
|
0ebe1b387d | ||
|
|
942aff2af5 | ||
|
|
c1606fef62 | ||
|
|
cc895353a9 | ||
|
|
73bac0ae8d | ||
|
|
158e424a47 | ||
|
|
133fa13d3b | ||
|
|
44ff4d0cc6 | ||
|
|
a7b280c08b | ||
|
|
5fe0429c98 | ||
|
|
bf391022fd | ||
|
|
a431bf8c30 | ||
|
|
a242944ee6 | ||
|
|
84a7e84486 | ||
|
|
d4988f1d4c | ||
|
|
04e52f0a8a | ||
|
|
370db97af1 | ||
|
|
84d7f4fbba | ||
|
|
afcbca251c | ||
|
|
aa9df04c9c | ||
|
|
a24452a962 | ||
|
|
e74176abb0 | ||
|
|
6f4cec4cf4 | ||
|
|
febd0cf463 | ||
|
|
a5f91486e9 | ||
|
|
d8300640af | ||
|
|
afef158169 | ||
|
|
a728d1d84d | ||
|
|
9f88b6348b | ||
|
|
6929ba1c0d | ||
|
|
49b296b983 | ||
|
|
f21a75b19a | ||
|
|
8946b0a9e1 | ||
|
|
eae470f13c | ||
|
|
5ce046b3f4 | ||
|
|
a51debb2e1 | ||
|
|
7312a4bd1a | ||
|
|
3c1a54e456 | ||
|
|
6d4babf944 | ||
|
|
7ecdf52f1a |
11
.github/workflows/android.yml
vendored
@@ -13,6 +13,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: gradle/wrapper-validation-action@v1
|
||||
- name: set up JDK 1.8
|
||||
uses: actions/setup-java@v1
|
||||
with:
|
||||
@@ -23,5 +24,11 @@ jobs:
|
||||
run: ./gradlew lintRelease
|
||||
- name: Run unit tests
|
||||
run: ./gradlew testReleaseUnitTest
|
||||
- name: FindBugs
|
||||
run: ./gradlew findbugs
|
||||
- name: SpotBugs
|
||||
run: ./gradlew spotbugsRelease
|
||||
- name: Archive test results
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: test-results
|
||||
path: app/build/reports
|
||||
|
||||
11
.gitignore
vendored
@@ -1,8 +1,9 @@
|
||||
*.iml
|
||||
.gradle
|
||||
/local.properties
|
||||
/.idea/workspace.xml
|
||||
/.idea/libraries
|
||||
local.properties
|
||||
.idea/
|
||||
.DS_Store
|
||||
/build
|
||||
/captures
|
||||
build/
|
||||
captures/
|
||||
**/release
|
||||
**/debug
|
||||
|
||||
146
CHANGELOG.md
@@ -1,5 +1,151 @@
|
||||
# Changelog
|
||||
|
||||
## v1.14 (2021-06-07)
|
||||
|
||||
Changes:
|
||||
|
||||
- Support new PDF417 export from Voucher Vault
|
||||
- Support copying multiple barcodes at once
|
||||
- Support sharing multiple loyalty cards at once
|
||||
- Ask to update barcode value if card ID changes
|
||||
|
||||
## v1.13 (2021-04-10)
|
||||
|
||||
Changes:
|
||||
|
||||
- Add option to set a separate barcode value from card ID
|
||||
- Simplify font sizing configuration
|
||||
- Several small UI fixes
|
||||
- Use letter icon for shortcuts too
|
||||
- Always show all barcode types in manual entry
|
||||
- Remove privacy policy first start dialog
|
||||
|
||||
## v1.12 (2021-03-30)
|
||||
|
||||
Changes:
|
||||
|
||||
- Support importing [Fidme](https://play.google.com/store/apps/details?id=fr.snapp.fidme) exports
|
||||
- Allow importing a card from a picture stored in the user's Android gallery
|
||||
- Fix multiline note cutoff
|
||||
- Change "Thank you" text on privacy dialog to "Accept" because Huawei is overly pedantic
|
||||
|
||||
## v1.11 (2021-03-21)
|
||||
|
||||
Changes:
|
||||
|
||||
- Add privacy policy dialog on first start (required by Huawei)
|
||||
|
||||
## v1.10 (2021-03-07)
|
||||
|
||||
Changes:
|
||||
|
||||
- Support importing [Voucher Vault](https://github.com/tim-smart/vouchervault/) exports
|
||||
- Option to keep the screen on while viewing a loyalty card
|
||||
- Option to suspend the lock screen while viewing a loyalty card
|
||||
|
||||
## v1.9.2 (2021-02-24)
|
||||
|
||||
Changes:
|
||||
|
||||
- Fix parsing balance for countries using space as separator
|
||||
|
||||
## v1.9.1 (2021-02-23)
|
||||
|
||||
Changes:
|
||||
|
||||
- Improve balance parsing logic
|
||||
- Fix currency decimal display on main screen
|
||||
|
||||
## v1.9 (2021-02-22)
|
||||
|
||||
Changes:
|
||||
|
||||
- Add balance support
|
||||
- Reorganize barcode tab of edit view
|
||||
|
||||
## v1.8.1 (2021-02-12)
|
||||
|
||||
Changes:
|
||||
|
||||
- Fix Crash on versions before Android 7
|
||||
|
||||
## v1.8 (2021-01-28)
|
||||
|
||||
Changes:
|
||||
|
||||
- Add support for scaling the barcode when moving to top to fit even more small scanners
|
||||
- Fix bottom sheet jumping after switching to fullscreen
|
||||
- Make header in loyalty card view small in landscape mode
|
||||
- Fix cards not staying in group when group gets renamed
|
||||
|
||||
## v1.7.1 (2021-01-18)
|
||||
|
||||
Changes:
|
||||
|
||||
- Fix crash on switching to barcode tab in edit view if there is no barcode
|
||||
|
||||
## v1.7.0 (2021-01-18)
|
||||
|
||||
Changes:
|
||||
|
||||
- Separate edit UI in tabs to make it feel more spacious
|
||||
- Add expiry field support
|
||||
|
||||
## v1.6.2 (2021-01-04)
|
||||
|
||||
Changes:
|
||||
|
||||
- Fix edit button or more info bottom sheet drawing over barcode ID
|
||||
|
||||
## v1.6.1 (2020-12-16)
|
||||
|
||||
Changes:
|
||||
|
||||
- Fix regression causing manual barcode entry to not be saved
|
||||
|
||||
## v1.6.0 (2020-12-15)
|
||||
|
||||
Changes:
|
||||
|
||||
- Automatically focus text field when creating or editing a group
|
||||
- Fix blurry icons (use SVG everywhere)
|
||||
- Always open camera but add manual scan button to camera view
|
||||
|
||||
## v1.5.1 (2020-12-03)
|
||||
|
||||
Changes:
|
||||
|
||||
- Fix bottomsheet background being transparent
|
||||
|
||||
## v1.5.0 (2020-12-03)
|
||||
|
||||
Changes:
|
||||
|
||||
- Improve contrast by always using white text on red buttons
|
||||
- Draggable bottom sheet in loyalty card view
|
||||
|
||||
## v1.4.1 (2020-12-01)
|
||||
|
||||
Changes:
|
||||
|
||||
- Improved translations
|
||||
- Small UI fixes
|
||||
|
||||
## v1.4.0 (2020-11-28)
|
||||
|
||||
Changes:
|
||||
|
||||
- Move About screen into its own activity
|
||||
- Ask user if they want to use their camera or manually enter ID on add/edit card
|
||||
- Make group ordering manual instead of forced alphabetically
|
||||
|
||||
## v1.3.0 (2020-11-22)
|
||||
|
||||
Changes:
|
||||
|
||||
- Always show all import/export options and show a toast on actual issues (improves compat with XPrivacyLua)
|
||||
- Ask for confirmation when leaving edit view after making changes without saving
|
||||
|
||||
## v1.2.2 (2020-11-19)
|
||||
|
||||
Changes:
|
||||
|
||||
1
app/.gitignore
vendored
@@ -1 +0,0 @@
|
||||
/build
|
||||
@@ -1,77 +1,101 @@
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'findbugs'
|
||||
import com.github.spotbugs.snom.SpotBugsTask
|
||||
|
||||
findbugs {
|
||||
sourceSets = []
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'com.github.spotbugs'
|
||||
|
||||
spotbugs {
|
||||
ignoreFailures = false
|
||||
effort = 'max'
|
||||
excludeFilter = file("./config/spotbugs/exclude.xml")
|
||||
reportsDir = file("$buildDir/reports/spotbugs/")
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdkVersion 29
|
||||
buildToolsVersion "30.0.3"
|
||||
|
||||
defaultConfig {
|
||||
applicationId "me.hackerchick.catima"
|
||||
minSdkVersion 16
|
||||
minSdkVersion 19
|
||||
targetSdkVersion 29
|
||||
versionCode 46
|
||||
versionName "1.2.2"
|
||||
versionCode 68
|
||||
versionName "1.14"
|
||||
|
||||
vectorDrawables.useSupportLibrary true
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
minifyEnabled true
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||
resValue "string", "app_name", "Catima"
|
||||
}
|
||||
debug {
|
||||
applicationIdSuffix ".debug"
|
||||
resValue "string", "app_name", "Catima Debug"
|
||||
}
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
encoding "UTF-8"
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
lintOptions {
|
||||
disable "GoogleAppIndexingWarning"
|
||||
disable "ButtonStyle"
|
||||
disable "AlwaysShowAction"
|
||||
disable "MissingTranslation"
|
||||
disable "MissingPrefix"
|
||||
disable "GoogleAppIndexingWarning", "ButtonStyle", "AlwaysShowAction",
|
||||
"MissingTranslation", "MissingPrefix"
|
||||
}
|
||||
|
||||
// Starting with Android Studio 3 Robolectric is unable to find resources.
|
||||
// The following allows it to find the resources.
|
||||
testOptions {
|
||||
unitTests {
|
||||
includeAndroidResources = true
|
||||
all {
|
||||
testLogging {
|
||||
events 'started', 'passed', 'skipped', 'failed'
|
||||
}
|
||||
}
|
||||
includeAndroidResources true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
||||
// AndroidX
|
||||
implementation 'androidx.appcompat:appcompat:1.2.0'
|
||||
implementation 'com.google.android.material:material:1.2.1'
|
||||
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
|
||||
implementation 'com.journeyapps:zxing-android-embedded:3.5.0@aar'
|
||||
implementation 'com.google.zxing:core:3.3.0'
|
||||
implementation 'org.apache.commons:commons-csv:1.5'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
|
||||
implementation 'com.jaredrummler:colorpicker:1.0.2'
|
||||
implementation group: 'com.google.guava', name: 'guava', version: '20.0'
|
||||
implementation 'com.github.apl-devs:appintro:v4.2.0'
|
||||
implementation "com.vanniktech:vntnumberpickerpreference:1.0.0"
|
||||
implementation 'androidx.cardview:cardview:1.0.0'
|
||||
testImplementation 'junit:junit:4.12'
|
||||
testImplementation "org.robolectric:robolectric:4.0.2"
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
|
||||
implementation 'androidx.preference:preference:1.1.1'
|
||||
implementation 'com.google.android.material:material:1.3.0'
|
||||
|
||||
// Third-party
|
||||
implementation 'com.journeyapps:zxing-android-embedded:4.1.0@aar'
|
||||
// Do not upgrade past 3.3.3! Causes a crash on versions before Android Nougat
|
||||
//noinspection GradleDependency
|
||||
implementation 'com.google.zxing:core:3.3.3'
|
||||
implementation 'org.apache.commons:commons-csv:1.8'
|
||||
implementation 'com.jaredrummler:colorpicker:1.1.0'
|
||||
implementation 'com.google.guava:guava:30.1.1-jre'
|
||||
implementation 'com.github.invissvenska:NumberPickerPreference:1.0.2'
|
||||
|
||||
// SpotBugs
|
||||
implementation 'io.wcm.tooling.spotbugs:io.wcm.tooling.spotbugs.annotations:1.0.0'
|
||||
|
||||
// Testing
|
||||
testImplementation 'androidx.test:core:1.3.0'
|
||||
testImplementation 'junit:junit:4.13.2'
|
||||
testImplementation 'org.robolectric:robolectric:4.4'
|
||||
}
|
||||
|
||||
task findbugs(type: FindBugs, dependsOn: 'assembleDebug') {
|
||||
tasks.withType(SpotBugsTask) {
|
||||
|
||||
description 'Run findbugs'
|
||||
description 'Run spotbugs'
|
||||
group 'verification'
|
||||
|
||||
classes = fileTree('build/intermediates/javac/debug/compileDebugJavaWithJavac/classes')
|
||||
source = fileTree('src/main/java')
|
||||
classpath = files()
|
||||
|
||||
effort = 'max'
|
||||
|
||||
excludeFilter = file("./config/findbugs/exclude.xml")
|
||||
//classes = fileTree('build/intermediates/javac/debug/compileDebugJavaWithJavac/classes')
|
||||
//source = fileTree('src/main/java')
|
||||
//classpath = files()
|
||||
|
||||
reports {
|
||||
xml.enabled = false
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
[{"outputType":{"type":"APK"},"apkInfo":{"type":"MAIN","splits":[],"versionCode":41,"versionName":"1.0","enabled":true,"outputFile":"app-release.apk","fullName":"release","baseName":"release"},"path":"app-release.apk","properties":{}}]
|
||||
@@ -1,15 +0,0 @@
|
||||
package protect.card_locker;
|
||||
|
||||
import android.app.Application;
|
||||
import android.test.ApplicationTestCase;
|
||||
|
||||
/**
|
||||
* <a href="http://d.android.com/tools/testing/testing_android.html">Testing Fundamentals</a>
|
||||
*/
|
||||
public class ApplicationTest extends ApplicationTestCase<Application>
|
||||
{
|
||||
public ApplicationTest()
|
||||
{
|
||||
super(Application.class);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest package="protect.card_locker"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-permission
|
||||
android:name="android.permission.CAMERA"/>
|
||||
@@ -16,6 +17,8 @@
|
||||
android:name="android.hardware.camera.autofocus"
|
||||
android:required="false" />
|
||||
|
||||
<uses-sdk tools:overrideLibrary="com.google.zxing.client.android" />
|
||||
|
||||
<application
|
||||
android:name=".LoyaltyCardLockerApplication"
|
||||
android:allowBackup="true"
|
||||
@@ -33,6 +36,11 @@
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".AboutActivity"
|
||||
android:label="@string/about"
|
||||
android:theme="@style/AppTheme.NoActionBar">
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".ManageGroupsActivity"
|
||||
android:label="@string/groups"
|
||||
@@ -41,7 +49,6 @@
|
||||
<activity
|
||||
android:name=".LoyaltyCardViewActivity"
|
||||
android:theme="@style/AppTheme.NoActionBar"
|
||||
android:label=""
|
||||
android:windowSoftInputMode="stateHidden"
|
||||
android:exported="true"/>
|
||||
<activity
|
||||
@@ -62,6 +69,10 @@
|
||||
android:pathPrefix="@string/intent_import_card_from_url_path_prefix_old" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".ScanActivity"
|
||||
android:label="@string/scanCardBarcode"
|
||||
android:theme="@style/AppTheme.NoActionBar"/>
|
||||
<activity
|
||||
android:name=".BarcodeSelectorActivity"
|
||||
android:label="@string/selectBarcodeTitle"
|
||||
|
||||
107
app/src/main/java/protect/card_locker/AboutActivity.java
Normal file
@@ -0,0 +1,107 @@
|
||||
package protect.card_locker;
|
||||
|
||||
import android.content.pm.PackageInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.os.Bundle;
|
||||
import android.text.method.LinkMovementMethod;
|
||||
import android.util.Log;
|
||||
import android.view.MenuItem;
|
||||
import android.widget.TextView;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Calendar;
|
||||
import java.util.List;
|
||||
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.core.text.HtmlCompat;
|
||||
|
||||
public class AboutActivity extends AppCompatActivity
|
||||
{
|
||||
private static final String TAG = "Catima";
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState)
|
||||
{
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.about_activity);
|
||||
Toolbar toolbar = findViewById(R.id.toolbar);
|
||||
setSupportActionBar(toolbar);
|
||||
ActionBar actionBar = getSupportActionBar();
|
||||
if(actionBar != null)
|
||||
{
|
||||
actionBar.setDisplayHomeAsUpEnabled(true);
|
||||
}
|
||||
|
||||
final List<ThirdPartyInfo> USED_LIBRARIES = new ArrayList<>();
|
||||
USED_LIBRARIES.add(new ThirdPartyInfo("Commons CSV", "https://commons.apache.org/proper/commons-csv/", "Apache 2.0"));
|
||||
USED_LIBRARIES.add(new ThirdPartyInfo("Guava", "https://github.com/google/guava", "Apache 2.0"));
|
||||
USED_LIBRARIES.add(new ThirdPartyInfo("ZXing", "https://github.com/zxing/zxing", "Apache 2.0"));
|
||||
USED_LIBRARIES.add(new ThirdPartyInfo("ZXing Android Embedded", "https://github.com/journeyapps/zxing-android-embedded", "Apache 2.0"));
|
||||
USED_LIBRARIES.add(new ThirdPartyInfo("Color Picker", "https://github.com/jaredrummler/ColorPicker", "Apache 2.0"));
|
||||
USED_LIBRARIES.add(new ThirdPartyInfo("NumberPickerPreference", "https://github.com/invissvenska/NumberPickerPreference", "GNU LGPL 3.0"));
|
||||
|
||||
final List<ThirdPartyInfo> USED_ASSETS = new ArrayList<>();
|
||||
USED_ASSETS.add(new ThirdPartyInfo("Android icons", "https://fonts.google.com/icons?selected=Material+Icons", "Apache 2.0"));
|
||||
|
||||
StringBuilder libs = new StringBuilder().append("<br/>");
|
||||
for (ThirdPartyInfo entry : USED_LIBRARIES)
|
||||
{
|
||||
libs.append("<br/><a href=\"").append(entry.url()).append("\">").append(entry.name()).append("</a> (").append(entry.license()).append(")<br/>");
|
||||
}
|
||||
|
||||
StringBuilder resources = new StringBuilder().append("<br/>");
|
||||
for (ThirdPartyInfo entry : USED_ASSETS)
|
||||
{
|
||||
resources.append("<br/><a href=\"").append(entry.url()).append("\">").append(entry.name()).append("</a> (").append(entry.license()).append(")<br/>");
|
||||
}
|
||||
|
||||
String appName = getString(R.string.app_name);
|
||||
int year = Calendar.getInstance().get(Calendar.YEAR);
|
||||
|
||||
String version = "?";
|
||||
try
|
||||
{
|
||||
PackageInfo pi = getPackageManager().getPackageInfo(getPackageName(), 0);
|
||||
version = pi.versionName;
|
||||
}
|
||||
catch (PackageManager.NameNotFoundException e)
|
||||
{
|
||||
Log.w(TAG, "Package name not found", e);
|
||||
}
|
||||
|
||||
setTitle(String.format(getString(R.string.about_title_fmt), appName));
|
||||
|
||||
TextView aboutTextView = findViewById(R.id.aboutText);
|
||||
aboutTextView.setText(HtmlCompat.fromHtml(String.format(getString(R.string.debug_version_fmt), version) +
|
||||
"<br/><br/>" +
|
||||
String.format(getString(R.string.app_revision_fmt),
|
||||
"<a href=\"" + getString(R.string.app_revision_url) + "\">" +
|
||||
"GitHub" +
|
||||
"</a>") +
|
||||
"<br/><br/>" +
|
||||
String.format(getString(R.string.app_copyright_fmt), year) +
|
||||
"<br/><br/>" +
|
||||
getString(R.string.app_copyright_old) +
|
||||
"<br/><br/>" +
|
||||
getString(R.string.app_license) +
|
||||
"<br/><br/>" +
|
||||
String.format(getString(R.string.app_libraries), libs.toString()) +
|
||||
"<br/><br/>" +
|
||||
String.format(getString(R.string.app_resources), resources.toString()), HtmlCompat.FROM_HTML_MODE_COMPACT));
|
||||
aboutTextView.setMovementMethod(LinkMovementMethod.getInstance());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item)
|
||||
{
|
||||
int id = item.getItemId();
|
||||
|
||||
if (id == android.R.id.home) {
|
||||
finish();
|
||||
}
|
||||
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
package protect.card_locker;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.os.AsyncTask;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
@@ -23,6 +25,9 @@ class BarcodeImageWriterTask extends AsyncTask<Void, Void, Bitmap>
|
||||
{
|
||||
private static final String TAG = "Catima";
|
||||
|
||||
private static final int IS_VALID = 999;
|
||||
private boolean isSuccesful;
|
||||
|
||||
// When drawn in a smaller window 1D barcodes for some reason end up
|
||||
// squished, whereas 2D barcodes look fine.
|
||||
private static final int MAX_WIDTH_1D = 1500;
|
||||
@@ -30,14 +35,20 @@ class BarcodeImageWriterTask extends AsyncTask<Void, Void, Bitmap>
|
||||
|
||||
private final WeakReference<ImageView> imageViewReference;
|
||||
private final WeakReference<TextView> textViewReference;
|
||||
private final String cardId;
|
||||
private String cardId;
|
||||
private final BarcodeFormat format;
|
||||
private final int imageHeight;
|
||||
private final int imageWidth;
|
||||
private final boolean showFallback;
|
||||
private final Runnable callback;
|
||||
|
||||
BarcodeImageWriterTask(ImageView imageView, String cardIdString,
|
||||
BarcodeFormat barcodeFormat, TextView textView)
|
||||
BarcodeFormat barcodeFormat, TextView textView,
|
||||
boolean showFallback, Runnable callback)
|
||||
{
|
||||
isSuccesful = true;
|
||||
this.callback = callback;
|
||||
|
||||
// Use a WeakReference to ensure the ImageView can be garbage collected
|
||||
imageViewReference = new WeakReference<>(imageView);
|
||||
textViewReference = new WeakReference<>(textView);
|
||||
@@ -59,11 +70,8 @@ class BarcodeImageWriterTask extends AsyncTask<Void, Void, Bitmap>
|
||||
double ratio = (double)MAX_WIDTH / (double)imageView.getWidth();
|
||||
imageHeight = (int)(imageView.getHeight() * ratio);
|
||||
}
|
||||
}
|
||||
|
||||
BarcodeImageWriterTask(ImageView imageView, String cardIdString, BarcodeFormat barcodeFormat)
|
||||
{
|
||||
this(imageView, cardIdString, barcodeFormat, null);
|
||||
this.showFallback = showFallback;
|
||||
}
|
||||
|
||||
private int getMaxWidth(BarcodeFormat format)
|
||||
@@ -96,7 +104,41 @@ class BarcodeImageWriterTask extends AsyncTask<Void, Void, Bitmap>
|
||||
}
|
||||
}
|
||||
|
||||
public Bitmap doInBackground(Void... params)
|
||||
private String getFallbackString(BarcodeFormat format)
|
||||
{
|
||||
switch(format)
|
||||
{
|
||||
// 2D barcodes
|
||||
case AZTEC:
|
||||
return "AZTEC";
|
||||
case DATA_MATRIX:
|
||||
return "DATA_MATRIX";
|
||||
case PDF_417:
|
||||
return "PDF_417";
|
||||
case QR_CODE:
|
||||
return "QR_CODE";
|
||||
|
||||
// 1D barcodes:
|
||||
case CODABAR:
|
||||
return "C0C";
|
||||
case CODE_39:
|
||||
return "CODE_39";
|
||||
case CODE_128:
|
||||
return "CODE_128";
|
||||
case EAN_8:
|
||||
return "32123456";
|
||||
case EAN_13:
|
||||
return "5901234123457";
|
||||
case ITF:
|
||||
return "1003";
|
||||
case UPC_A:
|
||||
return "123456789012";
|
||||
default:
|
||||
throw new IllegalArgumentException("No fallback known for this barcode type");
|
||||
}
|
||||
}
|
||||
|
||||
private Bitmap generate()
|
||||
{
|
||||
if (cardId.isEmpty())
|
||||
{
|
||||
@@ -165,13 +207,30 @@ class BarcodeImageWriterTask extends AsyncTask<Void, Void, Bitmap>
|
||||
catch(OutOfMemoryError e)
|
||||
{
|
||||
Log.w(TAG, "Insufficient memory to render barcode, "
|
||||
+ imageWidth + "x" + imageHeight + ", " + format.name()
|
||||
+ ", length=" + cardId.length(), e);
|
||||
+ imageWidth + "x" + imageHeight + ", " + format.name()
|
||||
+ ", length=" + cardId.length(), e);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public Bitmap doInBackground(Void... params)
|
||||
{
|
||||
Bitmap bitmap = generate();
|
||||
|
||||
if (bitmap == null) {
|
||||
isSuccesful = false;
|
||||
|
||||
if (showFallback) {
|
||||
Log.i(TAG, "Barcode generation failed, generating fallback...");
|
||||
cardId = getFallbackString(format);
|
||||
bitmap = generate();
|
||||
}
|
||||
}
|
||||
|
||||
return bitmap;
|
||||
}
|
||||
|
||||
protected void onPostExecute(Bitmap result)
|
||||
{
|
||||
Log.i(TAG, "Finished generating barcode image of type " + format + ": " + cardId);
|
||||
@@ -182,6 +241,8 @@ class BarcodeImageWriterTask extends AsyncTask<Void, Void, Bitmap>
|
||||
return;
|
||||
}
|
||||
|
||||
imageView.setTag(isSuccesful);
|
||||
|
||||
imageView.setImageBitmap(result);
|
||||
TextView textView = textViewReference.get();
|
||||
|
||||
@@ -190,6 +251,12 @@ class BarcodeImageWriterTask extends AsyncTask<Void, Void, Bitmap>
|
||||
Log.i(TAG, "Displaying barcode");
|
||||
imageView.setVisibility(View.VISIBLE);
|
||||
|
||||
if (isSuccesful) {
|
||||
imageView.setColorFilter(null);
|
||||
} else {
|
||||
imageView.setColorFilter(Color.LTGRAY, PorterDuff.Mode.LIGHTEN);
|
||||
}
|
||||
|
||||
if (textView != null) {
|
||||
textView.setVisibility(View.VISIBLE);
|
||||
textView.setText(format.name());
|
||||
@@ -203,5 +270,9 @@ class BarcodeImageWriterTask extends AsyncTask<Void, Void, Bitmap>
|
||||
textView.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
if (callback != null) {
|
||||
callback.run();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,11 +3,7 @@ package protect.card_locker;
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import android.text.Editable;
|
||||
import android.text.TextWatcher;
|
||||
import android.util.Log;
|
||||
@@ -18,6 +14,7 @@ import android.view.ViewTreeObserver;
|
||||
import android.widget.EditText;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.zxing.BarcodeFormat;
|
||||
@@ -28,6 +25,10 @@ import java.util.Collections;
|
||||
import java.util.LinkedList;
|
||||
import java.util.Map;
|
||||
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
|
||||
/**
|
||||
* This activity is callable and will allow a user to enter
|
||||
* barcode data and generate all barcodes possible for
|
||||
@@ -105,20 +106,7 @@ public class BarcodeSelectorActivity extends AppCompatActivity
|
||||
{
|
||||
Log.d(TAG, "Entered text: " + s);
|
||||
|
||||
// Stop any async tasks which may not have been started yet
|
||||
for(AsyncTask task : barcodeGeneratorTasks)
|
||||
{
|
||||
task.cancel(false);
|
||||
}
|
||||
barcodeGeneratorTasks.clear();
|
||||
|
||||
// Update barcodes
|
||||
for(String key : barcodeViewMap.keySet())
|
||||
{
|
||||
ImageView image = findViewById(barcodeViewMap.get(key).first);
|
||||
TextView text = findViewById(barcodeViewMap.get(key).second);
|
||||
createBarcodeOption(image, key, s.toString(), text);
|
||||
}
|
||||
generateBarcodes(s.toString());
|
||||
|
||||
View noBarcodeButtonView = findViewById(R.id.noBarcode);
|
||||
setButtonListener(noBarcodeButtonView, s.toString());
|
||||
@@ -138,6 +126,25 @@ public class BarcodeSelectorActivity extends AppCompatActivity
|
||||
if(initialCardId != null)
|
||||
{
|
||||
cardId.setText(initialCardId);
|
||||
} else {
|
||||
generateBarcodes("");
|
||||
}
|
||||
}
|
||||
|
||||
private void generateBarcodes(String value) {
|
||||
// Stop any async tasks which may not have been started yet
|
||||
for(AsyncTask task : barcodeGeneratorTasks)
|
||||
{
|
||||
task.cancel(false);
|
||||
}
|
||||
barcodeGeneratorTasks.clear();
|
||||
|
||||
// Update barcodes
|
||||
for(Map.Entry<String, Pair<Integer, Integer>> entry : barcodeViewMap.entrySet())
|
||||
{
|
||||
ImageView image = findViewById(entry.getValue().first);
|
||||
TextView text = findViewById(entry.getValue().second);
|
||||
createBarcodeOption(image, entry.getKey(), value, text);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -172,6 +179,12 @@ public class BarcodeSelectorActivity extends AppCompatActivity
|
||||
public void onClick(View v)
|
||||
{
|
||||
Log.d(TAG, "Selected barcode type " + formatType);
|
||||
|
||||
if (!((boolean) image.getTag())) {
|
||||
Toast.makeText(BarcodeSelectorActivity.this, getString(R.string.wrongValueForBarcodeType), Toast.LENGTH_LONG).show();
|
||||
return;
|
||||
}
|
||||
|
||||
Intent result = new Intent();
|
||||
result.putExtra(BARCODE_FORMAT, formatType);
|
||||
result.putExtra(BARCODE_CONTENTS, cardId);
|
||||
@@ -191,17 +204,10 @@ public class BarcodeSelectorActivity extends AppCompatActivity
|
||||
public void onGlobalLayout()
|
||||
{
|
||||
Log.d(TAG, "Global layout finished, type: + " + formatType + ", width: " + image.getWidth());
|
||||
if (Build.VERSION.SDK_INT < 16)
|
||||
{
|
||||
image.getViewTreeObserver().removeGlobalOnLayoutListener(this);
|
||||
}
|
||||
else
|
||||
{
|
||||
image.getViewTreeObserver().removeOnGlobalLayoutListener(this);
|
||||
}
|
||||
image.getViewTreeObserver().removeOnGlobalLayoutListener(this);
|
||||
|
||||
Log.d(TAG, "Generating barcode for type " + formatType);
|
||||
BarcodeImageWriterTask task = new BarcodeImageWriterTask(image, cardId, format, text);
|
||||
BarcodeImageWriterTask task = new BarcodeImageWriterTask(image, cardId, format, text, true, null);
|
||||
barcodeGeneratorTasks.add(task);
|
||||
task.execute();
|
||||
}
|
||||
@@ -210,7 +216,7 @@ public class BarcodeSelectorActivity extends AppCompatActivity
|
||||
else
|
||||
{
|
||||
Log.d(TAG, "Generating barcode for type " + formatType);
|
||||
BarcodeImageWriterTask task = new BarcodeImageWriterTask(image, cardId, format, text);
|
||||
BarcodeImageWriterTask task = new BarcodeImageWriterTask(image, cardId, format, text, true, null);
|
||||
barcodeGeneratorTasks.add(task);
|
||||
task.execute();
|
||||
}
|
||||
|
||||
23
app/src/main/java/protect/card_locker/BarcodeValues.java
Normal file
@@ -0,0 +1,23 @@
|
||||
package protect.card_locker;
|
||||
|
||||
public class BarcodeValues {
|
||||
private final String mFormat;
|
||||
private final String mContent;
|
||||
|
||||
public BarcodeValues(String format, String content) {
|
||||
mFormat = format;
|
||||
mContent = content;
|
||||
}
|
||||
|
||||
public String format() {
|
||||
return mFormat;
|
||||
}
|
||||
|
||||
public String content() {
|
||||
return mContent;
|
||||
}
|
||||
|
||||
public boolean isEmpty() {
|
||||
return mFormat == null && mContent == null;
|
||||
}
|
||||
}
|
||||
87
app/src/main/java/protect/card_locker/BaseCursorAdapter.java
Normal file
@@ -0,0 +1,87 @@
|
||||
package protect.card_locker;
|
||||
|
||||
import android.database.Cursor;
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
public abstract class BaseCursorAdapter<V extends RecyclerView.ViewHolder> extends RecyclerView.Adapter<V>
|
||||
{
|
||||
private Cursor mCursor;
|
||||
private boolean mDataValid;
|
||||
private int mRowIDColumn;
|
||||
|
||||
public BaseCursorAdapter(Cursor inputCursor)
|
||||
{
|
||||
setHasStableIds(true);
|
||||
swapCursor(inputCursor);
|
||||
}
|
||||
|
||||
public abstract void onBindViewHolder(V inputHolder, Cursor inputCursor);
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(V inputHolder, int inputPosition)
|
||||
{
|
||||
if (!mDataValid)
|
||||
{
|
||||
throw new IllegalStateException("Cannot bind view holder when cursor is in invalid state.");
|
||||
}
|
||||
|
||||
if (!mCursor.moveToPosition(inputPosition))
|
||||
{
|
||||
throw new IllegalStateException("Could not move cursor to position " + inputPosition + " when trying to bind view holder");
|
||||
}
|
||||
|
||||
onBindViewHolder(inputHolder, mCursor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount()
|
||||
{
|
||||
if (mDataValid)
|
||||
{
|
||||
return mCursor.getCount();
|
||||
}
|
||||
else
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getItemId(int inputPosition)
|
||||
{
|
||||
if (!mDataValid)
|
||||
{
|
||||
throw new IllegalStateException("Cannot lookup item id when cursor is in invalid state.");
|
||||
}
|
||||
|
||||
if (!mCursor.moveToPosition(inputPosition))
|
||||
{
|
||||
throw new IllegalStateException("Could not move cursor to position " + inputPosition + " when trying to get an item id");
|
||||
}
|
||||
|
||||
return mCursor.getLong(mRowIDColumn);
|
||||
}
|
||||
|
||||
public void swapCursor(Cursor inputCursor)
|
||||
{
|
||||
if (inputCursor == mCursor)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (inputCursor != null)
|
||||
{
|
||||
mCursor = inputCursor;
|
||||
mDataValid = true;
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
else
|
||||
{
|
||||
notifyItemRangeRemoved(0, getItemCount());
|
||||
mCursor = null;
|
||||
mRowIDColumn = -1;
|
||||
mDataValid = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,24 +4,26 @@ import android.content.Intent;
|
||||
import android.database.Cursor;
|
||||
import android.os.Bundle;
|
||||
import android.os.Parcelable;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.ListView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.google.android.material.floatingactionbutton.FloatingActionButton;
|
||||
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
/**
|
||||
* The configuration screen for creating a shortcut.
|
||||
*/
|
||||
public class CardShortcutConfigure extends AppCompatActivity
|
||||
public class CardShortcutConfigure extends AppCompatActivity implements LoyaltyCardCursorAdapter.CardAdapterListener
|
||||
{
|
||||
static final String TAG = "Catima";
|
||||
final DBHelper mDb = new DBHelper(this);
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle bundle)
|
||||
{
|
||||
public void onCreate(Bundle bundle) {
|
||||
super.onCreate(bundle);
|
||||
|
||||
// Set the result to CANCELED. This will cause nothing to happen if the
|
||||
@@ -32,52 +34,66 @@ public class CardShortcutConfigure extends AppCompatActivity
|
||||
Toolbar toolbar = findViewById(R.id.toolbar);
|
||||
toolbar.setVisibility(View.GONE);
|
||||
|
||||
// Hide new button because it won't work here anyway
|
||||
FloatingActionButton newFab = findViewById(R.id.fabAdd);
|
||||
newFab.setVisibility(View.GONE);
|
||||
|
||||
final DBHelper db = new DBHelper(this);
|
||||
|
||||
// If there are no cards, bail
|
||||
if(db.getLoyaltyCardCount() == 0)
|
||||
{
|
||||
if (db.getLoyaltyCardCount() == 0) {
|
||||
Toast.makeText(this, R.string.noCardsMessage, Toast.LENGTH_LONG).show();
|
||||
finish();
|
||||
}
|
||||
|
||||
final ListView cardList = findViewById(R.id.list);
|
||||
final RecyclerView cardList = findViewById(R.id.list);
|
||||
cardList.setVisibility(View.VISIBLE);
|
||||
|
||||
Cursor cardCursor = db.getLoyaltyCardCursor();
|
||||
|
||||
final LoyaltyCardCursorAdapter adapter = new LoyaltyCardCursorAdapter(this, cardCursor);
|
||||
final LoyaltyCardCursorAdapter adapter = new LoyaltyCardCursorAdapter(this, cardCursor, this);
|
||||
cardList.setAdapter(adapter);
|
||||
}
|
||||
|
||||
cardList.setOnItemClickListener(new AdapterView.OnItemClickListener()
|
||||
{
|
||||
@Override
|
||||
public void onItemClick(AdapterView<?> parent, View view, int position, long id)
|
||||
{
|
||||
Cursor selected = (Cursor) parent.getItemAtPosition(position);
|
||||
LoyaltyCard loyaltyCard = LoyaltyCard.toLoyaltyCard(selected);
|
||||
private void onClickAction(int position) {
|
||||
Cursor selected = mDb.getLoyaltyCardCursor();
|
||||
selected.moveToPosition(position);
|
||||
LoyaltyCard loyaltyCard = LoyaltyCard.toLoyaltyCard(selected);
|
||||
|
||||
Log.d(TAG, "Creating shortcut for card " + loyaltyCard.store + "," + loyaltyCard.id);
|
||||
Log.d(TAG, "Creating shortcut for card " + loyaltyCard.store + "," + loyaltyCard.id);
|
||||
|
||||
Intent shortcutIntent = new Intent(CardShortcutConfigure.this, LoyaltyCardViewActivity.class);
|
||||
shortcutIntent.setAction(Intent.ACTION_MAIN);
|
||||
// Prevent instances of the view activity from piling up; if one exists let this
|
||||
// one replace it.
|
||||
shortcutIntent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putInt("id", loyaltyCard.id);
|
||||
bundle.putBoolean("view", true);
|
||||
shortcutIntent.putExtras(bundle);
|
||||
Intent shortcutIntent = new Intent(CardShortcutConfigure.this, LoyaltyCardViewActivity.class);
|
||||
shortcutIntent.setAction(Intent.ACTION_MAIN);
|
||||
// Prevent instances of the view activity from piling up; if one exists let this
|
||||
// one replace it.
|
||||
shortcutIntent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putInt("id", loyaltyCard.id);
|
||||
bundle.putBoolean("view", true);
|
||||
shortcutIntent.putExtras(bundle);
|
||||
|
||||
Parcelable icon = Intent.ShortcutIconResource.fromContext(CardShortcutConfigure.this, R.mipmap.ic_launcher);
|
||||
Intent intent = new Intent();
|
||||
intent.putExtra(Intent.EXTRA_SHORTCUT_INTENT, shortcutIntent);
|
||||
intent.putExtra(Intent.EXTRA_SHORTCUT_NAME, loyaltyCard.store);
|
||||
intent.putExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE, icon);
|
||||
setResult(RESULT_OK, intent);
|
||||
Parcelable icon = Intent.ShortcutIconResource.fromContext(CardShortcutConfigure.this, R.mipmap.ic_launcher);
|
||||
Intent intent = new Intent();
|
||||
intent.putExtra(Intent.EXTRA_SHORTCUT_INTENT, shortcutIntent);
|
||||
intent.putExtra(Intent.EXTRA_SHORTCUT_NAME, loyaltyCard.store);
|
||||
intent.putExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE, icon);
|
||||
setResult(RESULT_OK, intent);
|
||||
|
||||
finish();
|
||||
}
|
||||
});
|
||||
finish();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onIconClicked(int inputPosition) {
|
||||
onClickAction(inputPosition);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRowClicked(int inputPosition) {
|
||||
onClickAction(inputPosition);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRowLongClicked(int inputPosition) {
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,38 +4,48 @@ import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.database.sqlite.SQLiteException;
|
||||
import android.database.sqlite.SQLiteOpenHelper;
|
||||
import android.graphics.Color;
|
||||
|
||||
import com.google.zxing.BarcodeFormat;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Currency;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
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 = 5;
|
||||
public static final int DATABASE_VERSION = 10;
|
||||
|
||||
static class LoyaltyCardDbGroups
|
||||
public static class LoyaltyCardDbGroups
|
||||
{
|
||||
public static final String TABLE = "groups";
|
||||
public static final String ID = "_id";
|
||||
public static final String ORDER = "orderId";
|
||||
}
|
||||
|
||||
static class LoyaltyCardDbIds
|
||||
public static class LoyaltyCardDbIds
|
||||
{
|
||||
public static final String TABLE = "cards";
|
||||
public static final String ID = "_id";
|
||||
public static final String STORE = "store";
|
||||
public static final String EXPIRY = "expiry";
|
||||
public static final String BALANCE = "balance";
|
||||
public static final String BALANCE_TYPE = "balancetype";
|
||||
public static final String NOTE = "note";
|
||||
public static final String HEADER_COLOR = "headercolor";
|
||||
public static final String HEADER_TEXT_COLOR = "headertextcolor";
|
||||
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 STAR_STATUS = "starstatus";
|
||||
}
|
||||
|
||||
static class LoyaltyCardDbIdsGroups
|
||||
public static class LoyaltyCardDbIdsGroups
|
||||
{
|
||||
public static final String TABLE = "cardsGroups";
|
||||
public static final String cardID = "cardId";
|
||||
@@ -52,17 +62,22 @@ public class DBHelper extends SQLiteOpenHelper
|
||||
{
|
||||
// create table for card groups
|
||||
db.execSQL("create table " + LoyaltyCardDbGroups.TABLE + "(" +
|
||||
LoyaltyCardDbGroups.ID + " TEXT primary key not null)");
|
||||
LoyaltyCardDbGroups.ID + " TEXT primary key not null," +
|
||||
LoyaltyCardDbGroups.ORDER + " INTEGER DEFAULT '0')");
|
||||
|
||||
// create table for cards
|
||||
// Balance is TEXT and not REAL to be able to store a BigDecimal without precision loss
|
||||
db.execSQL("create table " + LoyaltyCardDbIds.TABLE + "(" +
|
||||
LoyaltyCardDbIds.ID + " INTEGER primary key autoincrement," +
|
||||
LoyaltyCardDbIds.STORE + " TEXT not null," +
|
||||
LoyaltyCardDbIds.NOTE + " TEXT not null," +
|
||||
LoyaltyCardDbIds.EXPIRY + " INTEGER," +
|
||||
LoyaltyCardDbIds.BALANCE + " TEXT not null DEFAULT '0'," +
|
||||
LoyaltyCardDbIds.BALANCE_TYPE + " TEXT," +
|
||||
LoyaltyCardDbIds.HEADER_COLOR + " INTEGER," +
|
||||
LoyaltyCardDbIds.HEADER_TEXT_COLOR + " INTEGER," +
|
||||
LoyaltyCardDbIds.CARD_ID + " TEXT not null," +
|
||||
LoyaltyCardDbIds.BARCODE_TYPE + " TEXT not null," +
|
||||
LoyaltyCardDbIds.BARCODE_ID + " TEXT," +
|
||||
LoyaltyCardDbIds.BARCODE_TYPE + " TEXT," +
|
||||
LoyaltyCardDbIds.STAR_STATUS + " INTEGER DEFAULT '0' )");
|
||||
|
||||
// create associative table for cards in groups
|
||||
@@ -109,55 +124,211 @@ public class DBHelper extends SQLiteOpenHelper
|
||||
LoyaltyCardDbIdsGroups.groupID + " TEXT," +
|
||||
"primary key (" + LoyaltyCardDbIdsGroups.cardID + "," + LoyaltyCardDbIdsGroups.groupID +"))");
|
||||
}
|
||||
|
||||
// Upgrade from version 5 to 6
|
||||
if(oldVersion < 6 && newVersion >= 6)
|
||||
{
|
||||
db.execSQL("ALTER TABLE " + LoyaltyCardDbGroups.TABLE
|
||||
+ " ADD COLUMN " + LoyaltyCardDbGroups.ORDER + " INTEGER DEFAULT '0'");
|
||||
}
|
||||
|
||||
if(oldVersion < 7 && newVersion >= 7)
|
||||
{
|
||||
db.execSQL("ALTER TABLE " + LoyaltyCardDbIds.TABLE
|
||||
+ " ADD COLUMN " + LoyaltyCardDbIds.EXPIRY + " INTEGER");
|
||||
}
|
||||
|
||||
if(oldVersion < 8 && newVersion >= 8)
|
||||
{
|
||||
db.execSQL("ALTER TABLE " + LoyaltyCardDbIds.TABLE
|
||||
+ " ADD COLUMN " + LoyaltyCardDbIds.BALANCE + " TEXT not null DEFAULT '0'");
|
||||
db.execSQL("ALTER TABLE " + LoyaltyCardDbIds.TABLE
|
||||
+ " ADD COLUMN " + LoyaltyCardDbIds.BALANCE_TYPE + " TEXT");
|
||||
}
|
||||
|
||||
if(oldVersion < 9 && newVersion >= 9)
|
||||
{
|
||||
db.execSQL("ALTER TABLE " + LoyaltyCardDbIds.TABLE
|
||||
+ " ADD COLUMN " + LoyaltyCardDbIds.BARCODE_ID + " TEXT");
|
||||
}
|
||||
|
||||
if(oldVersion < 10 && newVersion >= 10)
|
||||
{
|
||||
// SQLite doesn't support modify column
|
||||
// So we need to create a temp column to make barcode type nullable
|
||||
// Let's drop header text colour too while we're at it
|
||||
// https://www.sqlite.org/faq.html#q11
|
||||
db.beginTransaction();
|
||||
|
||||
db.execSQL("CREATE TEMPORARY TABLE tmp (" +
|
||||
LoyaltyCardDbIds.ID + " INTEGER primary key autoincrement," +
|
||||
LoyaltyCardDbIds.STORE + " TEXT not null," +
|
||||
LoyaltyCardDbIds.NOTE + " TEXT not null," +
|
||||
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.STAR_STATUS + " INTEGER DEFAULT '0' )");
|
||||
|
||||
db.execSQL("INSERT INTO tmp (" +
|
||||
LoyaltyCardDbIds.ID + " ," +
|
||||
LoyaltyCardDbIds.STORE + " ," +
|
||||
LoyaltyCardDbIds.NOTE + " ," +
|
||||
LoyaltyCardDbIds.EXPIRY + " ," +
|
||||
LoyaltyCardDbIds.BALANCE + " ," +
|
||||
LoyaltyCardDbIds.BALANCE_TYPE + " ," +
|
||||
LoyaltyCardDbIds.HEADER_COLOR + " ," +
|
||||
LoyaltyCardDbIds.CARD_ID + " ," +
|
||||
LoyaltyCardDbIds.BARCODE_ID + " ," +
|
||||
LoyaltyCardDbIds.BARCODE_TYPE + " ," +
|
||||
LoyaltyCardDbIds.STAR_STATUS + ")" +
|
||||
" SELECT " +
|
||||
LoyaltyCardDbIds.ID + " ," +
|
||||
LoyaltyCardDbIds.STORE + " ," +
|
||||
LoyaltyCardDbIds.NOTE + " ," +
|
||||
LoyaltyCardDbIds.EXPIRY + " ," +
|
||||
LoyaltyCardDbIds.BALANCE + " ," +
|
||||
LoyaltyCardDbIds.BALANCE_TYPE + " ," +
|
||||
LoyaltyCardDbIds.HEADER_COLOR + " ," +
|
||||
LoyaltyCardDbIds.CARD_ID + " ," +
|
||||
LoyaltyCardDbIds.BARCODE_ID + " ," +
|
||||
" NULLIF(" + LoyaltyCardDbIds.BARCODE_TYPE + ",'') ," +
|
||||
LoyaltyCardDbIds.STAR_STATUS +
|
||||
" FROM " + LoyaltyCardDbIds.TABLE);
|
||||
|
||||
db.execSQL("DROP TABLE " + LoyaltyCardDbIds.TABLE);
|
||||
|
||||
db.execSQL("create table " + LoyaltyCardDbIds.TABLE + "(" +
|
||||
LoyaltyCardDbIds.ID + " INTEGER primary key autoincrement," +
|
||||
LoyaltyCardDbIds.STORE + " TEXT not null," +
|
||||
LoyaltyCardDbIds.NOTE + " TEXT not null," +
|
||||
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.STAR_STATUS + " INTEGER DEFAULT '0' )");
|
||||
|
||||
db.execSQL("INSERT INTO " + LoyaltyCardDbIds.TABLE + "(" +
|
||||
LoyaltyCardDbIds.ID + " ," +
|
||||
LoyaltyCardDbIds.STORE + " ," +
|
||||
LoyaltyCardDbIds.NOTE + " ," +
|
||||
LoyaltyCardDbIds.EXPIRY + " ," +
|
||||
LoyaltyCardDbIds.BALANCE + " ," +
|
||||
LoyaltyCardDbIds.BALANCE_TYPE + " ," +
|
||||
LoyaltyCardDbIds.HEADER_COLOR + " ," +
|
||||
LoyaltyCardDbIds.CARD_ID + " ," +
|
||||
LoyaltyCardDbIds.BARCODE_ID + " ," +
|
||||
LoyaltyCardDbIds.BARCODE_TYPE + " ," +
|
||||
LoyaltyCardDbIds.STAR_STATUS + ")" +
|
||||
" SELECT " +
|
||||
LoyaltyCardDbIds.ID + " ," +
|
||||
LoyaltyCardDbIds.STORE + " ," +
|
||||
LoyaltyCardDbIds.NOTE + " ," +
|
||||
LoyaltyCardDbIds.EXPIRY + " ," +
|
||||
LoyaltyCardDbIds.BALANCE + " ," +
|
||||
LoyaltyCardDbIds.BALANCE_TYPE + " ," +
|
||||
LoyaltyCardDbIds.HEADER_COLOR + " ," +
|
||||
LoyaltyCardDbIds.CARD_ID + " ," +
|
||||
LoyaltyCardDbIds.BARCODE_ID + " ," +
|
||||
LoyaltyCardDbIds.BARCODE_TYPE + " ," +
|
||||
LoyaltyCardDbIds.STAR_STATUS +
|
||||
" FROM tmp");
|
||||
|
||||
db.execSQL("DROP TABLE tmp");
|
||||
|
||||
db.setTransactionSuccessful();
|
||||
db.endTransaction();
|
||||
}
|
||||
}
|
||||
|
||||
public long insertLoyaltyCard(final String store, final String note, final String cardId,
|
||||
final String barcodeType, final Integer headerColor,
|
||||
public long insertLoyaltyCard(final String store, final String note, final Date expiry,
|
||||
final BigDecimal balance, final Currency balanceType,
|
||||
final String cardId, final String barcodeId,
|
||||
final BarcodeFormat barcodeType, final Integer headerColor,
|
||||
final int starStatus)
|
||||
{
|
||||
SQLiteDatabase db = getWritableDatabase();
|
||||
ContentValues contentValues = new ContentValues();
|
||||
contentValues.put(LoyaltyCardDbIds.STORE, store);
|
||||
contentValues.put(LoyaltyCardDbIds.NOTE, note);
|
||||
contentValues.put(LoyaltyCardDbIds.EXPIRY, expiry != null ? expiry.getTime() : null);
|
||||
contentValues.put(LoyaltyCardDbIds.BALANCE, balance.toString());
|
||||
contentValues.put(LoyaltyCardDbIds.BALANCE_TYPE, balanceType != null ? balanceType.getCurrencyCode() : null);
|
||||
contentValues.put(LoyaltyCardDbIds.CARD_ID, cardId);
|
||||
contentValues.put(LoyaltyCardDbIds.BARCODE_TYPE, barcodeType);
|
||||
contentValues.put(LoyaltyCardDbIds.BARCODE_ID, barcodeId);
|
||||
contentValues.put(LoyaltyCardDbIds.BARCODE_TYPE, barcodeType != null ? barcodeType.toString() : null);
|
||||
contentValues.put(LoyaltyCardDbIds.HEADER_COLOR, headerColor);
|
||||
contentValues.put(LoyaltyCardDbIds.HEADER_TEXT_COLOR, Color.WHITE);
|
||||
contentValues.put(LoyaltyCardDbIds.STAR_STATUS, starStatus);
|
||||
final long newId = db.insert(LoyaltyCardDbIds.TABLE, null, contentValues);
|
||||
return newId;
|
||||
}
|
||||
|
||||
public boolean insertLoyaltyCard(final SQLiteDatabase db, final String store,
|
||||
final String note, final Date expiry, final BigDecimal balance,
|
||||
final Currency balanceType, final String cardId,
|
||||
final String barcodeId, final BarcodeFormat barcodeType,
|
||||
final Integer headerColor, final int starStatus)
|
||||
{
|
||||
ContentValues contentValues = new ContentValues();
|
||||
contentValues.put(LoyaltyCardDbIds.STORE, store);
|
||||
contentValues.put(LoyaltyCardDbIds.NOTE, note);
|
||||
contentValues.put(LoyaltyCardDbIds.EXPIRY, expiry != null ? expiry.getTime() : null);
|
||||
contentValues.put(LoyaltyCardDbIds.BALANCE, balance.toString());
|
||||
contentValues.put(LoyaltyCardDbIds.BALANCE_TYPE, balanceType != null ? balanceType.getCurrencyCode() : null);
|
||||
contentValues.put(LoyaltyCardDbIds.CARD_ID, cardId);
|
||||
contentValues.put(LoyaltyCardDbIds.BARCODE_ID, barcodeId);
|
||||
contentValues.put(LoyaltyCardDbIds.BARCODE_TYPE, barcodeType != null ? barcodeType.toString() : null);
|
||||
contentValues.put(LoyaltyCardDbIds.HEADER_COLOR, headerColor);
|
||||
contentValues.put(LoyaltyCardDbIds.STAR_STATUS,starStatus);
|
||||
final long newId = db.insert(LoyaltyCardDbIds.TABLE, null, contentValues);
|
||||
return (newId != -1);
|
||||
}
|
||||
|
||||
public boolean insertLoyaltyCard(final SQLiteDatabase db, final int id, final String store,
|
||||
final String note, final String cardId,
|
||||
final String barcodeType, final Integer headerColor,
|
||||
final int starStatus)
|
||||
final String note, final Date expiry, final BigDecimal balance,
|
||||
final Currency balanceType, final String cardId,
|
||||
final String barcodeId, final BarcodeFormat barcodeType,
|
||||
final Integer headerColor, final int starStatus)
|
||||
{
|
||||
ContentValues contentValues = new ContentValues();
|
||||
contentValues.put(LoyaltyCardDbIds.ID, id);
|
||||
contentValues.put(LoyaltyCardDbIds.STORE, store);
|
||||
contentValues.put(LoyaltyCardDbIds.NOTE, note);
|
||||
contentValues.put(LoyaltyCardDbIds.EXPIRY, expiry != null ? expiry.getTime() : null);
|
||||
contentValues.put(LoyaltyCardDbIds.BALANCE, balance.toString());
|
||||
contentValues.put(LoyaltyCardDbIds.BALANCE_TYPE, balanceType != null ? balanceType.getCurrencyCode() : null);
|
||||
contentValues.put(LoyaltyCardDbIds.CARD_ID, cardId);
|
||||
contentValues.put(LoyaltyCardDbIds.BARCODE_TYPE, barcodeType);
|
||||
contentValues.put(LoyaltyCardDbIds.BARCODE_ID, barcodeId);
|
||||
contentValues.put(LoyaltyCardDbIds.BARCODE_TYPE, barcodeType != null ? barcodeType.toString() : null);
|
||||
contentValues.put(LoyaltyCardDbIds.HEADER_COLOR, headerColor);
|
||||
contentValues.put(LoyaltyCardDbIds.HEADER_TEXT_COLOR, Color.WHITE);
|
||||
contentValues.put(LoyaltyCardDbIds.STAR_STATUS,starStatus);
|
||||
final long newId = db.insert(LoyaltyCardDbIds.TABLE, null, contentValues);
|
||||
return (newId != -1);
|
||||
}
|
||||
|
||||
public boolean updateLoyaltyCard(final int id, final String store, final String note,
|
||||
final String cardId, final String barcodeType,
|
||||
final Date expiry, final BigDecimal balance,
|
||||
final Currency balanceType, final String cardId,
|
||||
final String barcodeId, final BarcodeFormat barcodeType,
|
||||
final Integer headerColor)
|
||||
{
|
||||
SQLiteDatabase db = getWritableDatabase();
|
||||
ContentValues contentValues = new ContentValues();
|
||||
contentValues.put(LoyaltyCardDbIds.STORE, store);
|
||||
contentValues.put(LoyaltyCardDbIds.NOTE, note);
|
||||
contentValues.put(LoyaltyCardDbIds.EXPIRY, expiry != null ? expiry.getTime() : null);
|
||||
contentValues.put(LoyaltyCardDbIds.BALANCE, balance.toString());
|
||||
contentValues.put(LoyaltyCardDbIds.BALANCE_TYPE, balanceType != null ? balanceType.getCurrencyCode() : null);
|
||||
contentValues.put(LoyaltyCardDbIds.CARD_ID, cardId);
|
||||
contentValues.put(LoyaltyCardDbIds.BARCODE_TYPE, barcodeType);
|
||||
contentValues.put(LoyaltyCardDbIds.BARCODE_ID, barcodeId);
|
||||
contentValues.put(LoyaltyCardDbIds.BARCODE_TYPE, barcodeType != null ? barcodeType.toString() : null);
|
||||
contentValues.put(LoyaltyCardDbIds.HEADER_COLOR, headerColor);
|
||||
contentValues.put(LoyaltyCardDbIds.HEADER_TEXT_COLOR, Color.WHITE);
|
||||
int rowsUpdated = db.update(LoyaltyCardDbIds.TABLE, contentValues,
|
||||
LoyaltyCardDbIds.ID + "=?",
|
||||
new String[]{Integer.toString(id)});
|
||||
@@ -206,6 +377,7 @@ public class DBHelper extends SQLiteOpenHelper
|
||||
List<Group> groups = new ArrayList<>();
|
||||
|
||||
if (!data.moveToFirst()) {
|
||||
data.close();
|
||||
return groups;
|
||||
}
|
||||
|
||||
@@ -215,6 +387,8 @@ public class DBHelper extends SQLiteOpenHelper
|
||||
groups.add(Group.toGroup(data));
|
||||
}
|
||||
|
||||
data.close();
|
||||
|
||||
return groups;
|
||||
}
|
||||
|
||||
@@ -376,7 +550,7 @@ public class DBHelper extends SQLiteOpenHelper
|
||||
SQLiteDatabase db = getReadableDatabase();
|
||||
|
||||
Cursor res = db.rawQuery("select * from " + LoyaltyCardDbGroups.TABLE +
|
||||
" ORDER BY " + LoyaltyCardDbGroups.ID + " COLLATE NOCASE ASC", null, null);
|
||||
" ORDER BY " + LoyaltyCardDbGroups.ORDER + " ASC," + LoyaltyCardDbGroups.ID + " COLLATE NOCASE ASC", null, null);
|
||||
return res;
|
||||
}
|
||||
|
||||
@@ -386,6 +560,7 @@ public class DBHelper extends SQLiteOpenHelper
|
||||
List<Group> groups = new ArrayList<>();
|
||||
|
||||
if (!data.moveToFirst()) {
|
||||
data.close();
|
||||
return groups;
|
||||
}
|
||||
|
||||
@@ -395,9 +570,31 @@ public class DBHelper extends SQLiteOpenHelper
|
||||
groups.add(Group.toGroup(data));
|
||||
}
|
||||
|
||||
data.close();
|
||||
|
||||
return groups;
|
||||
}
|
||||
|
||||
public void reorderGroups(final List<Group> groups)
|
||||
{
|
||||
Integer order = 0;
|
||||
|
||||
SQLiteDatabase db = getWritableDatabase();
|
||||
ContentValues contentValues;
|
||||
|
||||
for (Group group : groups)
|
||||
{
|
||||
contentValues = new ContentValues();
|
||||
contentValues.put(LoyaltyCardDbGroups.ORDER, order);
|
||||
|
||||
db.update(LoyaltyCardDbGroups.TABLE, contentValues,
|
||||
LoyaltyCardDbGroups.ID + "=?",
|
||||
new String[]{group._id});
|
||||
|
||||
order++;
|
||||
}
|
||||
}
|
||||
|
||||
public Group getGroup(final String groupName)
|
||||
{
|
||||
SQLiteDatabase db = getReadableDatabase();
|
||||
@@ -467,6 +664,7 @@ public class DBHelper extends SQLiteOpenHelper
|
||||
SQLiteDatabase db = getWritableDatabase();
|
||||
ContentValues contentValues = new ContentValues();
|
||||
contentValues.put(LoyaltyCardDbGroups.ID, name);
|
||||
contentValues.put(LoyaltyCardDbGroups.ORDER, getGroupCount());
|
||||
final long newId = db.insert(LoyaltyCardDbGroups.TABLE, null, contentValues);
|
||||
return newId;
|
||||
}
|
||||
@@ -475,6 +673,7 @@ public class DBHelper extends SQLiteOpenHelper
|
||||
{
|
||||
ContentValues contentValues = new ContentValues();
|
||||
contentValues.put(LoyaltyCardDbGroups.ID, name);
|
||||
contentValues.put(LoyaltyCardDbGroups.ORDER, getGroupCount());
|
||||
final long newId = db.insert(LoyaltyCardDbGroups.TABLE, null, contentValues);
|
||||
return (newId != -1);
|
||||
}
|
||||
@@ -483,33 +682,66 @@ public class DBHelper extends SQLiteOpenHelper
|
||||
{
|
||||
if (newName.isEmpty()) return false;
|
||||
|
||||
boolean success = false;
|
||||
|
||||
SQLiteDatabase db = getWritableDatabase();
|
||||
ContentValues contentValues = new ContentValues();
|
||||
contentValues.put(LoyaltyCardDbGroups.ID, newName);
|
||||
ContentValues groupContentValues = new ContentValues();
|
||||
groupContentValues.put(LoyaltyCardDbGroups.ID, newName);
|
||||
|
||||
ContentValues lookupContentValues = new ContentValues();
|
||||
lookupContentValues.put(LoyaltyCardDbIdsGroups.groupID, newName);
|
||||
|
||||
db.beginTransaction();
|
||||
try {
|
||||
int rowsUpdated = db.update(LoyaltyCardDbGroups.TABLE, contentValues,
|
||||
// Update group name
|
||||
int groupsChanged = db.update(LoyaltyCardDbGroups.TABLE, groupContentValues,
|
||||
LoyaltyCardDbGroups.ID + "=?",
|
||||
new String[]{groupName});
|
||||
return (rowsUpdated == 1);
|
||||
} catch (android.database.sqlite.SQLiteConstraintException _e) {
|
||||
return false;
|
||||
|
||||
// Also update lookup tables
|
||||
db.update(LoyaltyCardDbIdsGroups.TABLE, lookupContentValues,
|
||||
LoyaltyCardDbIdsGroups.groupID + "=?",
|
||||
new String[]{groupName});
|
||||
|
||||
if (groupsChanged == 1) {
|
||||
db.setTransactionSuccessful();
|
||||
success = true;
|
||||
}
|
||||
} catch (SQLiteException e) {
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
public boolean deleteGroup(final String groupName)
|
||||
{
|
||||
boolean success = false;
|
||||
|
||||
SQLiteDatabase db = getWritableDatabase();
|
||||
// Delete group
|
||||
int rowsDeleted = db.delete(LoyaltyCardDbGroups.TABLE,
|
||||
LoyaltyCardDbGroups.ID + " = ? ",
|
||||
new String[]{groupName});
|
||||
|
||||
// And delete lookup table entries associated with this group
|
||||
db.delete(LoyaltyCardDbIdsGroups.TABLE,
|
||||
LoyaltyCardDbIdsGroups.groupID + " = ? ",
|
||||
new String[]{groupName});
|
||||
db.beginTransaction();
|
||||
try {
|
||||
// Delete group
|
||||
int groupsDeleted = db.delete(LoyaltyCardDbGroups.TABLE,
|
||||
LoyaltyCardDbGroups.ID + " = ? ",
|
||||
new String[]{groupName});
|
||||
|
||||
return (rowsDeleted == 1);
|
||||
// And delete lookup table entries associated with this group
|
||||
db.delete(LoyaltyCardDbIdsGroups.TABLE,
|
||||
LoyaltyCardDbIdsGroups.groupID + " = ? ",
|
||||
new String[]{groupName});
|
||||
|
||||
if (groupsDeleted == 1) {
|
||||
db.setTransactionSuccessful();
|
||||
success = true;
|
||||
}
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
public int getGroupCardCount(final String groupName)
|
||||
|
||||
@@ -2,7 +2,8 @@ package protect.card_locker;
|
||||
|
||||
public enum DataFormat
|
||||
{
|
||||
CSV,
|
||||
|
||||
Catima,
|
||||
Fidme,
|
||||
VoucherVault
|
||||
;
|
||||
}
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
package protect.card_locker;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
|
||||
/**
|
||||
* Interface for a class which can import the contents of a stream
|
||||
* into the database.
|
||||
*/
|
||||
public interface DatabaseImporter
|
||||
{
|
||||
/**
|
||||
* Import data from the input stream in a given format into
|
||||
* the database.
|
||||
* @throws IOException
|
||||
* @throws FormatException
|
||||
*/
|
||||
void importData(DBHelper db, InputStreamReader input) throws IOException, FormatException, InterruptedException;
|
||||
}
|
||||
@@ -37,17 +37,20 @@ class GroupCursorAdapter extends CursorAdapter
|
||||
public void bindView(View view, Context context, Cursor cursor)
|
||||
{
|
||||
// Find fields to populate in inflated template
|
||||
TextView nameField = (TextView) view.findViewById(R.id.name);
|
||||
TextView countField = (TextView) view.findViewById(R.id.cardCount);
|
||||
TextView nameField = view.findViewById(R.id.name);
|
||||
TextView countField = view.findViewById(R.id.cardCount);
|
||||
|
||||
// Extract properties from cursor
|
||||
Group group = Group.toGroup(cursor);
|
||||
|
||||
Integer groupCardCount = db.getGroupCardCount(group._id);
|
||||
|
||||
// Populate fields with extracted properties
|
||||
nameField.setText(group._id);
|
||||
countField.setText(String.format(context.getString(R.string.groupCardCount), db.getGroupCardCount(group._id)));
|
||||
|
||||
nameField.setTextSize(settings.getCardTitleListFontSize());
|
||||
countField.setTextSize(settings.getCardNoteListFontSize());
|
||||
countField.setText(context.getResources().getQuantityString(R.plurals.groupCardCount, groupCardCount, groupCardCount));
|
||||
|
||||
nameField.setTextSize(settings.getFontSizeMax(settings.getMediumFont()));
|
||||
countField.setTextSize(settings.getFontSizeMax(settings.getSmallFont()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,22 +2,12 @@ package protect.card_locker;
|
||||
|
||||
import android.Manifest;
|
||||
import android.content.ActivityNotFoundException;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.pm.ResolveInfo;
|
||||
import android.net.Uri;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.core.app.ActivityCompat;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
|
||||
import android.util.Log;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
@@ -30,7 +20,13 @@ import java.io.FileNotFoundException;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.util.List;
|
||||
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.core.app.ActivityCompat;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
public class ImportExportActivity extends AppCompatActivity
|
||||
{
|
||||
@@ -38,10 +34,14 @@ public class ImportExportActivity extends AppCompatActivity
|
||||
|
||||
private static final int PERMISSIONS_EXTERNAL_STORAGE = 1;
|
||||
private static final int CHOOSE_EXPORT_LOCATION = 2;
|
||||
private static final int CHOOSE_EXPORTED_FILE = 3;
|
||||
private static final int IMPORT = 3;
|
||||
|
||||
private ImportExportTask importExporter;
|
||||
|
||||
private String importAlertTitle;
|
||||
private String importAlertMessage;
|
||||
private DataFormat importDataFormat;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState)
|
||||
{
|
||||
@@ -96,18 +96,10 @@ public class ImportExportActivity extends AppCompatActivity
|
||||
@Override
|
||||
public void onClick(View v)
|
||||
{
|
||||
chooseFileWithIntent(intentGetContentAction, CHOOSE_EXPORTED_FILE);
|
||||
chooseImportType(intentGetContentAction);
|
||||
}
|
||||
});
|
||||
|
||||
if(isCallable(getApplicationContext(), intentGetContentAction) == false)
|
||||
{
|
||||
findViewById(R.id.dividerImportFilesystem).setVisibility(View.GONE);
|
||||
findViewById(R.id.importOptionFilesystemTitle).setVisibility(View.GONE);
|
||||
findViewById(R.id.importOptionFilesystemExplanation).setVisibility(View.GONE);
|
||||
importFilesystem.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
// Check that there is an app that data can be imported from
|
||||
final Intent intentPickAction = new Intent(Intent.ACTION_PICK);
|
||||
|
||||
@@ -117,20 +109,60 @@ public class ImportExportActivity extends AppCompatActivity
|
||||
@Override
|
||||
public void onClick(View v)
|
||||
{
|
||||
chooseFileWithIntent(intentPickAction, CHOOSE_EXPORTED_FILE);
|
||||
chooseImportType(intentPickAction);
|
||||
}
|
||||
});
|
||||
|
||||
if(isCallable(getApplicationContext(), intentPickAction) == false)
|
||||
{
|
||||
findViewById(R.id.dividerImportApplication).setVisibility(View.GONE);
|
||||
findViewById(R.id.importOptionApplicationTitle).setVisibility(View.GONE);
|
||||
findViewById(R.id.importOptionApplicationExplanation).setVisibility(View.GONE);
|
||||
importApplication.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
private void startImport(final InputStream target, final Uri targetUri)
|
||||
private void chooseImportType(Intent baseIntent) {
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(this);
|
||||
builder.setTitle(R.string.chooseImportType)
|
||||
.setItems(R.array.import_types_array, (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");
|
||||
}
|
||||
|
||||
new AlertDialog.Builder(this)
|
||||
.setTitle(importAlertTitle)
|
||||
.setMessage(importAlertMessage)
|
||||
.setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
chooseFileWithIntent(baseIntent, IMPORT);
|
||||
}
|
||||
})
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.show();
|
||||
});
|
||||
builder.show();
|
||||
}
|
||||
|
||||
private void startImport(final InputStream target, final Uri targetUri, final DataFormat dataFormat)
|
||||
{
|
||||
ImportExportTask.TaskCompleteListener listener = new ImportExportTask.TaskCompleteListener()
|
||||
{
|
||||
@@ -142,7 +174,7 @@ public class ImportExportActivity extends AppCompatActivity
|
||||
};
|
||||
|
||||
importExporter = new ImportExportTask(ImportExportActivity.this,
|
||||
DataFormat.CSV, target, listener);
|
||||
dataFormat, target, listener);
|
||||
importExporter.execute();
|
||||
}
|
||||
|
||||
@@ -158,7 +190,7 @@ public class ImportExportActivity extends AppCompatActivity
|
||||
};
|
||||
|
||||
importExporter = new ImportExportTask(ImportExportActivity.this,
|
||||
DataFormat.CSV, target, listener);
|
||||
DataFormat.Catima, target, listener);
|
||||
importExporter.execute();
|
||||
}
|
||||
|
||||
@@ -295,31 +327,6 @@ public class ImportExportActivity extends AppCompatActivity
|
||||
builder.create().show();
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if there is at least one activity that can perform the given intent
|
||||
*/
|
||||
private boolean isCallable(Context context, final Intent intent)
|
||||
{
|
||||
PackageManager manager = context.getPackageManager();
|
||||
if(manager == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
List<ResolveInfo> list = manager.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY);
|
||||
|
||||
for(ResolveInfo info : list)
|
||||
{
|
||||
if(info.activityInfo.exported)
|
||||
{
|
||||
// There is one activity which is available to be called
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void chooseFileWithIntent(Intent intent, int requestCode)
|
||||
{
|
||||
try
|
||||
@@ -328,6 +335,7 @@ public class ImportExportActivity extends AppCompatActivity
|
||||
}
|
||||
catch (ActivityNotFoundException e)
|
||||
{
|
||||
Toast.makeText(getApplicationContext(), R.string.failedOpeningFileManager, Toast.LENGTH_LONG).show();
|
||||
Log.e(TAG, "No activity found to handle intent", e);
|
||||
}
|
||||
}
|
||||
@@ -337,7 +345,7 @@ public class ImportExportActivity extends AppCompatActivity
|
||||
{
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
|
||||
if (resultCode != RESULT_OK || (requestCode != CHOOSE_EXPORT_LOCATION && requestCode != CHOOSE_EXPORTED_FILE))
|
||||
if (resultCode != RESULT_OK)
|
||||
{
|
||||
Log.w(TAG, "Failed onActivityResult(), result=" + resultCode);
|
||||
return;
|
||||
@@ -379,8 +387,9 @@ public class ImportExportActivity extends AppCompatActivity
|
||||
reader = new FileInputStream(new File(uri.toString()));
|
||||
}
|
||||
|
||||
Log.e(TAG, "Starting file export with: " + uri.toString());
|
||||
startImport(reader, uri);
|
||||
Log.e(TAG, "Starting file import with: " + uri.toString());
|
||||
|
||||
startImport(reader, uri, importDataFormat);
|
||||
}
|
||||
}
|
||||
catch(FileNotFoundException e)
|
||||
|
||||
@@ -8,11 +8,13 @@ import android.util.Log;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.OutputStream;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.nio.charset.Charset;
|
||||
|
||||
import protect.card_locker.importexport.MultiFormatExporter;
|
||||
import protect.card_locker.importexport.MultiFormatImporter;
|
||||
|
||||
class ImportExportTask extends AsyncTask<Void, Void, Boolean>
|
||||
{
|
||||
private static final String TAG = "Catima";
|
||||
@@ -58,16 +60,8 @@ class ImportExportTask extends AsyncTask<Void, Void, Boolean>
|
||||
{
|
||||
boolean result = false;
|
||||
|
||||
try
|
||||
{
|
||||
InputStreamReader reader = new InputStreamReader(stream, Charset.forName("UTF-8"));
|
||||
result = MultiFormatImporter.importData(db, reader, format);
|
||||
reader.close();
|
||||
}
|
||||
catch(IOException e)
|
||||
{
|
||||
Log.e(TAG, "Unable to import file", e);
|
||||
}
|
||||
|
||||
result = MultiFormatImporter.importData(db, stream, format);
|
||||
|
||||
Log.i(TAG, "Import result: " + result);
|
||||
|
||||
|
||||
@@ -3,12 +3,23 @@ package protect.card_locker;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
|
||||
import com.google.zxing.BarcodeFormat;
|
||||
|
||||
import java.io.InvalidObjectException;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Currency;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
public class ImportURIHelper {
|
||||
private static final String STORE = DBHelper.LoyaltyCardDbIds.STORE;
|
||||
private static final String NOTE = DBHelper.LoyaltyCardDbIds.NOTE;
|
||||
private static final String EXPIRY = DBHelper.LoyaltyCardDbIds.EXPIRY;
|
||||
private static final String BALANCE = DBHelper.LoyaltyCardDbIds.BALANCE;
|
||||
private static final String BALANCE_TYPE = DBHelper.LoyaltyCardDbIds.BALANCE_TYPE;
|
||||
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 HEADER_COLOR = DBHelper.LoyaltyCardDbIds.HEADER_COLOR;
|
||||
@@ -19,6 +30,7 @@ public class ImportURIHelper {
|
||||
private final String oldHost;
|
||||
private final String oldPath;
|
||||
private final String shareText;
|
||||
private final String shareMultipleText;
|
||||
|
||||
public ImportURIHelper(Context context) {
|
||||
this.context = context;
|
||||
@@ -27,6 +39,7 @@ public class ImportURIHelper {
|
||||
oldHost = "brarcher.github.io";
|
||||
oldPath = "/loyalty-card-locker/share";
|
||||
shareText = context.getResources().getString(R.string.intent_import_card_from_url_share_text);
|
||||
shareMultipleText = context.getResources().getString(R.string.intent_import_card_from_url_share_multiple_text);
|
||||
}
|
||||
|
||||
private boolean isImportUri(Uri uri) {
|
||||
@@ -40,14 +53,39 @@ public class ImportURIHelper {
|
||||
|
||||
try {
|
||||
// These values are allowed to be null
|
||||
BarcodeFormat barcodeType = null;
|
||||
Date expiry = null;
|
||||
BigDecimal balance = new BigDecimal("0");
|
||||
Currency balanceType = null;
|
||||
Integer headerColor = null;
|
||||
Integer headerTextColor = null;
|
||||
|
||||
String store = uri.getQueryParameter(STORE);
|
||||
String note = uri.getQueryParameter(NOTE);
|
||||
String cardId = uri.getQueryParameter(CARD_ID);
|
||||
String barcodeType = uri.getQueryParameter(BARCODE_TYPE);
|
||||
if (store == null || note == null || cardId == null || barcodeType == null) throw new InvalidObjectException("Not a valid import URI");
|
||||
String barcodeId = uri.getQueryParameter(BARCODE_ID);
|
||||
if (store == null || note == null || cardId == null) throw new InvalidObjectException("Not a valid import URI");
|
||||
|
||||
String unparsedBarcodeType = uri.getQueryParameter(BARCODE_TYPE);
|
||||
if(unparsedBarcodeType != null && !unparsedBarcodeType.equals(""))
|
||||
{
|
||||
barcodeType = BarcodeFormat.valueOf(unparsedBarcodeType);
|
||||
}
|
||||
|
||||
String unparsedBalance = uri.getQueryParameter(BALANCE);
|
||||
if(unparsedBalance != null && !unparsedBalance.equals(""))
|
||||
{
|
||||
balance = new BigDecimal(unparsedBalance);
|
||||
}
|
||||
String unparsedBalanceType = uri.getQueryParameter(BALANCE_TYPE);
|
||||
if (unparsedBalanceType != null && !unparsedBalanceType.equals(""))
|
||||
{
|
||||
balanceType = Currency.getInstance(unparsedBalanceType);
|
||||
}
|
||||
String unparsedExpiry = uri.getQueryParameter(EXPIRY);
|
||||
if(unparsedExpiry != null && !unparsedExpiry.equals(""))
|
||||
{
|
||||
expiry = new Date(Long.parseLong(unparsedExpiry));
|
||||
}
|
||||
|
||||
String unparsedHeaderColor = uri.getQueryParameter(HEADER_COLOR);
|
||||
if(unparsedHeaderColor != null)
|
||||
@@ -55,7 +93,7 @@ public class ImportURIHelper {
|
||||
headerColor = Integer.parseInt(unparsedHeaderColor);
|
||||
}
|
||||
|
||||
return new LoyaltyCard(-1, store, note, cardId, barcodeType, headerColor, headerTextColor, 0);
|
||||
return new LoyaltyCard(-1, store, note, expiry, balance, balanceType, cardId, barcodeId, barcodeType, headerColor, 0);
|
||||
} catch (NullPointerException | NumberFormatException ex) {
|
||||
throw new InvalidObjectException("Not a valid import URI");
|
||||
}
|
||||
@@ -69,27 +107,55 @@ public class ImportURIHelper {
|
||||
uriBuilder.path(path);
|
||||
uriBuilder.appendQueryParameter(STORE, loyaltyCard.store);
|
||||
uriBuilder.appendQueryParameter(NOTE, loyaltyCard.note);
|
||||
uriBuilder.appendQueryParameter(BALANCE, loyaltyCard.balance.toString());
|
||||
if (loyaltyCard.balanceType != null) {
|
||||
uriBuilder.appendQueryParameter(BALANCE_TYPE, loyaltyCard.balanceType.getCurrencyCode());
|
||||
}
|
||||
if (loyaltyCard.expiry != null) {
|
||||
uriBuilder.appendQueryParameter(EXPIRY, String.valueOf(loyaltyCard.expiry.getTime()));
|
||||
}
|
||||
uriBuilder.appendQueryParameter(CARD_ID, loyaltyCard.cardId);
|
||||
uriBuilder.appendQueryParameter(BARCODE_TYPE, loyaltyCard.barcodeType);
|
||||
if(loyaltyCard.headerColor != null)
|
||||
{
|
||||
if(loyaltyCard.barcodeId != null) {
|
||||
uriBuilder.appendQueryParameter(BARCODE_ID, loyaltyCard.barcodeId);
|
||||
}
|
||||
|
||||
if(loyaltyCard.barcodeType != null) {
|
||||
uriBuilder.appendQueryParameter(BARCODE_TYPE, loyaltyCard.barcodeType.toString());
|
||||
}
|
||||
if(loyaltyCard.headerColor != null) {
|
||||
uriBuilder.appendQueryParameter(HEADER_COLOR, loyaltyCard.headerColor.toString());
|
||||
}
|
||||
//StarStatus will not be exported
|
||||
return uriBuilder.build();
|
||||
}
|
||||
|
||||
private void startShareIntent(Uri uri) {
|
||||
public void startShareIntent(List<LoyaltyCard> loyaltyCards) {
|
||||
int loyaltyCardCount = loyaltyCards.size();
|
||||
|
||||
StringBuilder text = new StringBuilder();
|
||||
if (loyaltyCardCount == 1) {
|
||||
text.append(shareText);
|
||||
} else {
|
||||
text.append(shareMultipleText);
|
||||
}
|
||||
text.append("\n\n");
|
||||
|
||||
for (int i = 0; i < loyaltyCardCount; i++) {
|
||||
LoyaltyCard loyaltyCard = loyaltyCards.get(i);
|
||||
|
||||
text.append(loyaltyCard.store + ": " + toUri(loyaltyCard));
|
||||
|
||||
if (i < (loyaltyCardCount - 1)) {
|
||||
text.append("\n\n");
|
||||
}
|
||||
}
|
||||
|
||||
Intent sendIntent = new Intent();
|
||||
sendIntent.setAction(Intent.ACTION_SEND);
|
||||
sendIntent.putExtra(Intent.EXTRA_TEXT, shareText + "\n" + uri.toString());
|
||||
sendIntent.putExtra(Intent.EXTRA_TEXT, text.toString());
|
||||
sendIntent.setType("text/plain");
|
||||
|
||||
Intent shareIntent = Intent.createChooser(sendIntent, null);
|
||||
context.startActivity(shareIntent);
|
||||
}
|
||||
|
||||
public void startShareIntent(LoyaltyCard loyaltyCard) {
|
||||
startShareIntent(toUri(loyaltyCard));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,12 @@ package protect.card_locker;
|
||||
|
||||
import android.database.Cursor;
|
||||
|
||||
import com.google.zxing.BarcodeFormat;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Currency;
|
||||
import java.util.Date;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
public class LoyaltyCard
|
||||
@@ -9,28 +15,36 @@ public class LoyaltyCard
|
||||
public final int id;
|
||||
public final String store;
|
||||
public final String note;
|
||||
public final Date expiry;
|
||||
public final BigDecimal balance;
|
||||
public final Currency balanceType;
|
||||
public final String cardId;
|
||||
public final String barcodeType;
|
||||
|
||||
@Nullable
|
||||
public final String barcodeId;
|
||||
|
||||
public final BarcodeFormat barcodeType;
|
||||
|
||||
@Nullable
|
||||
public final Integer headerColor;
|
||||
|
||||
@Nullable
|
||||
public final Integer headerTextColor;
|
||||
|
||||
public final int starStatus;
|
||||
|
||||
public LoyaltyCard(final int id, final String store, final String note, final String cardId,
|
||||
final String barcodeType, final Integer headerColor, final Integer headerTextColor,
|
||||
public LoyaltyCard(final int id, final String store, final String note, final Date expiry,
|
||||
final BigDecimal balance, final Currency balanceType, final String cardId,
|
||||
final String barcodeId, final BarcodeFormat barcodeType, final Integer headerColor,
|
||||
final int starStatus)
|
||||
{
|
||||
this.id = id;
|
||||
this.store = store;
|
||||
this.note = note;
|
||||
this.expiry = expiry;
|
||||
this.balance = balance;
|
||||
this.balanceType = balanceType;
|
||||
this.cardId = cardId;
|
||||
this.barcodeId = barcodeId;
|
||||
this.barcodeType = barcodeType;
|
||||
this.headerColor = headerColor;
|
||||
this.headerTextColor = headerTextColor;
|
||||
this.starStatus = starStatus;
|
||||
}
|
||||
|
||||
@@ -39,27 +53,41 @@ public class LoyaltyCard
|
||||
int id = cursor.getInt(cursor.getColumnIndexOrThrow(DBHelper.LoyaltyCardDbIds.ID));
|
||||
String store = cursor.getString(cursor.getColumnIndexOrThrow(DBHelper.LoyaltyCardDbIds.STORE));
|
||||
String note = cursor.getString(cursor.getColumnIndexOrThrow(DBHelper.LoyaltyCardDbIds.NOTE));
|
||||
long expiryLong = cursor.getLong(cursor.getColumnIndexOrThrow(DBHelper.LoyaltyCardDbIds.EXPIRY));
|
||||
BigDecimal balance = new BigDecimal(cursor.getString(cursor.getColumnIndexOrThrow(DBHelper.LoyaltyCardDbIds.BALANCE)));
|
||||
String cardId = cursor.getString(cursor.getColumnIndexOrThrow(DBHelper.LoyaltyCardDbIds.CARD_ID));
|
||||
String barcodeType = cursor.getString(cursor.getColumnIndexOrThrow(DBHelper.LoyaltyCardDbIds.BARCODE_TYPE));
|
||||
String barcodeId = cursor.getString(cursor.getColumnIndexOrThrow(DBHelper.LoyaltyCardDbIds.BARCODE_ID));
|
||||
int starred = cursor.getInt(cursor.getColumnIndexOrThrow(DBHelper.LoyaltyCardDbIds.STAR_STATUS));
|
||||
|
||||
|
||||
int barcodeTypeColumn = cursor.getColumnIndexOrThrow(DBHelper.LoyaltyCardDbIds.BARCODE_TYPE);
|
||||
int balanceTypeColumn = cursor.getColumnIndexOrThrow(DBHelper.LoyaltyCardDbIds.BALANCE_TYPE);
|
||||
int headerColorColumn = cursor.getColumnIndexOrThrow(DBHelper.LoyaltyCardDbIds.HEADER_COLOR);
|
||||
int headerTextColorColumn = cursor.getColumnIndexOrThrow(DBHelper.LoyaltyCardDbIds.HEADER_TEXT_COLOR);
|
||||
|
||||
BarcodeFormat barcodeType = null;
|
||||
Currency balanceType = null;
|
||||
Date expiry = null;
|
||||
Integer headerColor = null;
|
||||
Integer headerTextColor = null;
|
||||
|
||||
if (cursor.isNull(barcodeTypeColumn) == false)
|
||||
{
|
||||
barcodeType = BarcodeFormat.valueOf(cursor.getString(barcodeTypeColumn));
|
||||
}
|
||||
|
||||
if (cursor.isNull(balanceTypeColumn) == false)
|
||||
{
|
||||
balanceType = Currency.getInstance(cursor.getString(balanceTypeColumn));
|
||||
}
|
||||
|
||||
if(expiryLong > 0)
|
||||
{
|
||||
expiry = new Date(expiryLong);
|
||||
}
|
||||
|
||||
if(cursor.isNull(headerColorColumn) == false)
|
||||
{
|
||||
headerColor = cursor.getInt(headerColorColumn);
|
||||
}
|
||||
|
||||
if(cursor.isNull(headerTextColorColumn) == false)
|
||||
{
|
||||
headerTextColor = cursor.getInt(headerTextColorColumn);
|
||||
}
|
||||
|
||||
return new LoyaltyCard(id, store, note, cardId, barcodeType, headerColor, headerTextColor, starred);
|
||||
return new LoyaltyCard(id, store, note, expiry, balance, balanceType, cardId, barcodeId, barcodeType, headerColor, starred);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
package protect.card_locker;
|
||||
|
||||
import android.animation.AnimatorInflater;
|
||||
import android.animation.AnimatorSet;
|
||||
import android.content.Context;
|
||||
import android.view.View;
|
||||
|
||||
public class LoyaltyCardAnimator {
|
||||
|
||||
private static AnimatorSet selectedViewIn, defaultViewOut, selectedViewOut, defaultViewIn;
|
||||
|
||||
public static void flipView(Context inputContext, final View inputSelectedView, final View inputDefaultView, boolean inputItemSelected) {
|
||||
|
||||
selectedViewIn = (AnimatorSet) AnimatorInflater.loadAnimator(inputContext, R.animator.flip_left_in);
|
||||
defaultViewOut = (AnimatorSet) AnimatorInflater.loadAnimator(inputContext, R.animator.flip_right_out);
|
||||
selectedViewOut = (AnimatorSet) AnimatorInflater.loadAnimator(inputContext, R.animator.flip_left_out);
|
||||
defaultViewIn = (AnimatorSet) AnimatorInflater.loadAnimator(inputContext, R.animator.flip_right_in);
|
||||
|
||||
final AnimatorSet showFrontAnim = new AnimatorSet();
|
||||
final AnimatorSet showBackAnim = new AnimatorSet();
|
||||
|
||||
selectedViewIn.setTarget(inputSelectedView);
|
||||
defaultViewOut.setTarget(inputDefaultView);
|
||||
showFrontAnim.playTogether(selectedViewIn, defaultViewOut);
|
||||
|
||||
selectedViewOut.setTarget(inputSelectedView);
|
||||
defaultViewIn.setTarget(inputDefaultView);
|
||||
showBackAnim.playTogether(defaultViewIn, selectedViewOut);
|
||||
|
||||
if (inputItemSelected) {
|
||||
showFrontAnim.start();
|
||||
} else {
|
||||
showBackAnim.start();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -4,73 +4,294 @@ import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.util.SparseBooleanArray;
|
||||
import android.view.HapticFeedbackConstants;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.CursorAdapter;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.RelativeLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.text.DateFormat;
|
||||
import java.util.ArrayList;
|
||||
|
||||
import androidx.cardview.widget.CardView;
|
||||
import androidx.constraintlayout.widget.ConstraintLayout;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
|
||||
import protect.card_locker.preferences.Settings;
|
||||
|
||||
class LoyaltyCardCursorAdapter extends CursorAdapter
|
||||
public class LoyaltyCardCursorAdapter extends BaseCursorAdapter<LoyaltyCardCursorAdapter.LoyaltyCardListItemViewHolder>
|
||||
{
|
||||
Settings settings;
|
||||
boolean darkModeEnabled;
|
||||
private static int mCurrentSelectedIndex = -1;
|
||||
private Cursor mCursor;
|
||||
Settings mSettings;
|
||||
boolean mDarkModeEnabled;
|
||||
private Context mContext;
|
||||
private CardAdapterListener mListener;
|
||||
private SparseBooleanArray mSelectedItems;
|
||||
private SparseBooleanArray mAnimationItemsIndex;
|
||||
private boolean mReverseAllAnimations = false;
|
||||
|
||||
public LoyaltyCardCursorAdapter(Context context, Cursor cursor)
|
||||
public LoyaltyCardCursorAdapter(Context inputContext, Cursor inputCursor, CardAdapterListener inputListener)
|
||||
{
|
||||
super(context, cursor, 0);
|
||||
settings = new Settings(context);
|
||||
darkModeEnabled= MainActivity.isDarkModeEnabled(context);
|
||||
super(inputCursor);
|
||||
mSettings = new Settings(inputContext);
|
||||
mCursor = inputCursor;
|
||||
mContext = inputContext;
|
||||
mListener = inputListener;
|
||||
mSelectedItems = new SparseBooleanArray();
|
||||
mAnimationItemsIndex = new SparseBooleanArray();
|
||||
|
||||
mDarkModeEnabled = MainActivity.isDarkModeEnabled(inputContext);
|
||||
}
|
||||
|
||||
// The newView method is used to inflate a new view and return it,
|
||||
// you don't bind any data to the view at this point.
|
||||
@Override
|
||||
public View newView(Context context, Cursor cursor, ViewGroup parent)
|
||||
public LoyaltyCardListItemViewHolder onCreateViewHolder(ViewGroup inputParent, int inputViewType)
|
||||
{
|
||||
return LayoutInflater.from(context).inflate(R.layout.loyalty_card_layout, parent, false);
|
||||
View itemView = LayoutInflater.from(inputParent.getContext()).inflate(R.layout.loyalty_card_layout, inputParent, false);
|
||||
return new LoyaltyCardListItemViewHolder(itemView);
|
||||
}
|
||||
|
||||
// The bindView method is used to bind all data to a given view
|
||||
// such as setting the text on a TextView.
|
||||
@Override
|
||||
public void bindView(View view, Context context, Cursor cursor)
|
||||
public Cursor getCursor()
|
||||
{
|
||||
// Find fields to populate in inflated template
|
||||
ImageView thumbnail = view.findViewById(R.id.thumbnail);
|
||||
TextView storeField = view.findViewById(R.id.store);
|
||||
TextView noteField = view.findViewById(R.id.note);
|
||||
ImageView star = view.findViewById(R.id.star);
|
||||
return mCursor;
|
||||
}
|
||||
|
||||
if(darkModeEnabled)
|
||||
{
|
||||
star.setColorFilter(Color.WHITE, PorterDuff.Mode.SRC_ATOP);
|
||||
public void onBindViewHolder(LoyaltyCardListItemViewHolder inputHolder, Cursor inputCursor) {
|
||||
if (mDarkModeEnabled) {
|
||||
inputHolder.mStarIcon.setColorFilter(Color.WHITE, PorterDuff.Mode.SRC_ATOP);
|
||||
}
|
||||
|
||||
// Extract properties from cursor
|
||||
LoyaltyCard loyaltyCard = LoyaltyCard.toLoyaltyCard(cursor);
|
||||
LoyaltyCard loyaltyCard = LoyaltyCard.toLoyaltyCard(inputCursor);
|
||||
|
||||
// Populate fields with extracted properties
|
||||
storeField.setText(loyaltyCard.store);
|
||||
inputHolder.mStoreField.setText(loyaltyCard.store);
|
||||
inputHolder.mStoreField.setTextSize(mSettings.getFontSizeMax(mSettings.getMediumFont()));
|
||||
if (!loyaltyCard.note.isEmpty()) {
|
||||
inputHolder.mNoteField.setVisibility(View.VISIBLE);
|
||||
inputHolder.mNoteField.setText(loyaltyCard.note);
|
||||
inputHolder.mNoteField.setTextSize(mSettings.getFontSizeMax(mSettings.getSmallFont()));
|
||||
} else {
|
||||
inputHolder.mNoteField.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
storeField.setTextSize(settings.getCardTitleListFontSize());
|
||||
if (!loyaltyCard.balance.equals(new BigDecimal("0"))) {
|
||||
inputHolder.mBalanceField.setVisibility(View.VISIBLE);
|
||||
inputHolder.mBalanceField.setText(mContext.getString(R.string.balanceSentence, Utils.formatBalance(mContext, loyaltyCard.balance, loyaltyCard.balanceType)));
|
||||
inputHolder.mBalanceField.setTextSize(mSettings.getFontSizeMax(mSettings.getSmallFont()));
|
||||
} else {
|
||||
inputHolder.mBalanceField.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
if(!loyaltyCard.note.isEmpty())
|
||||
if (loyaltyCard.expiry != null)
|
||||
{
|
||||
noteField.setVisibility(View.VISIBLE);
|
||||
noteField.setText(loyaltyCard.note);
|
||||
noteField.setTextSize(settings.getCardNoteListFontSize());
|
||||
inputHolder.mExpiryField.setVisibility(View.VISIBLE);
|
||||
int expiryString = R.string.expiryStateSentence;
|
||||
if(Utils.hasExpired(loyaltyCard.expiry)) {
|
||||
expiryString = R.string.expiryStateSentenceExpired;
|
||||
inputHolder.mExpiryField.setTextColor(mContext.getResources().getColor(R.color.alert));
|
||||
}
|
||||
inputHolder.mExpiryField.setText(mContext.getString(expiryString, DateFormat.getDateInstance(DateFormat.LONG).format(loyaltyCard.expiry)));
|
||||
inputHolder.mExpiryField.setTextSize(mSettings.getFontSizeMax(mSettings.getSmallFont()));
|
||||
} else {
|
||||
inputHolder.mExpiryField.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
inputHolder.mStarIcon.setVisibility((loyaltyCard.starStatus != 0) ? View.VISIBLE : View.GONE);
|
||||
inputHolder.mCardIcon.setImageBitmap(Utils.generateIcon(mContext, loyaltyCard.store, loyaltyCard.headerColor).getLetterTile());
|
||||
|
||||
inputHolder.itemView.setActivated(mSelectedItems.get(inputCursor.getPosition(), false));
|
||||
applyIconAnimation(inputHolder, inputCursor.getPosition());
|
||||
applyClickEvents(inputHolder, inputCursor.getPosition());
|
||||
|
||||
}
|
||||
|
||||
private void applyClickEvents(LoyaltyCardListItemViewHolder inputHolder, final int inputPosition)
|
||||
{
|
||||
inputHolder.mThumbnailContainer.setOnClickListener(new View.OnClickListener()
|
||||
{
|
||||
@Override
|
||||
public void onClick(View inputView)
|
||||
{
|
||||
mListener.onIconClicked(inputPosition);
|
||||
}
|
||||
});
|
||||
|
||||
inputHolder.mRow.setOnClickListener(new View.OnClickListener()
|
||||
{
|
||||
@Override
|
||||
public void onClick(View inputView)
|
||||
{
|
||||
mListener.onRowClicked(inputPosition);
|
||||
}
|
||||
});
|
||||
|
||||
inputHolder.mInformationContainer.setOnClickListener(new View.OnClickListener()
|
||||
{
|
||||
@Override
|
||||
public void onClick(View inputView)
|
||||
{
|
||||
mListener.onRowClicked(inputPosition);
|
||||
}
|
||||
});
|
||||
|
||||
inputHolder.mRow.setOnLongClickListener(new View.OnLongClickListener()
|
||||
{
|
||||
@Override
|
||||
public boolean onLongClick(View inputView)
|
||||
{
|
||||
mListener.onRowLongClicked(inputPosition);
|
||||
inputView.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
inputHolder.mInformationContainer.setOnLongClickListener(new View.OnLongClickListener()
|
||||
{
|
||||
@Override
|
||||
public boolean onLongClick(View inputView)
|
||||
{
|
||||
mListener.onRowLongClicked(inputPosition);
|
||||
inputView.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void applyIconAnimation(LoyaltyCardListItemViewHolder inputHolder, int inputPosition)
|
||||
{
|
||||
if (mSelectedItems.get(inputPosition, false))
|
||||
{
|
||||
inputHolder.mThumbnailFrontContainer.setVisibility(View.GONE);
|
||||
resetIconYAxis(inputHolder.mThumbnailBackContainer);
|
||||
inputHolder.mThumbnailBackContainer.setVisibility(View.VISIBLE);
|
||||
inputHolder.mThumbnailBackContainer.setAlpha(1);
|
||||
if (mCurrentSelectedIndex == inputPosition)
|
||||
{
|
||||
LoyaltyCardAnimator.flipView(mContext, inputHolder.mThumbnailBackContainer, inputHolder.mThumbnailFrontContainer, true);
|
||||
resetCurrentIndex();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
noteField.setVisibility(View.GONE);
|
||||
inputHolder.mThumbnailBackContainer.setVisibility(View.GONE);
|
||||
resetIconYAxis(inputHolder.mThumbnailFrontContainer);
|
||||
inputHolder.mThumbnailFrontContainer.setVisibility(View.VISIBLE);
|
||||
inputHolder.mThumbnailFrontContainer.setAlpha(1);
|
||||
if ((mReverseAllAnimations && mAnimationItemsIndex.get(inputPosition, false)) || mCurrentSelectedIndex == inputPosition)
|
||||
{
|
||||
LoyaltyCardAnimator.flipView(mContext, inputHolder.mThumbnailBackContainer, inputHolder.mThumbnailFrontContainer, false);
|
||||
resetCurrentIndex();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void resetIconYAxis(View inputView)
|
||||
{
|
||||
if (inputView.getRotationY() != 0)
|
||||
{
|
||||
inputView.setRotationY(0);
|
||||
}
|
||||
}
|
||||
|
||||
public void resetAnimationIndex()
|
||||
{
|
||||
mReverseAllAnimations = false;
|
||||
mAnimationItemsIndex.clear();
|
||||
}
|
||||
|
||||
@SuppressFBWarnings("ST_WRITE_TO_STATIC_FROM_INSTANCE_METHOD")
|
||||
public void toggleSelection(int inputPosition)
|
||||
{
|
||||
mCurrentSelectedIndex = inputPosition;
|
||||
if (mSelectedItems.get(inputPosition, false))
|
||||
{
|
||||
mSelectedItems.delete(inputPosition);
|
||||
mAnimationItemsIndex.delete(inputPosition);
|
||||
}
|
||||
else
|
||||
{
|
||||
mSelectedItems.put(inputPosition, true);
|
||||
mAnimationItemsIndex.put(inputPosition, true);
|
||||
}
|
||||
notifyItemChanged(inputPosition);
|
||||
}
|
||||
|
||||
public void clearSelections()
|
||||
{
|
||||
mReverseAllAnimations = true;
|
||||
mSelectedItems.clear();
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
public int getSelectedItemCount()
|
||||
{
|
||||
return mSelectedItems.size();
|
||||
}
|
||||
|
||||
public ArrayList<LoyaltyCard> getSelectedItems()
|
||||
{
|
||||
|
||||
ArrayList<LoyaltyCard> result = new ArrayList<>();
|
||||
|
||||
int i;
|
||||
for(i = 0; i < mSelectedItems.size(); i++)
|
||||
{
|
||||
mCursor.moveToPosition(mSelectedItems.keyAt(i));
|
||||
result.add(LoyaltyCard.toLoyaltyCard(mCursor));
|
||||
}
|
||||
|
||||
if (loyaltyCard.starStatus!=0) star.setVisibility(View.VISIBLE);
|
||||
else star.setVisibility(View.GONE);
|
||||
|
||||
thumbnail.setImageBitmap(Utils.generateIcon(context, loyaltyCard.store, loyaltyCard.headerColor).getLetterTile());
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
private void resetCurrentIndex()
|
||||
{
|
||||
mCurrentSelectedIndex = -1;
|
||||
}
|
||||
|
||||
public interface CardAdapterListener
|
||||
{
|
||||
void onIconClicked(int inputPosition);
|
||||
void onRowClicked(int inputPosition);
|
||||
void onRowLongClicked(int inputPosition);
|
||||
}
|
||||
|
||||
public class LoyaltyCardListItemViewHolder extends RecyclerView.ViewHolder implements View.OnLongClickListener
|
||||
{
|
||||
|
||||
public TextView mStoreField, mNoteField, mBalanceField, mExpiryField;
|
||||
public LinearLayout mInformationContainer;
|
||||
public ImageView mCardIcon, mStarIcon;
|
||||
public CardView mThumbnailContainer;
|
||||
public ConstraintLayout mRow;
|
||||
public RelativeLayout mThumbnailFrontContainer, mThumbnailBackContainer;
|
||||
|
||||
public LoyaltyCardListItemViewHolder(View inputView)
|
||||
{
|
||||
super(inputView);
|
||||
mThumbnailContainer = inputView.findViewById(R.id.thumbnail_container);
|
||||
mRow = inputView.findViewById(R.id.row);
|
||||
mThumbnailFrontContainer = inputView.findViewById(R.id.thumbnail_front);
|
||||
mThumbnailBackContainer = inputView.findViewById(R.id.thumbnail_back);
|
||||
mInformationContainer = inputView.findViewById(R.id.information_container);
|
||||
mStoreField = inputView.findViewById(R.id.store);
|
||||
mNoteField = inputView.findViewById(R.id.note);
|
||||
mBalanceField = inputView.findViewById(R.id.balance);
|
||||
mExpiryField = inputView.findViewById(R.id.expiry);
|
||||
mCardIcon = inputView.findViewById(R.id.thumbnail);
|
||||
mStarIcon = inputView.findViewById(R.id.star);
|
||||
inputView.setOnLongClickListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onLongClick(View inputView)
|
||||
{
|
||||
mListener.onRowLongClicked(getAdapterPosition());
|
||||
inputView.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
package protect.card_locker;
|
||||
|
||||
import android.app.Application;
|
||||
import androidx.appcompat.app.AppCompatDelegate;
|
||||
|
||||
import androidx.appcompat.app.AppCompatDelegate;
|
||||
import protect.card_locker.preferences.Settings;
|
||||
|
||||
public class LoyaltyCardLockerApplication extends Application {
|
||||
|
||||
@@ -2,34 +2,44 @@ package protect.card_locker;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.content.pm.ActivityInfo;
|
||||
import android.content.res.Configuration;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.constraintlayout.widget.ConstraintLayout;
|
||||
import androidx.core.graphics.drawable.DrawableCompat;
|
||||
import androidx.core.widget.TextViewCompat;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.content.res.AppCompatResources;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import android.util.DisplayMetrics;
|
||||
import android.util.Log;
|
||||
import android.util.TypedValue;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.ViewTreeObserver;
|
||||
import android.view.Window;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.SeekBar;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.google.android.material.appbar.AppBarLayout;
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior;
|
||||
import com.google.android.material.floatingactionbutton.FloatingActionButton;
|
||||
import com.google.zxing.BarcodeFormat;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.text.DateFormat;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.content.res.AppCompatResources;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.constraintlayout.widget.Guideline;
|
||||
import androidx.core.graphics.drawable.DrawableCompat;
|
||||
import androidx.core.widget.TextViewCompat;
|
||||
import protect.card_locker.preferences.Settings;
|
||||
|
||||
public class LoyaltyCardViewActivity extends AppCompatActivity
|
||||
@@ -37,11 +47,19 @@ public class LoyaltyCardViewActivity extends AppCompatActivity
|
||||
private static final String TAG = "Catima";
|
||||
|
||||
TextView cardIdFieldView;
|
||||
BottomSheetBehavior behavior;
|
||||
View bottomSheet;
|
||||
ImageView bottomSheetButton;
|
||||
TextView noteView;
|
||||
View noteViewDivider;
|
||||
TextView groupsView;
|
||||
TextView balanceView;
|
||||
TextView expiryView;
|
||||
TextView storeName;
|
||||
ImageButton maximizeButton;
|
||||
ImageView barcodeImage;
|
||||
ImageButton minimizeButton;
|
||||
View collapsingToolbarLayout;
|
||||
AppBarLayout appBarLayout;
|
||||
int loyaltyCardId;
|
||||
LoyaltyCard loyaltyCard;
|
||||
boolean rotationEnabled;
|
||||
@@ -50,12 +68,17 @@ public class LoyaltyCardViewActivity extends AppCompatActivity
|
||||
Settings settings;
|
||||
|
||||
String cardIdString;
|
||||
String barcodeIdString;
|
||||
BarcodeFormat format;
|
||||
|
||||
FloatingActionButton editButton;
|
||||
|
||||
Guideline centerGuideline;
|
||||
SeekBar barcodeScaler;
|
||||
|
||||
boolean starred;
|
||||
boolean backgroundNeedsDarkIcons;
|
||||
boolean barcodeIsFullscreen = false;
|
||||
ViewGroup.LayoutParams barcodeImageState;
|
||||
|
||||
private void extractIntentFields(Intent intent)
|
||||
{
|
||||
@@ -67,6 +90,7 @@ public class LoyaltyCardViewActivity extends AppCompatActivity
|
||||
private Drawable getIcon(int icon, boolean dark)
|
||||
{
|
||||
Drawable unwrappedIcon = AppCompatResources.getDrawable(this, icon);
|
||||
assert unwrappedIcon != null;
|
||||
Drawable wrappedIcon = DrawableCompat.wrap(unwrappedIcon);
|
||||
if(dark)
|
||||
{
|
||||
@@ -91,39 +115,102 @@ public class LoyaltyCardViewActivity extends AppCompatActivity
|
||||
|
||||
setContentView(R.layout.loyalty_card_view_layout);
|
||||
|
||||
Toolbar toolbar = findViewById(R.id.toolbar);
|
||||
setSupportActionBar(toolbar);
|
||||
ActionBar actionBar = getSupportActionBar();
|
||||
if(actionBar != null)
|
||||
{
|
||||
actionBar.setDisplayHomeAsUpEnabled(true);
|
||||
}
|
||||
|
||||
db = new DBHelper(this);
|
||||
importURIHelper = new ImportURIHelper(this);
|
||||
|
||||
cardIdFieldView = findViewById(R.id.cardIdView);
|
||||
bottomSheet = findViewById(R.id.bottom_sheet);
|
||||
bottomSheetButton = findViewById(R.id.bottomSheetButton);
|
||||
noteView = findViewById(R.id.noteView);
|
||||
noteViewDivider = findViewById(R.id.noteViewDivider);
|
||||
groupsView = findViewById(R.id.groupsView);
|
||||
balanceView = findViewById(R.id.balanceView);
|
||||
expiryView = findViewById(R.id.expiryView);
|
||||
storeName = findViewById(R.id.storeName);
|
||||
maximizeButton = findViewById(R.id.maximizeButton);
|
||||
barcodeImage = findViewById(R.id.barcode);
|
||||
minimizeButton = findViewById(R.id.minimizeButton);
|
||||
collapsingToolbarLayout = findViewById(R.id.collapsingToolbarLayout);
|
||||
appBarLayout = findViewById(R.id.app_bar_layout);
|
||||
|
||||
centerGuideline = findViewById(R.id.centerGuideline);
|
||||
centerGuideline.setGuidelinePercent(0.5f);
|
||||
barcodeScaler = findViewById(R.id.barcodeScaler);
|
||||
barcodeScaler.setProgress(100);
|
||||
barcodeScaler.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
|
||||
@Override
|
||||
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
|
||||
Log.d(TAG, "Progress is " + progress);
|
||||
Log.d(TAG, "Max is " + barcodeScaler.getMax());
|
||||
float scale = (float) progress / (float) barcodeScaler.getMax();
|
||||
Log.d(TAG, "Scaling to " + scale);
|
||||
|
||||
redrawBarcodeAfterResize();
|
||||
centerGuideline.setGuidelinePercent(0.5f * scale);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStartTrackingTouch(SeekBar seekBar) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStopTrackingTouch(SeekBar seekBar) {
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
rotationEnabled = true;
|
||||
|
||||
// Allow making barcode fullscreen on tap
|
||||
barcodeImage.setOnClickListener(new View.OnClickListener() {
|
||||
maximizeButton.setOnClickListener(v -> setFullscreen(true));
|
||||
barcodeImage.setOnClickListener(view -> {
|
||||
if (barcodeIsFullscreen)
|
||||
{
|
||||
setFullscreen(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
setFullscreen(true);
|
||||
}
|
||||
});
|
||||
minimizeButton.setOnClickListener(v -> setFullscreen(false));
|
||||
|
||||
editButton = findViewById(R.id.fabEdit);
|
||||
editButton.setOnClickListener(v -> {
|
||||
Intent intent = new Intent(getApplicationContext(), LoyaltyCardEditActivity.class);
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putInt("id", loyaltyCardId);
|
||||
bundle.putBoolean("update", true);
|
||||
intent.putExtras(bundle);
|
||||
startActivity(intent);
|
||||
finish();
|
||||
});
|
||||
|
||||
behavior = BottomSheetBehavior.from(bottomSheet);
|
||||
behavior.addBottomSheetCallback(new BottomSheetBehavior.BottomSheetCallback() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
if(barcodeIsFullscreen)
|
||||
{
|
||||
setFullscreen(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
setFullscreen(true);
|
||||
public void onStateChanged(@NonNull View bottomSheet, int newState) {
|
||||
if (newState == BottomSheetBehavior.STATE_DRAGGING) {
|
||||
editButton.hide();
|
||||
} else if (newState == BottomSheetBehavior.STATE_EXPANDED) {
|
||||
bottomSheetButton.setImageResource(R.drawable.ic_baseline_arrow_drop_down_24);
|
||||
editButton.hide();
|
||||
} else if (newState == BottomSheetBehavior.STATE_COLLAPSED) {
|
||||
bottomSheetButton.setImageResource(R.drawable.ic_baseline_arrow_drop_up_24);
|
||||
editButton.show();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSlide(@NonNull View bottomSheet, float slideOffset) { }
|
||||
});
|
||||
|
||||
bottomSheetButton.setOnClickListener(v -> {
|
||||
if (behavior.getState() == BottomSheetBehavior.STATE_EXPANDED) {
|
||||
behavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
|
||||
} else {
|
||||
behavior.setState(BottomSheetBehavior.STATE_EXPANDED);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -143,23 +230,28 @@ public class LoyaltyCardViewActivity extends AppCompatActivity
|
||||
|
||||
Log.i(TAG, "To view card: " + loyaltyCardId);
|
||||
|
||||
if(barcodeIsFullscreen)
|
||||
{
|
||||
// Completely reset state
|
||||
//
|
||||
// This prevents the barcode from taking up the entire screen
|
||||
// on resume and thus being stretched out of proportion.
|
||||
recreate();
|
||||
}
|
||||
|
||||
// The brightness value is on a scale from [0, ..., 1], where
|
||||
// '1' is the brightest. We attempt to maximize the brightness
|
||||
// to help barcode readers scan the barcode.
|
||||
Window window = getWindow();
|
||||
if(window != null && settings.useMaxBrightnessDisplayingBarcode())
|
||||
if(window != null)
|
||||
{
|
||||
WindowManager.LayoutParams attributes = window.getAttributes();
|
||||
attributes.screenBrightness = 1F;
|
||||
|
||||
if (settings.useMaxBrightnessDisplayingBarcode())
|
||||
{
|
||||
attributes.screenBrightness = 1F;
|
||||
}
|
||||
|
||||
if (settings.getKeepScreenOn()) {
|
||||
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
|
||||
}
|
||||
|
||||
if (settings.getDisableLockscreenWhileViewingCard()) {
|
||||
window.addFlags(WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD|
|
||||
WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED);
|
||||
}
|
||||
|
||||
window.setAttributes(attributes);
|
||||
}
|
||||
|
||||
@@ -172,30 +264,79 @@ public class LoyaltyCardViewActivity extends AppCompatActivity
|
||||
return;
|
||||
}
|
||||
|
||||
String formatString = loyaltyCard.barcodeType;
|
||||
format = !formatString.isEmpty() ? BarcodeFormat.valueOf(formatString) : null;
|
||||
setupOrientation();
|
||||
|
||||
format = loyaltyCard.barcodeType;
|
||||
cardIdString = loyaltyCard.cardId;
|
||||
barcodeIdString = loyaltyCard.barcodeId;
|
||||
|
||||
cardIdFieldView.setText(loyaltyCard.cardId);
|
||||
TextViewCompat.setAutoSizeTextTypeUniformWithConfiguration(cardIdFieldView,
|
||||
getResources().getInteger(R.integer.settings_card_id_min_font_size_sp)-1, settings.getCardIdFontSize(),
|
||||
settings.getFontSizeMin(settings.getLargeFont()), settings.getFontSizeMax(settings.getLargeFont()),
|
||||
1, TypedValue.COMPLEX_UNIT_SP);
|
||||
|
||||
if(loyaltyCard.note.length() > 0)
|
||||
{
|
||||
noteView.setVisibility(View.VISIBLE);
|
||||
noteView.setText(loyaltyCard.note);
|
||||
TextViewCompat.setAutoSizeTextTypeUniformWithConfiguration(noteView,
|
||||
getResources().getInteger(R.integer.settings_card_note_min_font_size_sp)-1,
|
||||
settings.getCardNoteFontSize(), 1, TypedValue.COMPLEX_UNIT_SP);
|
||||
noteView.setTextSize(settings.getFontSizeMax(settings.getMediumFont()));
|
||||
}
|
||||
else
|
||||
{
|
||||
noteView.setVisibility(View.GONE);
|
||||
noteViewDivider.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
List<Group> loyaltyCardGroups = db.getLoyaltyCardGroups(loyaltyCardId);
|
||||
|
||||
if(loyaltyCardGroups.size() > 0) {
|
||||
StringBuilder groupsString = new StringBuilder();
|
||||
for (Group group : loyaltyCardGroups) {
|
||||
groupsString.append(group._id);
|
||||
groupsString.append(" ");
|
||||
}
|
||||
|
||||
groupsView.setVisibility(View.VISIBLE);
|
||||
groupsView.setText(getString(R.string.groupsList, groupsString.toString()));
|
||||
groupsView.setTextSize(settings.getFontSizeMax(settings.getMediumFont()));
|
||||
}
|
||||
else
|
||||
{
|
||||
groupsView.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
if(!loyaltyCard.balance.equals(new BigDecimal(0))) {
|
||||
balanceView.setVisibility(View.VISIBLE);
|
||||
balanceView.setText(getString(R.string.balanceSentence, Utils.formatBalance(this, loyaltyCard.balance, loyaltyCard.balanceType)));
|
||||
balanceView.setTextSize(settings.getFontSizeMax(settings.getMediumFont()));
|
||||
}
|
||||
else
|
||||
{
|
||||
balanceView.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
if(loyaltyCard.expiry != null) {
|
||||
expiryView.setVisibility(View.VISIBLE);
|
||||
|
||||
int expiryString = R.string.expiryStateSentence;
|
||||
if(Utils.hasExpired(loyaltyCard.expiry)) {
|
||||
expiryString = R.string.expiryStateSentenceExpired;
|
||||
expiryView.setTextColor(getResources().getColor(R.color.alert));
|
||||
}
|
||||
expiryView.setText(getString(expiryString, DateFormat.getDateInstance(DateFormat.LONG).format(loyaltyCard.expiry)));
|
||||
expiryView.setTextSize(settings.getFontSizeMax(settings.getMediumFont()));
|
||||
}
|
||||
else
|
||||
{
|
||||
expiryView.setVisibility(View.GONE);
|
||||
}
|
||||
expiryView.setTag(loyaltyCard.expiry);
|
||||
|
||||
if (!barcodeIsFullscreen) {
|
||||
makeBottomSheetVisibleIfUseful();
|
||||
}
|
||||
|
||||
storeName.setText(loyaltyCard.store);
|
||||
storeName.setTextSize(settings.getCardTitleFontSize());
|
||||
storeName.setTextSize(settings.getFontSizeMax(settings.getLargeFont()));
|
||||
|
||||
int backgroundHeaderColor;
|
||||
if(loyaltyCard.headerColor != null)
|
||||
@@ -208,6 +349,7 @@ public class LoyaltyCardViewActivity extends AppCompatActivity
|
||||
}
|
||||
|
||||
collapsingToolbarLayout.setBackgroundColor(backgroundHeaderColor);
|
||||
appBarLayout.setBackgroundColor(backgroundHeaderColor);
|
||||
|
||||
int textColor;
|
||||
if(Utils.needsDarkForeground(backgroundHeaderColor))
|
||||
@@ -219,6 +361,7 @@ public class LoyaltyCardViewActivity extends AppCompatActivity
|
||||
textColor = Color.WHITE;
|
||||
}
|
||||
storeName.setTextColor(textColor);
|
||||
((Toolbar) findViewById(R.id.toolbar_landscape)).setTitleTextColor(textColor);
|
||||
|
||||
// If the background is very bright, we should use dark icons
|
||||
backgroundNeedsDarkIcons = Utils.needsDarkForeground(backgroundHeaderColor);
|
||||
@@ -241,51 +384,48 @@ public class LoyaltyCardViewActivity extends AppCompatActivity
|
||||
// Set shadow colour of store text so even same color on same color would be readable
|
||||
storeName.setShadowLayer(1, 1, 1, backgroundNeedsDarkIcons ? Color.BLACK : Color.WHITE);
|
||||
|
||||
if(format != null)
|
||||
Boolean isBarcodeSupported = true;
|
||||
|
||||
if (format != null && !BarcodeSelectorActivity.SUPPORTED_BARCODE_TYPES.contains(format.name())) {
|
||||
isBarcodeSupported = false;
|
||||
|
||||
Toast.makeText(this, getString(R.string.unsupportedBarcodeType), Toast.LENGTH_LONG).show();
|
||||
}
|
||||
|
||||
if(format != null && isBarcodeSupported)
|
||||
{
|
||||
findViewById(R.id.barcode).setVisibility(View.VISIBLE);
|
||||
if (!barcodeIsFullscreen) {
|
||||
maximizeButton.setVisibility(View.VISIBLE);
|
||||
}
|
||||
barcodeImage.setVisibility(View.VISIBLE);
|
||||
if(barcodeImage.getHeight() == 0)
|
||||
{
|
||||
Log.d(TAG, "ImageView size is not known known at start, waiting for load");
|
||||
// The size of the ImageView is not yet available as it has not
|
||||
// yet been drawn. Wait for it to be drawn so the size is available.
|
||||
barcodeImage.getViewTreeObserver().addOnGlobalLayoutListener(
|
||||
new ViewTreeObserver.OnGlobalLayoutListener()
|
||||
{
|
||||
@Override
|
||||
public void onGlobalLayout()
|
||||
{
|
||||
barcodeImage.getViewTreeObserver().removeOnGlobalLayoutListener(this);
|
||||
|
||||
Log.d(TAG, "ImageView size now known");
|
||||
new BarcodeImageWriterTask(barcodeImage, cardIdString, format).execute();
|
||||
}
|
||||
});
|
||||
redrawBarcodeAfterResize();
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.d(TAG, "ImageView size known known, creating barcode");
|
||||
new BarcodeImageWriterTask(barcodeImage, cardIdString, format).execute();
|
||||
new BarcodeImageWriterTask(
|
||||
barcodeImage,
|
||||
barcodeIdString != null ? barcodeIdString : cardIdString,
|
||||
format,
|
||||
null,
|
||||
false,
|
||||
null)
|
||||
.execute();
|
||||
}
|
||||
|
||||
// Force redraw fullscreen state
|
||||
setFullscreen(barcodeIsFullscreen);
|
||||
}
|
||||
else
|
||||
{
|
||||
findViewById(R.id.barcode).setVisibility(View.GONE);
|
||||
maximizeButton.setVisibility(View.GONE);
|
||||
barcodeImage.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
FloatingActionButton editButton = findViewById(R.id.fabEdit);
|
||||
editButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
Intent intent = new Intent(getApplicationContext(), LoyaltyCardEditActivity.class);
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putInt("id", loyaltyCardId);
|
||||
bundle.putBoolean("update", true);
|
||||
intent.putExtras(bundle);
|
||||
startActivity(intent);
|
||||
finish();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -297,7 +437,6 @@ public class LoyaltyCardViewActivity extends AppCompatActivity
|
||||
}
|
||||
|
||||
super.onBackPressed();
|
||||
return;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -349,7 +488,7 @@ public class LoyaltyCardViewActivity extends AppCompatActivity
|
||||
break;
|
||||
|
||||
case R.id.action_share:
|
||||
importURIHelper.startShareIntent(loyaltyCard);
|
||||
importURIHelper.startShareIntent(Arrays.asList(loyaltyCard));
|
||||
return true;
|
||||
|
||||
case R.id.action_lock_unlock:
|
||||
@@ -374,7 +513,40 @@ public class LoyaltyCardViewActivity extends AppCompatActivity
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
private void setupOrientation()
|
||||
{
|
||||
Toolbar portraitToolbar = findViewById(R.id.toolbar);
|
||||
Toolbar landscapeToolbar = findViewById(R.id.toolbar_landscape);
|
||||
|
||||
int orientation = getResources().getConfiguration().orientation;
|
||||
if (orientation == Configuration.ORIENTATION_LANDSCAPE) {
|
||||
Log.d(TAG, "Detected landscape mode");
|
||||
|
||||
setTitle(loyaltyCard.store);
|
||||
|
||||
collapsingToolbarLayout.setVisibility(View.GONE);
|
||||
portraitToolbar.setVisibility(View.GONE);
|
||||
landscapeToolbar.setVisibility(View.VISIBLE);
|
||||
|
||||
setSupportActionBar(landscapeToolbar);
|
||||
} else {
|
||||
Log.d(TAG, "Detected portrait mode");
|
||||
|
||||
setTitle("");
|
||||
|
||||
collapsingToolbarLayout.setVisibility(View.VISIBLE);
|
||||
portraitToolbar.setVisibility(View.VISIBLE);
|
||||
landscapeToolbar.setVisibility(View.GONE);
|
||||
|
||||
setSupportActionBar(portraitToolbar);
|
||||
}
|
||||
|
||||
ActionBar actionBar = getSupportActionBar();
|
||||
if(actionBar != null)
|
||||
{
|
||||
actionBar.setDisplayHomeAsUpEnabled(true);
|
||||
}
|
||||
}
|
||||
private void setOrientatonLock(MenuItem item, boolean lock)
|
||||
{
|
||||
if(lock)
|
||||
@@ -392,6 +564,40 @@ public class LoyaltyCardViewActivity extends AppCompatActivity
|
||||
}
|
||||
}
|
||||
|
||||
private void makeBottomSheetVisibleIfUseful()
|
||||
{
|
||||
if (noteView.getVisibility() == View.VISIBLE || groupsView.getVisibility() == View.VISIBLE || balanceView.getVisibility() == View.VISIBLE || expiryView.getVisibility() == View.VISIBLE) {
|
||||
bottomSheet.setVisibility(View.VISIBLE);
|
||||
}
|
||||
else
|
||||
{
|
||||
bottomSheet.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
private void redrawBarcodeAfterResize()
|
||||
{
|
||||
if (format != null) {
|
||||
barcodeImage.getViewTreeObserver().addOnGlobalLayoutListener(
|
||||
new ViewTreeObserver.OnGlobalLayoutListener() {
|
||||
@Override
|
||||
public void onGlobalLayout() {
|
||||
barcodeImage.getViewTreeObserver().removeOnGlobalLayoutListener(this);
|
||||
|
||||
Log.d(TAG, "ImageView size now known");
|
||||
new BarcodeImageWriterTask(
|
||||
barcodeImage,
|
||||
barcodeIdString != null ? barcodeIdString : cardIdString,
|
||||
format,
|
||||
null,
|
||||
false,
|
||||
null)
|
||||
.execute();
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* When enabled, hides the status bar and moves the barcode to the top of the screen.
|
||||
*
|
||||
@@ -401,10 +607,16 @@ public class LoyaltyCardViewActivity extends AppCompatActivity
|
||||
private void setFullscreen(boolean enable)
|
||||
{
|
||||
ActionBar actionBar = getSupportActionBar();
|
||||
if(enable && !barcodeIsFullscreen)
|
||||
if(enable)
|
||||
{
|
||||
// Save previous barcodeImage state
|
||||
barcodeImageState = barcodeImage.getLayoutParams();
|
||||
Log.d(TAG, "Move into of fullscreen");
|
||||
// Prepare redraw after size change
|
||||
redrawBarcodeAfterResize();
|
||||
|
||||
// Hide maximize and show minimize button and scaler
|
||||
maximizeButton.setVisibility(View.GONE);
|
||||
minimizeButton.setVisibility(View.VISIBLE);
|
||||
barcodeScaler.setVisibility(View.VISIBLE);
|
||||
|
||||
// Hide actionbar
|
||||
if(actionBar != null)
|
||||
@@ -412,55 +624,73 @@ public class LoyaltyCardViewActivity extends AppCompatActivity
|
||||
actionBar.hide();
|
||||
}
|
||||
|
||||
// Hide collapsingToolbar
|
||||
// Hide toolbars
|
||||
//
|
||||
// Appbar needs to be invisible and have padding removed
|
||||
// Or the barcode will be centered instead of on top of the screen
|
||||
// Don't ask me why...
|
||||
appBarLayout.setVisibility(View.INVISIBLE);
|
||||
appBarLayout.setPadding(0, 0, 0, 0);
|
||||
collapsingToolbarLayout.setVisibility(View.GONE);
|
||||
findViewById(R.id.toolbar_landscape).setVisibility(View.GONE);
|
||||
|
||||
// Hide other UI elements
|
||||
cardIdFieldView.setVisibility(View.GONE);
|
||||
bottomSheet.setVisibility(View.GONE);
|
||||
behavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
|
||||
editButton.hide();
|
||||
|
||||
// Set Android to fullscreen mode
|
||||
getWindow().getDecorView().setSystemUiVisibility(
|
||||
getWindow().getDecorView().getSystemUiVisibility()
|
||||
| View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
|
||||
| View.SYSTEM_UI_FLAG_FULLSCREEN
|
||||
getWindow().getDecorView().getSystemUiVisibility()
|
||||
| View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
|
||||
| View.SYSTEM_UI_FLAG_FULLSCREEN
|
||||
);
|
||||
|
||||
// Make barcode take all space
|
||||
barcodeImage.setLayoutParams(new ConstraintLayout.LayoutParams(
|
||||
ConstraintLayout.LayoutParams.MATCH_PARENT,
|
||||
ConstraintLayout.LayoutParams.MATCH_PARENT
|
||||
));
|
||||
|
||||
// Move barcode to top
|
||||
barcodeImage.setScaleType(ImageView.ScaleType.FIT_START);
|
||||
|
||||
// Prevent centering
|
||||
barcodeImage.setAdjustViewBounds(false);
|
||||
|
||||
// Set current state
|
||||
barcodeIsFullscreen = true;
|
||||
}
|
||||
else if(!enable && barcodeIsFullscreen)
|
||||
else if(!enable)
|
||||
{
|
||||
Log.d(TAG, "Move out of fullscreen");
|
||||
|
||||
// Reset center guideline
|
||||
barcodeScaler.setProgress(100);
|
||||
|
||||
// Prepare redraw after size change
|
||||
redrawBarcodeAfterResize();
|
||||
|
||||
// Show maximize and hide minimize button and scaler
|
||||
maximizeButton.setVisibility(View.VISIBLE);
|
||||
minimizeButton.setVisibility(View.GONE);
|
||||
barcodeScaler.setVisibility(View.GONE);
|
||||
|
||||
// Show actionbar
|
||||
if(actionBar != null)
|
||||
{
|
||||
actionBar.show();
|
||||
}
|
||||
|
||||
// Show collapsingToolbar
|
||||
collapsingToolbarLayout.setVisibility(View.VISIBLE);
|
||||
// Show appropriate toolbar
|
||||
// And restore 24dp paddingTop for appBarLayout
|
||||
appBarLayout.setVisibility(View.VISIBLE);
|
||||
DisplayMetrics metrics = new DisplayMetrics();
|
||||
getWindowManager().getDefaultDisplay().getMetrics(metrics);
|
||||
appBarLayout.setPadding(0, (int) Math.ceil(metrics.density * 24), 0, 0);
|
||||
setupOrientation();
|
||||
|
||||
// Show other UI elements
|
||||
cardIdFieldView.setVisibility(View.VISIBLE);
|
||||
makeBottomSheetVisibleIfUseful();
|
||||
editButton.show();
|
||||
|
||||
// Unset fullscreen mode
|
||||
getWindow().getDecorView().setSystemUiVisibility(
|
||||
getWindow().getDecorView().getSystemUiVisibility()
|
||||
& ~View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
|
||||
& ~View.SYSTEM_UI_FLAG_FULLSCREEN
|
||||
getWindow().getDecorView().getSystemUiVisibility()
|
||||
& ~View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
|
||||
& ~View.SYSTEM_UI_FLAG_FULLSCREEN
|
||||
);
|
||||
|
||||
// Turn barcode back to normal
|
||||
barcodeImage.setLayoutParams(barcodeImageState);
|
||||
|
||||
// Fix barcode centering
|
||||
barcodeImage.setAdjustViewBounds(true);
|
||||
|
||||
// Set current state
|
||||
barcodeIsFullscreen = false;
|
||||
}
|
||||
|
||||
@@ -4,66 +4,154 @@ import android.app.SearchManager;
|
||||
import android.content.ClipData;
|
||||
import android.content.ClipboardManager;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.pm.PackageInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.res.Configuration;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.widget.SearchView;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import android.util.Log;
|
||||
import android.view.ContextMenu;
|
||||
import android.view.GestureDetector;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.webkit.WebView;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.ListView;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.google.android.material.floatingactionbutton.FloatingActionButton;
|
||||
import com.google.android.material.tabs.TabLayout;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
|
||||
import java.util.Calendar;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.view.ActionMode;
|
||||
import androidx.appcompat.widget.SearchView;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.recyclerview.widget.DefaultItemAnimator;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import protect.card_locker.preferences.SettingsActivity;
|
||||
|
||||
public class MainActivity extends AppCompatActivity implements GestureDetector.OnGestureListener
|
||||
public class MainActivity extends AppCompatActivity implements LoyaltyCardCursorAdapter.CardAdapterListener, GestureDetector.OnGestureListener
|
||||
{
|
||||
private static final String TAG = "Catima";
|
||||
private static final int MAIN_REQUEST_CODE = 1;
|
||||
|
||||
private Menu menu;
|
||||
private GestureDetector gestureDetector;
|
||||
protected String filter = "";
|
||||
private final DBHelper mDB = new DBHelper(this);
|
||||
private LoyaltyCardCursorAdapter mAdapter;
|
||||
private ActionMode mCurrentActionMode;
|
||||
private Menu mMenu;
|
||||
private GestureDetector mGestureDetector;
|
||||
protected String mFilter = "";
|
||||
protected int selectedTab = 0;
|
||||
private RecyclerView mCardList;
|
||||
|
||||
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_copy_to_clipboard)
|
||||
{
|
||||
ClipboardManager clipboard = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE);
|
||||
|
||||
String clipboardData;
|
||||
int cardCount = mAdapter.getSelectedItemCount();
|
||||
|
||||
if (cardCount == 1) {
|
||||
clipboardData = mAdapter.getSelectedItems().get(0).cardId;
|
||||
} else {
|
||||
StringBuilder cardIds = new StringBuilder();
|
||||
|
||||
for (int i = 0; i < cardCount; i++) {
|
||||
LoyaltyCard loyaltyCard = mAdapter.getSelectedItems().get(i);
|
||||
|
||||
cardIds.append(loyaltyCard.store + ": " + loyaltyCard.cardId);
|
||||
if (i < (cardCount - 1)) {
|
||||
cardIds.append("\n");
|
||||
}
|
||||
}
|
||||
|
||||
clipboardData = cardIds.toString();
|
||||
}
|
||||
|
||||
ClipData clip = ClipData.newPlainText(getString(R.string.card_ids_copied), clipboardData);
|
||||
clipboard.setPrimaryClip(clip);
|
||||
Toast.makeText(MainActivity.this, cardCount > 1 ? R.string.copy_to_clipboard_multiple_toast : R.string.copy_to_clipboard_toast, Toast.LENGTH_LONG).show();
|
||||
inputMode.finish();
|
||||
return true;
|
||||
}
|
||||
else if (inputItem.getItemId() == R.id.action_share)
|
||||
{
|
||||
final ImportURIHelper importURIHelper = new ImportURIHelper(MainActivity.this);
|
||||
importURIHelper.startShareIntent(mAdapter.getSelectedItems());
|
||||
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("id", mAdapter.getSelectedItems().get(0).id);
|
||||
bundle.putBoolean("update", true);
|
||||
intent.putExtras(bundle);
|
||||
startActivity(intent);
|
||||
inputMode.finish();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyActionMode(ActionMode inputMode)
|
||||
{
|
||||
mAdapter.clearSelections();
|
||||
mCurrentActionMode = null;
|
||||
mCardList.post(new Runnable()
|
||||
{
|
||||
@Override
|
||||
public void run()
|
||||
{
|
||||
mAdapter.resetAnimationIndex();
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState)
|
||||
protected void onCreate(Bundle inputSavedInstanceState)
|
||||
{
|
||||
super.onCreate(savedInstanceState);
|
||||
super.onCreate(inputSavedInstanceState);
|
||||
setContentView(R.layout.main_activity);
|
||||
Toolbar toolbar = findViewById(R.id.toolbar);
|
||||
setSupportActionBar(toolbar);
|
||||
|
||||
updateLoyaltyCardList(filter, null);
|
||||
updateLoyaltyCardList(mFilter, null);
|
||||
|
||||
TabLayout groupsTabLayout = findViewById(R.id.groups);
|
||||
groupsTabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
|
||||
@Override
|
||||
public void onTabSelected(TabLayout.Tab tab) {
|
||||
selectedTab = tab.getPosition();
|
||||
updateLoyaltyCardList(filter, tab.getTag());
|
||||
updateLoyaltyCardList(mFilter, tab.getTag());
|
||||
|
||||
// Store active tab in Shared Preference to restore next app launch
|
||||
SharedPreferences activeTabPref = getApplicationContext().getSharedPreferences(
|
||||
@@ -85,12 +173,12 @@ public class MainActivity extends AppCompatActivity implements GestureDetector.O
|
||||
}
|
||||
});
|
||||
|
||||
gestureDetector = new GestureDetector(this, this);
|
||||
mGestureDetector = new GestureDetector(this, this);
|
||||
|
||||
View.OnTouchListener gestureTouchListener = new View.OnTouchListener() {
|
||||
@Override
|
||||
public boolean onTouch(final View v, final MotionEvent event){
|
||||
return gestureDetector.onTouchEvent(event);
|
||||
return mGestureDetector.onTouchEvent(event);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -101,18 +189,57 @@ public class MainActivity extends AppCompatActivity implements GestureDetector.O
|
||||
helpText.setOnTouchListener(gestureTouchListener);
|
||||
noMatchingCardsText.setOnTouchListener(gestureTouchListener);
|
||||
list.setOnTouchListener(gestureTouchListener);
|
||||
|
||||
/*
|
||||
* This was added for Huawei, but Huawei is just too much of a fucking pain.
|
||||
* Just leaving this commented out if needed for the future idk
|
||||
* https://twitter.com/SylvieLorxu/status/1379437902741012483
|
||||
*
|
||||
|
||||
// Show privacy policy on first run
|
||||
SharedPreferences privacyPolicyShownPref = getApplicationContext().getSharedPreferences(
|
||||
getString(R.string.sharedpreference_privacy_policy_shown),
|
||||
Context.MODE_PRIVATE);
|
||||
|
||||
|
||||
if (privacyPolicyShownPref.getInt(getString(R.string.sharedpreference_privacy_policy_shown), 0) == 0) {
|
||||
SharedPreferences.Editor privacyPolicyShownPrefEditor = privacyPolicyShownPref.edit();
|
||||
privacyPolicyShownPrefEditor.putInt(getString(R.string.sharedpreference_privacy_policy_shown), 1);
|
||||
privacyPolicyShownPrefEditor.apply();
|
||||
|
||||
new AlertDialog.Builder(this)
|
||||
.setTitle(R.string.privacy_policy)
|
||||
.setMessage(R.string.privacy_policy_popup_text)
|
||||
.setPositiveButton(R.string.accept, null)
|
||||
.setNegativeButton(R.string.privacy_policy, new DialogInterface.OnClickListener() {
|
||||
public void onClick(DialogInterface dialog, int whichButton) {
|
||||
openPrivacyPolicy();
|
||||
}
|
||||
})
|
||||
.setIcon(android.R.drawable.ic_dialog_info)
|
||||
.show();
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
protected void onResume()
|
||||
{
|
||||
super.onResume();
|
||||
|
||||
if (menu != null)
|
||||
if(mCurrentActionMode != null)
|
||||
{
|
||||
SearchView searchView = (SearchView) menu.findItem(R.id.action_search).getActionView();
|
||||
mAdapter.clearSelections();
|
||||
mCurrentActionMode.finish();
|
||||
}
|
||||
|
||||
if (!searchView.isIconified()) {
|
||||
filter = searchView.getQuery().toString();
|
||||
if (mMenu != null)
|
||||
{
|
||||
SearchView searchView = (SearchView) mMenu.findItem(R.id.action_search).getActionView();
|
||||
|
||||
if (!searchView.isIconified())
|
||||
{
|
||||
mFilter = searchView.getQuery().toString();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -129,59 +256,72 @@ public class MainActivity extends AppCompatActivity implements GestureDetector.O
|
||||
Object group = null;
|
||||
|
||||
if (groupsTabLayout.getTabCount() != 0) {
|
||||
TabLayout.Tab tab = groupsTabLayout.getTabAt(0);
|
||||
|
||||
if (selectedTab < groupsTabLayout.getTabCount()) {
|
||||
tab = groupsTabLayout.getTabAt(selectedTab);
|
||||
TabLayout.Tab tab = groupsTabLayout.getTabAt(selectedTab);
|
||||
if (tab == null) {
|
||||
tab = groupsTabLayout.getTabAt(0);
|
||||
}
|
||||
|
||||
groupsTabLayout.selectTab(tab);
|
||||
assert tab != null;
|
||||
group = tab.getTag();
|
||||
}
|
||||
updateLoyaltyCardList(filter, group);
|
||||
updateLoyaltyCardList(mFilter, group);
|
||||
// End of active tab logic
|
||||
|
||||
FloatingActionButton addButton = findViewById(R.id.fabAdd);
|
||||
addButton.setOnClickListener(new View.OnClickListener() {
|
||||
addButton.setOnClickListener(new View.OnClickListener()
|
||||
{
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
Intent i = new Intent(getApplicationContext(), LoyaltyCardEditActivity.class);
|
||||
startActivityForResult(i, MAIN_REQUEST_CODE);
|
||||
Intent i = new Intent(getApplicationContext(), ScanActivity.class);
|
||||
startActivityForResult(i, Utils.BARCODE_SCAN);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
protected void onActivityResult(int requestCode, int resultCode, Intent intent) {
|
||||
super.onActivityResult(requestCode, resultCode, intent);
|
||||
|
||||
if (requestCode == MAIN_REQUEST_CODE)
|
||||
{
|
||||
if (requestCode == Utils.MAIN_REQUEST) {
|
||||
// We're coming back from another view so clear the search
|
||||
// We only do this now to prevent a flash of all entries right after picking one
|
||||
filter = "";
|
||||
if (menu != null)
|
||||
mFilter = "";
|
||||
if (mMenu != null)
|
||||
{
|
||||
MenuItem searchItem = menu.findItem(R.id.action_search);
|
||||
MenuItem searchItem = mMenu.findItem(R.id.action_search);
|
||||
searchItem.collapseActionView();
|
||||
}
|
||||
|
||||
// In case the theme changed
|
||||
recreate();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
BarcodeValues barcodeValues = Utils.parseSetBarcodeActivityResult(requestCode, resultCode, intent, this);
|
||||
|
||||
if(!barcodeValues.isEmpty()) {
|
||||
Intent newIntent = new Intent(getApplicationContext(), LoyaltyCardEditActivity.class);
|
||||
Bundle newBundle = new Bundle();
|
||||
newBundle.putString("barcodeType", barcodeValues.format());
|
||||
newBundle.putString("cardId", barcodeValues.content());
|
||||
newIntent.putExtras(newBundle);
|
||||
startActivity(newIntent);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
if (menu == null)
|
||||
public void onBackPressed()
|
||||
{
|
||||
if (mMenu == null)
|
||||
{
|
||||
super.onBackPressed();
|
||||
return;
|
||||
}
|
||||
|
||||
SearchView searchView = (SearchView) menu.findItem(R.id.action_search).getActionView();
|
||||
SearchView searchView = (SearchView) mMenu.findItem(R.id.action_search).getActionView();
|
||||
|
||||
if (!searchView.isIconified()) {
|
||||
if (!searchView.isIconified())
|
||||
{
|
||||
searchView.setIconified(true);
|
||||
} else {
|
||||
TabLayout groupsTabLayout = findViewById(R.id.groups);
|
||||
@@ -202,21 +342,29 @@ public class MainActivity extends AppCompatActivity implements GestureDetector.O
|
||||
group = (Group) tag;
|
||||
}
|
||||
|
||||
final ListView cardList = findViewById(R.id.list);
|
||||
mCardList = findViewById(R.id.list);
|
||||
RecyclerView.LayoutManager mLayoutManager = new LinearLayoutManager(getApplicationContext());
|
||||
mCardList.setLayoutManager(mLayoutManager);
|
||||
mCardList.setItemAnimator(new DefaultItemAnimator());
|
||||
|
||||
final TextView helpText = findViewById(R.id.helpText);
|
||||
final TextView noMatchingCardsText = findViewById(R.id.noMatchingCardsText);
|
||||
final DBHelper db = new DBHelper(this);
|
||||
|
||||
Cursor cardCursor = db.getLoyaltyCardCursor(filterText, group);
|
||||
Cursor cardCursor = mDB.getLoyaltyCardCursor(filterText, group);
|
||||
|
||||
if(db.getLoyaltyCardCount() > 0)
|
||||
mAdapter = new LoyaltyCardCursorAdapter(this, cardCursor, this);
|
||||
mCardList.setAdapter(mAdapter);
|
||||
|
||||
registerForContextMenu(mCardList);
|
||||
|
||||
if(mDB.getLoyaltyCardCount() > 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
|
||||
cardList.setVisibility(View.VISIBLE);
|
||||
mCardList.setVisibility(View.VISIBLE);
|
||||
helpText.setVisibility(View.GONE);
|
||||
if(cardCursor.getCount() > 0)
|
||||
if(mAdapter.getItemCount() > 0)
|
||||
{
|
||||
noMatchingCardsText.setVisibility(View.GONE);
|
||||
}
|
||||
@@ -227,35 +375,14 @@ public class MainActivity extends AppCompatActivity implements GestureDetector.O
|
||||
}
|
||||
else
|
||||
{
|
||||
cardList.setVisibility(View.GONE);
|
||||
mCardList.setVisibility(View.GONE);
|
||||
helpText.setVisibility(View.VISIBLE);
|
||||
noMatchingCardsText.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
final LoyaltyCardCursorAdapter adapter = new LoyaltyCardCursorAdapter(this, cardCursor);
|
||||
cardList.setAdapter(adapter);
|
||||
|
||||
registerForContextMenu(cardList);
|
||||
|
||||
cardList.setOnItemClickListener(new AdapterView.OnItemClickListener()
|
||||
{
|
||||
@Override
|
||||
public void onItemClick(AdapterView<?> parent, View view, int position, long id)
|
||||
{
|
||||
Cursor selected = (Cursor) parent.getItemAtPosition(position);
|
||||
LoyaltyCard loyaltyCard = LoyaltyCard.toLoyaltyCard(selected);
|
||||
|
||||
Intent i = new Intent(view.getContext(), LoyaltyCardViewActivity.class);
|
||||
i.setAction("");
|
||||
final Bundle b = new Bundle();
|
||||
b.putInt("id", loyaltyCard.id);
|
||||
i.putExtras(b);
|
||||
|
||||
ShortcutHelper.updateShortcuts(MainActivity.this, loyaltyCard, i);
|
||||
|
||||
startActivityForResult(i, MAIN_REQUEST_CODE);
|
||||
}
|
||||
});
|
||||
if (mCurrentActionMode != null) {
|
||||
mCurrentActionMode.finish();
|
||||
}
|
||||
}
|
||||
|
||||
public void updateTabGroups(TabLayout groupsTabLayout)
|
||||
@@ -287,216 +414,106 @@ public class MainActivity extends AppCompatActivity implements GestureDetector.O
|
||||
groupsTabLayout.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo)
|
||||
{
|
||||
super.onCreateContextMenu(menu, v, menuInfo);
|
||||
if (v.getId()==R.id.list)
|
||||
{
|
||||
MenuInflater inflater = getMenuInflater();
|
||||
inflater.inflate(R.menu.card_longclick_menu, menu);
|
||||
}
|
||||
private void openPrivacyPolicy() {
|
||||
Intent browserIntent = new Intent(
|
||||
Intent.ACTION_VIEW,
|
||||
Uri.parse("https://thelastproject.github.io/Catima/privacy-policy")
|
||||
);
|
||||
startActivity(browserIntent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onContextItemSelected(MenuItem item)
|
||||
public boolean onCreateOptionsMenu(Menu inputMenu)
|
||||
{
|
||||
AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) item.getMenuInfo();
|
||||
ListView listView = findViewById(R.id.list);
|
||||
this.mMenu = inputMenu;
|
||||
|
||||
Cursor cardCursor = (Cursor)listView.getItemAtPosition(info.position);
|
||||
LoyaltyCard card = LoyaltyCard.toLoyaltyCard(cardCursor);
|
||||
|
||||
if(item.getItemId() == R.id.action_clipboard)
|
||||
{
|
||||
ClipboardManager clipboard = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE);
|
||||
ClipData clip = ClipData.newPlainText(card.store, card.cardId);
|
||||
clipboard.setPrimaryClip(clip);
|
||||
|
||||
Toast.makeText(this, R.string.copy_to_clipboard_toast, Toast.LENGTH_LONG).show();
|
||||
return true;
|
||||
}
|
||||
else if(item.getItemId() == R.id.action_share)
|
||||
{
|
||||
final ImportURIHelper importURIHelper = new ImportURIHelper(this);
|
||||
importURIHelper.startShareIntent(card);
|
||||
return true;
|
||||
}
|
||||
|
||||
return super.onContextItemSelected(item);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu)
|
||||
{
|
||||
this.menu = menu;
|
||||
|
||||
getMenuInflater().inflate(R.menu.main_menu, menu);
|
||||
getMenuInflater().inflate(R.menu.main_menu, inputMenu);
|
||||
|
||||
SearchManager searchManager = (SearchManager) getSystemService(Context.SEARCH_SERVICE);
|
||||
if (searchManager != null) {
|
||||
SearchView searchView = (SearchView) menu.findItem(R.id.action_search).getActionView();
|
||||
if (searchManager != null)
|
||||
{
|
||||
SearchView searchView = (SearchView) inputMenu.findItem(R.id.action_search).getActionView();
|
||||
searchView.setSearchableInfo(searchManager.getSearchableInfo(getComponentName()));
|
||||
searchView.setSubmitButtonEnabled(false);
|
||||
|
||||
searchView.setOnCloseListener(new SearchView.OnCloseListener() {
|
||||
searchView.setOnCloseListener(new SearchView.OnCloseListener()
|
||||
{
|
||||
@Override
|
||||
public boolean onClose() {
|
||||
public boolean onClose()
|
||||
{
|
||||
invalidateOptionsMenu();
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
|
||||
searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener()
|
||||
{
|
||||
@Override
|
||||
public boolean onQueryTextSubmit(String query) {
|
||||
public boolean onQueryTextSubmit(String query)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onQueryTextChange(String newText) {
|
||||
filter = newText;
|
||||
public boolean onQueryTextChange(String newText)
|
||||
{
|
||||
mFilter = newText;
|
||||
|
||||
TabLayout groupsTabLayout = findViewById(R.id.groups);
|
||||
TabLayout.Tab currentTab = groupsTabLayout.getTabAt(groupsTabLayout.getSelectedTabPosition());
|
||||
|
||||
updateLoyaltyCardList(
|
||||
mFilter,
|
||||
currentTab != null ? currentTab.getTag() : null
|
||||
);
|
||||
|
||||
updateLoyaltyCardList(newText, groupsTabLayout.getTabCount() > 0 ? groupsTabLayout.getTabAt(groupsTabLayout.getSelectedTabPosition()).getTag() : null);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return super.onCreateOptionsMenu(menu);
|
||||
return super.onCreateOptionsMenu(inputMenu);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item)
|
||||
public boolean onOptionsItemSelected(MenuItem inputItem)
|
||||
{
|
||||
int id = item.getItemId();
|
||||
int id = inputItem.getItemId();
|
||||
|
||||
if (id == R.id.action_manage_groups)
|
||||
{
|
||||
Intent i = new Intent(getApplicationContext(), ManageGroupsActivity.class);
|
||||
startActivityForResult(i, MAIN_REQUEST_CODE);
|
||||
startActivityForResult(i, Utils.MAIN_REQUEST);
|
||||
return true;
|
||||
}
|
||||
|
||||
if(id == R.id.action_import_export)
|
||||
if (id == R.id.action_import_export)
|
||||
{
|
||||
Intent i = new Intent(getApplicationContext(), ImportExportActivity.class);
|
||||
startActivityForResult(i, MAIN_REQUEST_CODE);
|
||||
startActivityForResult(i, Utils.MAIN_REQUEST);
|
||||
return true;
|
||||
}
|
||||
|
||||
if(id == R.id.action_settings)
|
||||
if (id == R.id.action_settings)
|
||||
{
|
||||
Intent i = new Intent(getApplicationContext(), SettingsActivity.class);
|
||||
startActivityForResult(i, MAIN_REQUEST_CODE);
|
||||
startActivityForResult(i, Utils.MAIN_REQUEST);
|
||||
return true;
|
||||
}
|
||||
|
||||
if(id == R.id.action_about)
|
||||
if(id == R.id.action_privacy_policy)
|
||||
{
|
||||
displayAboutDialog();
|
||||
openPrivacyPolicy();
|
||||
return true;
|
||||
}
|
||||
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
private void displayAboutDialog()
|
||||
{
|
||||
final Map<String, String> USED_LIBRARIES = new ImmutableMap.Builder<String, String>()
|
||||
.put("Commons CSV", "https://commons.apache.org/proper/commons-csv/")
|
||||
.put("Guava", "https://github.com/google/guava")
|
||||
.put("ZXing", "https://github.com/zxing/zxing")
|
||||
.put("ZXing Android Embedded", "https://github.com/journeyapps/zxing-android-embedded")
|
||||
.put("AppIntro", "https://github.com/apl-devs/AppIntro")
|
||||
.put("Color Picker", "https://github.com/jaredrummler/ColorPicker")
|
||||
.put("VNTNumberPickerPreference", "https://github.com/vanniktech/VNTNumberPickerPreference")
|
||||
.build();
|
||||
|
||||
final Map<String, String> USED_ASSETS = ImmutableMap.of
|
||||
(
|
||||
"Save by Bernar Novalyi", "https://thenounproject.com/term/save/716011"
|
||||
);
|
||||
|
||||
StringBuilder libs = new StringBuilder().append("<ul>");
|
||||
for (Map.Entry<String, String> entry : USED_LIBRARIES.entrySet())
|
||||
if (id == R.id.action_about)
|
||||
{
|
||||
libs.append("<li><a href=\"").append(entry.getValue()).append("\">").append(entry.getKey()).append("</a></li>");
|
||||
}
|
||||
libs.append("</ul>");
|
||||
|
||||
StringBuilder resources = new StringBuilder().append("<ul>");
|
||||
for (Map.Entry<String, String> entry : USED_ASSETS.entrySet())
|
||||
{
|
||||
resources.append("<li><a href=\"").append(entry.getValue()).append("\">").append(entry.getKey()).append("</a></li>");
|
||||
}
|
||||
resources.append("</ul>");
|
||||
|
||||
String appName = getString(R.string.app_name);
|
||||
int year = Calendar.getInstance().get(Calendar.YEAR);
|
||||
|
||||
String version = "?";
|
||||
try
|
||||
{
|
||||
PackageInfo pi = getPackageManager().getPackageInfo(getPackageName(), 0);
|
||||
version = pi.versionName;
|
||||
}
|
||||
catch (PackageManager.NameNotFoundException e)
|
||||
{
|
||||
Log.w(TAG, "Package name not found", e);
|
||||
Intent i = new Intent(getApplicationContext(), AboutActivity.class);
|
||||
startActivityForResult(i, Utils.MAIN_REQUEST);
|
||||
return true;
|
||||
}
|
||||
|
||||
WebView wv = new WebView(this);
|
||||
|
||||
// Set CSS for dark mode if dark mode
|
||||
String css = "";
|
||||
if(isDarkModeEnabled(this))
|
||||
{
|
||||
css = "<style>body {color:white; background-color:black;}</style>";
|
||||
}
|
||||
|
||||
String html =
|
||||
"<meta http-equiv=\"content-type\" content=\"text/html; charset=utf-8\" />" +
|
||||
css +
|
||||
"<h1>" +
|
||||
String.format(getString(R.string.about_title_fmt),
|
||||
"<a href=\"" + getString(R.string.app_webpage_url)) + "\">" +
|
||||
appName +
|
||||
"</a>" +
|
||||
"</h1><p>" +
|
||||
appName +
|
||||
" " +
|
||||
String.format(getString(R.string.debug_version_fmt), version) +
|
||||
"</p><p>" +
|
||||
String.format(getString(R.string.app_revision_fmt),
|
||||
"<a href=\"" + getString(R.string.app_revision_url) + "\">" +
|
||||
"GitHub" +
|
||||
"</a>") +
|
||||
"</p><hr/><p>" +
|
||||
String.format(getString(R.string.app_copyright_fmt), year) +
|
||||
"</p><p>" +
|
||||
getString(R.string.app_copyright_old) +
|
||||
"</p><hr/><p>" +
|
||||
getString(R.string.app_license) +
|
||||
"</p><hr/><p>" +
|
||||
String.format(getString(R.string.app_libraries), appName, libs.toString()) +
|
||||
"</p><hr/><p>" +
|
||||
String.format(getString(R.string.app_resources), appName, resources.toString());
|
||||
|
||||
wv.loadDataWithBaseURL("file:///android_res/drawable/", html, "text/html", "utf-8", null);
|
||||
new AlertDialog.Builder(this)
|
||||
.setView(wv)
|
||||
.setCancelable(true)
|
||||
.setPositiveButton(R.string.ok, new DialogInterface.OnClickListener()
|
||||
{
|
||||
public void onClick(DialogInterface dialog, int which)
|
||||
{
|
||||
dialog.dismiss();
|
||||
}
|
||||
})
|
||||
.show();
|
||||
return super.onOptionsItemSelected(inputItem);
|
||||
}
|
||||
|
||||
protected static boolean isDarkModeEnabled(Context inputContext)
|
||||
@@ -575,4 +592,79 @@ public class MainActivity extends AppCompatActivity implements GestureDetector.O
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRowLongClicked(int inputPosition)
|
||||
{
|
||||
enableActionMode(inputPosition);
|
||||
}
|
||||
|
||||
private void enableActionMode(int inputPosition)
|
||||
{
|
||||
if (mCurrentActionMode == null)
|
||||
{
|
||||
mCurrentActionMode = startSupportActionMode(mCurrentActionModeCallback);
|
||||
}
|
||||
toggleSelection(inputPosition);
|
||||
}
|
||||
|
||||
private void toggleSelection(int inputPosition)
|
||||
{
|
||||
mAdapter.toggleSelection(inputPosition);
|
||||
int count = mAdapter.getSelectedItemCount();
|
||||
|
||||
if (count == 0) {
|
||||
mCurrentActionMode.finish();
|
||||
} else {
|
||||
mCurrentActionMode.setTitle("Selected: " + count + " Cards");
|
||||
|
||||
MenuItem editItem = mCurrentActionMode.getMenu().findItem(R.id.action_edit);
|
||||
if (count == 1) {
|
||||
editItem.setVisible(true);
|
||||
editItem.setEnabled(true);
|
||||
} else {
|
||||
editItem.setVisible(false);
|
||||
editItem.setEnabled(false);
|
||||
}
|
||||
|
||||
mCurrentActionMode.invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onIconClicked(int inputPosition)
|
||||
{
|
||||
if (mCurrentActionMode == null)
|
||||
{
|
||||
mCurrentActionMode = startSupportActionMode(mCurrentActionModeCallback);
|
||||
}
|
||||
|
||||
toggleSelection(inputPosition);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRowClicked(int inputPosition)
|
||||
{
|
||||
|
||||
if (mAdapter.getSelectedItemCount() > 0)
|
||||
{
|
||||
enableActionMode(inputPosition);
|
||||
}
|
||||
else
|
||||
{
|
||||
Cursor selected = mAdapter.getCursor();
|
||||
selected.moveToPosition(inputPosition);
|
||||
LoyaltyCard loyaltyCard = LoyaltyCard.toLoyaltyCard(selected);
|
||||
|
||||
Intent i = new Intent(this, LoyaltyCardViewActivity.class);
|
||||
i.setAction("");
|
||||
final Bundle b = new Bundle();
|
||||
b.putInt("id", loyaltyCard.id);
|
||||
i.putExtras(b);
|
||||
|
||||
ShortcutHelper.updateShortcuts(MainActivity.this, loyaltyCard, i);
|
||||
|
||||
startActivityForResult(i, Utils.MAIN_REQUEST);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import android.os.Bundle;
|
||||
import android.text.InputType;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.EditText;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.ListView;
|
||||
@@ -15,6 +16,8 @@ import android.widget.TextView;
|
||||
|
||||
import com.google.android.material.floatingactionbutton.FloatingActionButton;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
@@ -24,7 +27,6 @@ public class ManageGroupsActivity extends AppCompatActivity
|
||||
{
|
||||
private static final String TAG = "Catima";
|
||||
|
||||
private AlertDialog newGroupDialog;
|
||||
private final DBHelper db = new DBHelper(this);
|
||||
|
||||
@Override
|
||||
@@ -40,8 +42,6 @@ public class ManageGroupsActivity extends AppCompatActivity
|
||||
actionBar.setDisplayHomeAsUpEnabled(true);
|
||||
}
|
||||
|
||||
newGroupDialog = createNewGroupDialog();
|
||||
|
||||
updateGroupList();
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ public class ManageGroupsActivity extends AppCompatActivity
|
||||
addButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
newGroupDialog.show();
|
||||
createGroup();
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -112,12 +112,16 @@ public class ManageGroupsActivity extends AppCompatActivity
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
public void moveGroupUp(View view) {
|
||||
moveGroup(view, true);
|
||||
}
|
||||
|
||||
public void moveGroupDown(View view) {
|
||||
moveGroup(view, false);
|
||||
}
|
||||
|
||||
public void editGroup(View view) {
|
||||
LinearLayout parentRow = (LinearLayout) view.getParent();
|
||||
|
||||
TextView groupNameTextView = (TextView) parentRow.findViewById(R.id.name);
|
||||
|
||||
final String groupName = (String) groupNameTextView.getText();
|
||||
final String groupName = getGroupname(view);
|
||||
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(this);
|
||||
builder.setTitle(R.string.enter_group_name);
|
||||
@@ -131,8 +135,6 @@ public class ManageGroupsActivity extends AppCompatActivity
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
db.updateGroup(groupName, input.getText().toString());
|
||||
updateGroupList();
|
||||
// Rename may change ordering, so invalidate
|
||||
invalidateHomescreenActiveTab();
|
||||
}
|
||||
});
|
||||
builder.setNegativeButton(getString(R.string.cancel), new DialogInterface.OnClickListener() {
|
||||
@@ -143,14 +145,12 @@ public class ManageGroupsActivity extends AppCompatActivity
|
||||
});
|
||||
AlertDialog dialog = builder.create();
|
||||
dialog.show();
|
||||
dialog.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE);
|
||||
input.requestFocus();
|
||||
}
|
||||
|
||||
public void deleteGroup(View view) {
|
||||
LinearLayout parentRow = (LinearLayout) view.getParent();
|
||||
|
||||
TextView groupNameTextView = (TextView) parentRow.findViewById(R.id.name);
|
||||
|
||||
final String groupName = (String) groupNameTextView.getText();
|
||||
final String groupName = getGroupname(view);
|
||||
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(this);
|
||||
builder.setTitle(R.string.deleteConfirmationGroup);
|
||||
@@ -175,7 +175,7 @@ public class ManageGroupsActivity extends AppCompatActivity
|
||||
dialog.show();
|
||||
}
|
||||
|
||||
private AlertDialog createNewGroupDialog() {
|
||||
private void createGroup() {
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(this);
|
||||
builder.setTitle(R.string.enter_group_name);
|
||||
final EditText input = new EditText(this);
|
||||
@@ -187,8 +187,6 @@ public class ManageGroupsActivity extends AppCompatActivity
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
db.insertGroup(input.getText().toString());
|
||||
updateGroupList();
|
||||
// Create may change ordering, so invalidate
|
||||
invalidateHomescreenActiveTab();
|
||||
}
|
||||
});
|
||||
builder.setNegativeButton(getString(R.string.cancel), new DialogInterface.OnClickListener() {
|
||||
@@ -198,7 +196,60 @@ public class ManageGroupsActivity extends AppCompatActivity
|
||||
}
|
||||
});
|
||||
AlertDialog dialog = builder.create();
|
||||
dialog.show();
|
||||
dialog.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE);
|
||||
input.requestFocus();
|
||||
}
|
||||
|
||||
return dialog;
|
||||
private String getGroupname(View view) {
|
||||
LinearLayout parentRow = (LinearLayout) view.getParent().getParent();
|
||||
TextView groupNameTextView = parentRow.findViewById(R.id.name);
|
||||
return (String) groupNameTextView.getText();
|
||||
}
|
||||
|
||||
private void moveGroup(View view, boolean up) {
|
||||
final String groupName = getGroupname(view);
|
||||
|
||||
List<Group> groups = db.getGroups();
|
||||
|
||||
int currentIndex = -1;
|
||||
Integer newIndex;
|
||||
|
||||
// Get current index in group list
|
||||
for (int i = 0; i < groups.size(); i++) {
|
||||
if (groups.get(i)._id.equals(groupName)) {
|
||||
currentIndex = i;
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (currentIndex == -1) {
|
||||
throw new IndexOutOfBoundsException();
|
||||
}
|
||||
|
||||
// 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
|
||||
db.reorderGroups(groups);
|
||||
|
||||
// Update UI
|
||||
updateGroupList();
|
||||
|
||||
// Ordering may have changed, so invalidate
|
||||
invalidateHomescreenActiveTab();
|
||||
}
|
||||
}
|
||||
163
app/src/main/java/protect/card_locker/ScanActivity.java
Normal file
@@ -0,0 +1,163 @@
|
||||
package protect.card_locker;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
|
||||
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.List;
|
||||
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
|
||||
/**
|
||||
* Custom Scannner Activity extending from Activity to display a custom layout form scanner view.
|
||||
*
|
||||
* 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 AppCompatActivity {
|
||||
private static final String TAG = "Catima";
|
||||
|
||||
private CaptureManager capture;
|
||||
private DecoratedBarcodeView barcodeScannerView;
|
||||
|
||||
private String cardId;
|
||||
|
||||
private void extractIntentFields(Intent intent) {
|
||||
final Bundle b = intent.getExtras();
|
||||
cardId = b != null ? b.getString("cardId") : null;
|
||||
Log.d(TAG, "Scan activity: id=" + cardId);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.scan_activity);
|
||||
Toolbar toolbar = findViewById(R.id.toolbar);
|
||||
setSupportActionBar(toolbar);
|
||||
ActionBar actionBar = getSupportActionBar();
|
||||
if(actionBar != null)
|
||||
{
|
||||
actionBar.setDisplayHomeAsUpEnabled(true);
|
||||
}
|
||||
|
||||
extractIntentFields(getIntent());
|
||||
|
||||
barcodeScannerView = findViewById(R.id.zxing_barcode_scanner);
|
||||
|
||||
// Even though we do the actual decoding with the barcodeScannerView
|
||||
// CaptureManager needs to be running to show the camera and scanning bar
|
||||
capture = new CaptureManager(this, barcodeScannerView);
|
||||
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) {
|
||||
Intent scanResult = new Intent();
|
||||
Bundle scanResultBundle = new Bundle();
|
||||
scanResultBundle.putString(BarcodeSelectorActivity.BARCODE_CONTENTS, result.getText());
|
||||
scanResultBundle.putString(BarcodeSelectorActivity.BARCODE_FORMAT, result.getBarcodeFormat().toString());
|
||||
scanResult.putExtras(scanResultBundle);
|
||||
ScanActivity.this.setResult(RESULT_OK, scanResult);
|
||||
finish();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void possibleResultPoints(List<ResultPoint> resultPoints) {
|
||||
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
capture.onResume();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPause() {
|
||||
super.onPause();
|
||||
capture.onPause();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
capture.onDestroy();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSaveInstanceState(Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
capture.onSaveInstanceState(outState);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onKeyDown(int keyCode, KeyEvent event) {
|
||||
return barcodeScannerView.onKeyDown(keyCode, event) || super.onKeyDown(keyCode, event);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item)
|
||||
{
|
||||
if (item.getItemId() == android.R.id.home)
|
||||
{
|
||||
setResult(Activity.RESULT_CANCELED);
|
||||
finish();
|
||||
return true;
|
||||
}
|
||||
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResult(int requestCode, int resultCode, Intent intent)
|
||||
{
|
||||
super.onActivityResult(requestCode, resultCode, intent);
|
||||
|
||||
BarcodeValues barcodeValues = Utils.parseSetBarcodeActivityResult(requestCode, resultCode, intent, this);
|
||||
|
||||
if (!barcodeValues.isEmpty()) {
|
||||
Intent manualResult = new Intent();
|
||||
Bundle manualResultBundle = new Bundle();
|
||||
manualResultBundle.putString(BarcodeSelectorActivity.BARCODE_CONTENTS, barcodeValues.content());
|
||||
manualResultBundle.putString(BarcodeSelectorActivity.BARCODE_FORMAT, barcodeValues.format());
|
||||
manualResult.putExtras(manualResultBundle);
|
||||
ScanActivity.this.setResult(RESULT_OK, manualResult);
|
||||
finish();
|
||||
}
|
||||
}
|
||||
|
||||
public void addManually(View view) {
|
||||
Intent i = new Intent(getApplicationContext(), BarcodeSelectorActivity.class);
|
||||
if (cardId != null) {
|
||||
final Bundle b = new Bundle();
|
||||
b.putString("initialCardId", cardId);
|
||||
i.putExtras(b);
|
||||
}
|
||||
startActivityForResult(i, Utils.SELECT_BARCODE_REQUEST);
|
||||
}
|
||||
|
||||
public void addFromImage(View view) {
|
||||
Intent photoPickerIntent = new Intent(Intent.ACTION_PICK);
|
||||
photoPickerIntent.setType("image/*");
|
||||
startActivityForResult(photoPickerIntent, Utils.BARCODE_IMPORT_FROM_IMAGE_FILE);
|
||||
}
|
||||
}
|
||||
25
app/src/main/java/protect/card_locker/ThirdPartyInfo.java
Normal file
@@ -0,0 +1,25 @@
|
||||
package protect.card_locker;
|
||||
|
||||
public class ThirdPartyInfo {
|
||||
private final String mName;
|
||||
private final String mUrl;
|
||||
private final String mLicense;
|
||||
|
||||
public ThirdPartyInfo(String name, String url, String license) {
|
||||
mName = name;
|
||||
mUrl = url;
|
||||
mLicense = license;
|
||||
}
|
||||
|
||||
public String name() {
|
||||
return mName;
|
||||
}
|
||||
|
||||
public String url() {
|
||||
return mUrl;
|
||||
}
|
||||
|
||||
public String license() {
|
||||
return mLicense;
|
||||
}
|
||||
}
|
||||
@@ -1,19 +1,59 @@
|
||||
package protect.card_locker;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Color;
|
||||
import android.provider.MediaStore;
|
||||
import android.util.Log;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.google.zxing.BinaryBitmap;
|
||||
import com.google.zxing.LuminanceSource;
|
||||
import com.google.zxing.MultiFormatReader;
|
||||
import com.google.zxing.NotFoundException;
|
||||
import com.google.zxing.RGBLuminanceSource;
|
||||
import com.google.zxing.Result;
|
||||
import com.google.zxing.common.HybridBinarizer;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.math.BigDecimal;
|
||||
import java.text.NumberFormat;
|
||||
import java.util.Calendar;
|
||||
import java.util.Currency;
|
||||
import java.util.Date;
|
||||
import java.util.GregorianCalendar;
|
||||
|
||||
import androidx.core.graphics.ColorUtils;
|
||||
|
||||
public class Utils {
|
||||
private static final String TAG = "Catima";
|
||||
|
||||
// Activity request codes
|
||||
public static final int MAIN_REQUEST = 1;
|
||||
public static final int SELECT_BARCODE_REQUEST = 2;
|
||||
public static final int BARCODE_SCAN = 3;
|
||||
public static final int BARCODE_IMPORT_FROM_IMAGE_FILE = 4;
|
||||
|
||||
static final double LUMINANCE_MIDPOINT = 0.5;
|
||||
|
||||
static public LetterBitmap generateIcon(Context context, String store, Integer backgroundColor) {
|
||||
return generateIcon(context, store, backgroundColor, false);
|
||||
}
|
||||
|
||||
static public LetterBitmap generateIcon(Context context, String store, Integer backgroundColor, boolean forShortcut) {
|
||||
if (store.length() == 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
int tileLetterFontSize = context.getResources().getDimensionPixelSize(R.dimen.tileLetterFontSize);
|
||||
int tileLetterFontSize;
|
||||
if (forShortcut) {
|
||||
tileLetterFontSize = context.getResources().getDimensionPixelSize(R.dimen.tileLetterFontSizeForShortcut);
|
||||
} else {
|
||||
tileLetterFontSize = context.getResources().getDimensionPixelSize(R.dimen.tileLetterFontSize);
|
||||
}
|
||||
|
||||
int pixelSize = context.getResources().getDimensionPixelSize(R.dimen.cardThumbnailSize);
|
||||
|
||||
if (backgroundColor == null) {
|
||||
@@ -27,4 +67,145 @@ public class Utils {
|
||||
static public boolean needsDarkForeground(Integer backgroundColor) {
|
||||
return ColorUtils.calculateLuminance(backgroundColor) > LUMINANCE_MIDPOINT;
|
||||
}
|
||||
|
||||
static public BarcodeValues parseSetBarcodeActivityResult(int requestCode, int resultCode, Intent intent, Context context) {
|
||||
String contents;
|
||||
String format;
|
||||
|
||||
if (resultCode != Activity.RESULT_OK) {
|
||||
return new BarcodeValues(null, null);
|
||||
}
|
||||
|
||||
if (requestCode == Utils.BARCODE_IMPORT_FROM_IMAGE_FILE) {
|
||||
Log.i(TAG, "Received image file with possible barcode");
|
||||
|
||||
Bitmap bitmap;
|
||||
try {
|
||||
bitmap = MediaStore.Images.Media.getBitmap(context.getContentResolver(), intent.getData());
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "Error getting data from image file");
|
||||
e.printStackTrace();
|
||||
Toast.makeText(context, R.string.errorReadingImage, Toast.LENGTH_LONG).show();
|
||||
return new BarcodeValues(null, null);
|
||||
}
|
||||
|
||||
BarcodeValues barcodeFromBitmap = getBarcodeFromBitmap(bitmap);
|
||||
|
||||
if (barcodeFromBitmap.isEmpty()) {
|
||||
Log.i(TAG, "No barcode found in image file");
|
||||
Toast.makeText(context, R.string.noBarcodeFound, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
|
||||
Log.i(TAG, "Read barcode id: " + barcodeFromBitmap.content());
|
||||
Log.i(TAG, "Read format: " + barcodeFromBitmap.format());
|
||||
|
||||
return barcodeFromBitmap;
|
||||
}
|
||||
|
||||
if (requestCode == Utils.BARCODE_SCAN || requestCode == Utils.SELECT_BARCODE_REQUEST) {
|
||||
if (requestCode == Utils.BARCODE_SCAN) {
|
||||
Log.i(TAG, "Received barcode information from camera");
|
||||
} else if (requestCode == Utils.SELECT_BARCODE_REQUEST) {
|
||||
Log.i(TAG, "Received barcode information from typing it");
|
||||
}
|
||||
|
||||
contents = intent.getStringExtra(BarcodeSelectorActivity.BARCODE_CONTENTS);
|
||||
format = intent.getStringExtra(BarcodeSelectorActivity.BARCODE_FORMAT);
|
||||
|
||||
Log.i(TAG, "Read barcode id: " + contents);
|
||||
Log.i(TAG, "Read format: " + format);
|
||||
|
||||
return new BarcodeValues(format, contents);
|
||||
}
|
||||
|
||||
throw new UnsupportedOperationException("Unknown request code for parseSetBarcodeActivityResult");
|
||||
}
|
||||
|
||||
static public BarcodeValues getBarcodeFromBitmap(Bitmap bitmap) {
|
||||
// In order to decode it, the Bitmap must first be converted into a pixel array...
|
||||
int[] intArray = new int[bitmap.getWidth() * bitmap.getHeight()];
|
||||
bitmap.getPixels(intArray, 0, bitmap.getWidth(), 0, 0, bitmap.getWidth(), bitmap.getHeight());
|
||||
|
||||
// ...and then turned into a binary bitmap from its luminance
|
||||
LuminanceSource source = new RGBLuminanceSource(bitmap.getWidth(), bitmap.getHeight(), intArray);
|
||||
BinaryBitmap binaryBitmap = new BinaryBitmap(new HybridBinarizer(source));
|
||||
|
||||
try {
|
||||
Result barcodeResult = new MultiFormatReader().decode(binaryBitmap);
|
||||
|
||||
return new BarcodeValues(barcodeResult.getBarcodeFormat().name(), barcodeResult.getText());
|
||||
} catch (NotFoundException e) {
|
||||
return new BarcodeValues(null, null);
|
||||
}
|
||||
}
|
||||
|
||||
static public Boolean hasExpired(Date expiryDate) {
|
||||
// today
|
||||
Calendar date = new GregorianCalendar();
|
||||
// reset hour, minutes, seconds and millis
|
||||
date.set(Calendar.HOUR_OF_DAY, 0);
|
||||
date.set(Calendar.MINUTE, 0);
|
||||
date.set(Calendar.SECOND, 0);
|
||||
date.set(Calendar.MILLISECOND, 0);
|
||||
|
||||
return expiryDate.before(date.getTime());
|
||||
}
|
||||
|
||||
static public String formatBalance(Context context, BigDecimal value, Currency currency) {
|
||||
NumberFormat numberFormat = NumberFormat.getInstance();
|
||||
|
||||
if (currency == null) {
|
||||
numberFormat.setMaximumFractionDigits(0);
|
||||
return context.getString(R.string.balancePoints, numberFormat.format(value));
|
||||
}
|
||||
|
||||
NumberFormat currencyFormat = NumberFormat.getCurrencyInstance();
|
||||
currencyFormat.setCurrency(currency);
|
||||
currencyFormat.setMinimumFractionDigits(currency.getDefaultFractionDigits());
|
||||
currencyFormat.setMaximumFractionDigits(currency.getDefaultFractionDigits());
|
||||
|
||||
return currencyFormat.format(value);
|
||||
}
|
||||
|
||||
static public String formatBalanceWithoutCurrencySymbol(BigDecimal value, Currency currency) {
|
||||
NumberFormat numberFormat = NumberFormat.getInstance();
|
||||
|
||||
if (currency == null) {
|
||||
numberFormat.setMaximumFractionDigits(0);
|
||||
return numberFormat.format(value);
|
||||
}
|
||||
|
||||
numberFormat.setMinimumFractionDigits(currency.getDefaultFractionDigits());
|
||||
numberFormat.setMaximumFractionDigits(currency.getDefaultFractionDigits());
|
||||
|
||||
return numberFormat.format(value);
|
||||
}
|
||||
|
||||
static public Boolean currencyHasDecimals(Currency currency) {
|
||||
if (currency == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return currency.getDefaultFractionDigits() != 0;
|
||||
}
|
||||
|
||||
static public BigDecimal parseCurrency(String value, Boolean hasDecimals) throws NumberFormatException {
|
||||
// If there are no decimals expected, remove all separators before parsing
|
||||
if (!hasDecimals) {
|
||||
value = value.replaceAll("[^0-9]", "");
|
||||
return new BigDecimal(value);
|
||||
}
|
||||
|
||||
// There are many ways users can write a currency, so we fix it up a bit
|
||||
// 1. Replace all non-numbers with dots
|
||||
value = value.replaceAll("[^0-9]", ".");
|
||||
|
||||
// 2. Remove all but the last dot
|
||||
while (value.split("\\.").length > 2) {
|
||||
value = value.replaceFirst("\\.", "");
|
||||
}
|
||||
|
||||
// Parse as BigDecimal
|
||||
return new BigDecimal(value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
package protect.card_locker.importexport;
|
||||
|
||||
import org.apache.commons.csv.CSVRecord;
|
||||
|
||||
import protect.card_locker.FormatException;
|
||||
|
||||
public class CSVHelpers {
|
||||
/**
|
||||
* Extract a string from the items array. The index into the array
|
||||
* is determined by looking up the index in the fields map using the
|
||||
* "key" as the key. If no such key exists, defaultValue is returned
|
||||
* if it is not null. Otherwise, a FormatException is thrown.
|
||||
*/
|
||||
static String extractString(String key, CSVRecord record, String defaultValue)
|
||||
throws FormatException
|
||||
{
|
||||
String toReturn = defaultValue;
|
||||
|
||||
if(record.isMapped(key))
|
||||
{
|
||||
toReturn = record.get(key);
|
||||
}
|
||||
else
|
||||
{
|
||||
if(defaultValue == null)
|
||||
{
|
||||
throw new FormatException("Field not used but expected: " + key);
|
||||
}
|
||||
}
|
||||
|
||||
return toReturn;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract an integer from the items array. The index into the array
|
||||
* is determined by looking up the index in the fields map using the
|
||||
* "key" as the key. If no such key exists, or the data is not a valid
|
||||
* int, a FormatException is thrown.
|
||||
*/
|
||||
static Integer extractInt(String key, CSVRecord record, boolean nullIsOk)
|
||||
throws FormatException
|
||||
{
|
||||
if(record.isMapped(key) == false)
|
||||
{
|
||||
throw new FormatException("Field not used but expected: " + key);
|
||||
}
|
||||
|
||||
String value = record.get(key);
|
||||
if(value.isEmpty() && nullIsOk)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return Integer.parseInt(record.get(key));
|
||||
}
|
||||
catch(NumberFormatException e)
|
||||
{
|
||||
throw new FormatException("Failed to parse field: " + key, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract a long from the items array. The index into the array
|
||||
* is determined by looking up the index in the fields map using the
|
||||
* "key" as the key. If no such key exists, or the data is not a valid
|
||||
* int, a FormatException is thrown.
|
||||
*/
|
||||
static Long extractLong(String key, CSVRecord record, boolean nullIsOk)
|
||||
throws FormatException
|
||||
{
|
||||
if(record.isMapped(key) == false)
|
||||
{
|
||||
throw new FormatException("Field not used but expected: " + key);
|
||||
}
|
||||
|
||||
String value = record.get(key);
|
||||
if(value.isEmpty() && nullIsOk)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return Long.parseLong(record.get(key));
|
||||
}
|
||||
catch(NumberFormatException e)
|
||||
{
|
||||
throw new FormatException("Failed to parse field: " + key, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package protect.card_locker;
|
||||
package protect.card_locker.importexport;
|
||||
|
||||
import android.database.Cursor;
|
||||
|
||||
@@ -8,6 +8,10 @@ import org.apache.commons.csv.CSVPrinter;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStreamWriter;
|
||||
|
||||
import protect.card_locker.DBHelper;
|
||||
import protect.card_locker.Group;
|
||||
import protect.card_locker.LoyaltyCard;
|
||||
|
||||
/**
|
||||
* Class for exporting the database into CSV (Comma Separate Values)
|
||||
* format.
|
||||
@@ -49,6 +53,9 @@ public class CsvDatabaseExporter implements DatabaseExporter
|
||||
printer.printRecord(DBHelper.LoyaltyCardDbIds.ID,
|
||||
DBHelper.LoyaltyCardDbIds.STORE,
|
||||
DBHelper.LoyaltyCardDbIds.NOTE,
|
||||
DBHelper.LoyaltyCardDbIds.EXPIRY,
|
||||
DBHelper.LoyaltyCardDbIds.BALANCE,
|
||||
DBHelper.LoyaltyCardDbIds.BALANCE_TYPE,
|
||||
DBHelper.LoyaltyCardDbIds.CARD_ID,
|
||||
DBHelper.LoyaltyCardDbIds.HEADER_COLOR,
|
||||
DBHelper.LoyaltyCardDbIds.BARCODE_TYPE,
|
||||
@@ -63,6 +70,9 @@ public class CsvDatabaseExporter implements DatabaseExporter
|
||||
printer.printRecord(card.id,
|
||||
card.store,
|
||||
card.note,
|
||||
card.expiry != null ? card.expiry.getTime() : "",
|
||||
card.balance,
|
||||
card.balanceType,
|
||||
card.cardId,
|
||||
card.headerColor,
|
||||
card.barcodeType,
|
||||
@@ -1,17 +1,28 @@
|
||||
package protect.card_locker;
|
||||
package protect.card_locker.importexport;
|
||||
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
|
||||
import com.google.zxing.BarcodeFormat;
|
||||
|
||||
import org.apache.commons.csv.CSVFormat;
|
||||
import org.apache.commons.csv.CSVParser;
|
||||
import org.apache.commons.csv.CSVRecord;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.StringReader;
|
||||
import java.math.BigDecimal;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Currency;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
import protect.card_locker.DBHelper;
|
||||
import protect.card_locker.FormatException;
|
||||
import protect.card_locker.Group;
|
||||
|
||||
/**
|
||||
* Class for importing a database from CSV (Comma Separate Values)
|
||||
* formatted data.
|
||||
@@ -21,9 +32,9 @@ import java.util.List;
|
||||
*/
|
||||
public class CsvDatabaseImporter implements DatabaseImporter
|
||||
{
|
||||
public void importData(DBHelper db, InputStreamReader input) throws IOException, FormatException, InterruptedException
|
||||
public void importData(DBHelper db, InputStream input) throws IOException, FormatException, InterruptedException
|
||||
{
|
||||
BufferedReader bufferedReader = new BufferedReader(input);
|
||||
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8));
|
||||
|
||||
bufferedReader.mark(100);
|
||||
|
||||
@@ -47,6 +58,8 @@ public class CsvDatabaseImporter implements DatabaseImporter
|
||||
default:
|
||||
throw new FormatException(String.format("No code to parse version %s", version));
|
||||
}
|
||||
|
||||
bufferedReader.close();
|
||||
}
|
||||
|
||||
public void parseV1(DBHelper db, BufferedReader input) throws IOException, FormatException, InterruptedException
|
||||
@@ -131,7 +144,6 @@ public class CsvDatabaseImporter implements DatabaseImporter
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public void parseV2Groups(DBHelper db, SQLiteDatabase database, String data) throws IOException, FormatException, InterruptedException
|
||||
{
|
||||
// Parse groups
|
||||
@@ -145,10 +157,10 @@ public class CsvDatabaseImporter implements DatabaseImporter
|
||||
throw new InterruptedException();
|
||||
}
|
||||
}
|
||||
|
||||
groupParser.close();
|
||||
} catch (IllegalArgumentException | IllegalStateException e) {
|
||||
throw new FormatException("Issue parsing CSV data", e);
|
||||
} finally {
|
||||
groupParser.close();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -165,10 +177,10 @@ public class CsvDatabaseImporter implements DatabaseImporter
|
||||
throw new InterruptedException();
|
||||
}
|
||||
}
|
||||
|
||||
cardParser.close();
|
||||
} catch (IllegalArgumentException | IllegalStateException e) {
|
||||
throw new FormatException("Issue parsing CSV data", e);
|
||||
} finally {
|
||||
cardParser.close();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,66 +197,10 @@ public class CsvDatabaseImporter implements DatabaseImporter
|
||||
throw new InterruptedException();
|
||||
}
|
||||
}
|
||||
|
||||
cardGroupParser.close();
|
||||
} catch (IllegalArgumentException | IllegalStateException e) {
|
||||
throw new FormatException("Issue parsing CSV data", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract a string from the items array. The index into the array
|
||||
* is determined by looking up the index in the fields map using the
|
||||
* "key" as the key. If no such key exists, defaultValue is returned
|
||||
* if it is not null. Otherwise, a FormatException is thrown.
|
||||
*/
|
||||
private String extractString(String key, CSVRecord record, String defaultValue)
|
||||
throws FormatException
|
||||
{
|
||||
String toReturn = defaultValue;
|
||||
|
||||
if(record.isMapped(key))
|
||||
{
|
||||
toReturn = record.get(key);
|
||||
}
|
||||
else
|
||||
{
|
||||
if(defaultValue == null)
|
||||
{
|
||||
throw new FormatException("Field not used but expected: " + key);
|
||||
}
|
||||
}
|
||||
|
||||
return toReturn;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract an integer from the items array. The index into the array
|
||||
* is determined by looking up the index in the fields map using the
|
||||
* "key" as the key. If no such key exists, or the data is not a valid
|
||||
* int, a FormatException is thrown.
|
||||
*/
|
||||
private Integer extractInt(String key, CSVRecord record, boolean nullIsOk)
|
||||
throws FormatException
|
||||
{
|
||||
if(record.isMapped(key) == false)
|
||||
{
|
||||
throw new FormatException("Field not used but expected: " + key);
|
||||
}
|
||||
|
||||
String value = record.get(key);
|
||||
if(value.isEmpty() && nullIsOk)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return Integer.parseInt(record.get(key));
|
||||
}
|
||||
catch(NumberFormatException e)
|
||||
{
|
||||
throw new FormatException("Failed to parse field: " + key, e);
|
||||
} finally {
|
||||
cardGroupParser.close();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -255,40 +211,70 @@ public class CsvDatabaseImporter implements DatabaseImporter
|
||||
private void importLoyaltyCard(SQLiteDatabase database, DBHelper helper, CSVRecord record)
|
||||
throws IOException, FormatException
|
||||
{
|
||||
int id = extractInt(DBHelper.LoyaltyCardDbIds.ID, record, false);
|
||||
int id = CSVHelpers.extractInt(DBHelper.LoyaltyCardDbIds.ID, record, false);
|
||||
|
||||
String store = extractString(DBHelper.LoyaltyCardDbIds.STORE, record, "");
|
||||
String store = CSVHelpers.extractString(DBHelper.LoyaltyCardDbIds.STORE, record, "");
|
||||
if(store.isEmpty())
|
||||
{
|
||||
throw new FormatException("No store listed, but is required");
|
||||
}
|
||||
|
||||
String note = extractString(DBHelper.LoyaltyCardDbIds.NOTE, record, "");
|
||||
String note = CSVHelpers.extractString(DBHelper.LoyaltyCardDbIds.NOTE, record, "");
|
||||
Date expiry = null;
|
||||
try {
|
||||
expiry = new Date(CSVHelpers.extractLong(DBHelper.LoyaltyCardDbIds.EXPIRY, record, true));
|
||||
} catch (NullPointerException | FormatException e) { }
|
||||
|
||||
String cardId = extractString(DBHelper.LoyaltyCardDbIds.CARD_ID, record, "");
|
||||
BigDecimal balance;
|
||||
try {
|
||||
balance = new BigDecimal(CSVHelpers.extractString(DBHelper.LoyaltyCardDbIds.BALANCE, record, null));
|
||||
} catch (FormatException _e ) {
|
||||
// These fields did not exist in versions 1.8.1 and before
|
||||
// We catch this exception so we can still import old backups
|
||||
balance = new BigDecimal("0");
|
||||
}
|
||||
|
||||
Currency balanceType = null;
|
||||
String unparsedBalanceType = CSVHelpers.extractString(DBHelper.LoyaltyCardDbIds.BALANCE_TYPE, record, "");
|
||||
if(!unparsedBalanceType.isEmpty()) {
|
||||
balanceType = Currency.getInstance(unparsedBalanceType);
|
||||
}
|
||||
|
||||
String cardId = CSVHelpers.extractString(DBHelper.LoyaltyCardDbIds.CARD_ID, record, "");
|
||||
if(cardId.isEmpty())
|
||||
{
|
||||
throw new FormatException("No card ID listed, but is required");
|
||||
}
|
||||
|
||||
String barcodeType = extractString(DBHelper.LoyaltyCardDbIds.BARCODE_TYPE, record, "");
|
||||
String barcodeId = CSVHelpers.extractString(DBHelper.LoyaltyCardDbIds.BARCODE_ID, record, "");
|
||||
if(barcodeId.isEmpty())
|
||||
{
|
||||
barcodeId = null;
|
||||
}
|
||||
|
||||
BarcodeFormat barcodeType = null;
|
||||
String unparsedBarcodeType = CSVHelpers.extractString(DBHelper.LoyaltyCardDbIds.BARCODE_TYPE, record, "");
|
||||
if(!unparsedBarcodeType.isEmpty())
|
||||
{
|
||||
barcodeType = BarcodeFormat.valueOf(unparsedBarcodeType);
|
||||
}
|
||||
|
||||
Integer headerColor = null;
|
||||
|
||||
if(record.isMapped(DBHelper.LoyaltyCardDbIds.HEADER_COLOR))
|
||||
{
|
||||
headerColor = extractInt(DBHelper.LoyaltyCardDbIds.HEADER_COLOR, record, true);
|
||||
headerColor = CSVHelpers.extractInt(DBHelper.LoyaltyCardDbIds.HEADER_COLOR, record, true);
|
||||
}
|
||||
|
||||
int starStatus = 0;
|
||||
try {
|
||||
starStatus = extractInt(DBHelper.LoyaltyCardDbIds.STAR_STATUS, record, false);
|
||||
starStatus = CSVHelpers.extractInt(DBHelper.LoyaltyCardDbIds.STAR_STATUS, record, false);
|
||||
} catch (FormatException _e ) {
|
||||
// This field did not exist in versions 0.28 and before
|
||||
// We catch this exception so we can still import old backups
|
||||
}
|
||||
if (starStatus != 1) starStatus = 0;
|
||||
helper.insertLoyaltyCard(database, id, store, note, cardId, barcodeType, headerColor, starStatus);
|
||||
helper.insertLoyaltyCard(database, id, store, note, expiry, balance, balanceType, cardId, barcodeId, barcodeType, headerColor, starStatus);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -298,7 +284,7 @@ public class CsvDatabaseImporter implements DatabaseImporter
|
||||
private void importGroup(SQLiteDatabase database, DBHelper helper, CSVRecord record)
|
||||
throws IOException, FormatException
|
||||
{
|
||||
String id = extractString(DBHelper.LoyaltyCardDbGroups.ID, record, null);
|
||||
String id = CSVHelpers.extractString(DBHelper.LoyaltyCardDbGroups.ID, record, null);
|
||||
|
||||
helper.insertGroup(database, id);
|
||||
}
|
||||
@@ -310,8 +296,8 @@ public class CsvDatabaseImporter implements DatabaseImporter
|
||||
private void importCardGroupMapping(SQLiteDatabase database, DBHelper helper, CSVRecord record)
|
||||
throws IOException, FormatException
|
||||
{
|
||||
Integer cardId = extractInt(DBHelper.LoyaltyCardDbIdsGroups.cardID, record, false);
|
||||
String groupId = extractString(DBHelper.LoyaltyCardDbIdsGroups.groupID, record, null);
|
||||
Integer cardId = CSVHelpers.extractInt(DBHelper.LoyaltyCardDbIdsGroups.cardID, record, false);
|
||||
String groupId = CSVHelpers.extractString(DBHelper.LoyaltyCardDbIdsGroups.groupID, record, null);
|
||||
|
||||
List<Group> cardGroups = helper.getLoyaltyCardGroups(cardId);
|
||||
cardGroups.add(helper.getGroup(groupId));
|
||||
@@ -1,8 +1,10 @@
|
||||
package protect.card_locker;
|
||||
package protect.card_locker.importexport;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStreamWriter;
|
||||
|
||||
import protect.card_locker.DBHelper;
|
||||
|
||||
/**
|
||||
* Interface for a class which can export the contents of the database
|
||||
* in a given format.
|
||||
@@ -0,0 +1,25 @@
|
||||
package protect.card_locker.importexport;
|
||||
|
||||
import org.json.JSONException;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.text.ParseException;
|
||||
|
||||
import protect.card_locker.DBHelper;
|
||||
import protect.card_locker.FormatException;
|
||||
|
||||
/**
|
||||
* Interface for a class which can import the contents of a stream
|
||||
* into the database.
|
||||
*/
|
||||
public interface DatabaseImporter
|
||||
{
|
||||
/**
|
||||
* Import data from the input stream in a given format into
|
||||
* the database.
|
||||
* @throws IOException
|
||||
* @throws FormatException
|
||||
*/
|
||||
void importData(DBHelper db, InputStream input) throws IOException, FormatException, InterruptedException, JSONException, ParseException;
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
package protect.card_locker.importexport;
|
||||
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
|
||||
import com.google.zxing.BarcodeFormat;
|
||||
|
||||
import org.apache.commons.csv.CSVFormat;
|
||||
import org.apache.commons.csv.CSVParser;
|
||||
import org.apache.commons.csv.CSVRecord;
|
||||
import org.json.JSONException;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.StringReader;
|
||||
import java.math.BigDecimal;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.text.ParseException;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipInputStream;
|
||||
|
||||
import protect.card_locker.DBHelper;
|
||||
import protect.card_locker.FormatException;
|
||||
|
||||
/**
|
||||
* Class for importing a database from CSV (Comma Separate Values)
|
||||
* formatted data.
|
||||
*
|
||||
* The database's loyalty cards are expected to appear in the CSV data.
|
||||
* A header is expected for the each table showing the names of the columns.
|
||||
*/
|
||||
public class FidmeImporter implements DatabaseImporter
|
||||
{
|
||||
public void importData(DBHelper db, InputStream input) throws IOException, FormatException, JSONException, ParseException {
|
||||
// We actually retrieve a .zip file
|
||||
ZipInputStream zipInputStream = new ZipInputStream(input);
|
||||
|
||||
StringBuilder loyaltyCards = new StringBuilder();
|
||||
byte[] buffer = new byte[1024];
|
||||
int read = 0;
|
||||
|
||||
ZipEntry zipEntry;
|
||||
|
||||
while ((zipEntry = zipInputStream.getNextEntry()) != null) {
|
||||
if (zipEntry.getName().equals("loyalty_programs.csv")) {
|
||||
while ((read = zipInputStream.read(buffer, 0, 1024)) >= 0) {
|
||||
loyaltyCards.append(new String(buffer, 0, read, StandardCharsets.UTF_8));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (loyaltyCards.length() == 0) {
|
||||
throw new FormatException("Couldn't find loyalty_programs.csv in zip file or it is empty");
|
||||
}
|
||||
|
||||
SQLiteDatabase database = db.getWritableDatabase();
|
||||
database.beginTransaction();
|
||||
|
||||
final CSVParser fidmeParser = new CSVParser(new StringReader(loyaltyCards.toString()), CSVFormat.RFC4180.withDelimiter(';').withHeader());
|
||||
|
||||
try {
|
||||
for (CSVRecord record : fidmeParser) {
|
||||
importLoyaltyCard(database, db, record);
|
||||
|
||||
if (Thread.currentThread().isInterrupted()) {
|
||||
throw new InterruptedException();
|
||||
}
|
||||
}
|
||||
} catch (IllegalArgumentException | IllegalStateException | InterruptedException e) {
|
||||
throw new FormatException("Issue parsing CSV data", e);
|
||||
} finally {
|
||||
fidmeParser.close();
|
||||
}
|
||||
|
||||
database.setTransactionSuccessful();
|
||||
database.endTransaction();
|
||||
database.close();
|
||||
|
||||
zipInputStream.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Import a single loyalty card into the database using the given
|
||||
* session.
|
||||
*/
|
||||
private void importLoyaltyCard(SQLiteDatabase database, DBHelper helper, CSVRecord record)
|
||||
throws IOException, FormatException
|
||||
{
|
||||
// A loyalty card export from Fidme contains the following fields:
|
||||
// Retailer (store name)
|
||||
// Program (program name)
|
||||
// Added at (YYYY-MM-DD HH:MM:SS UTC)
|
||||
// Reference (card ID)
|
||||
// Firstname (card holder first name)
|
||||
// Lastname (card holder last name)
|
||||
|
||||
// The store is called Retailer
|
||||
String store = CSVHelpers.extractString("Retailer", record, "");
|
||||
|
||||
if (store.isEmpty())
|
||||
{
|
||||
throw new FormatException("No store listed, but is required");
|
||||
}
|
||||
|
||||
// There seems to be no note field in the CSV? So let's combine other fields instead...
|
||||
String program = CSVHelpers.extractString("Program", record, "");
|
||||
String addedAt = CSVHelpers.extractString("Added At", record, "");
|
||||
String firstName = CSVHelpers.extractString("Firstname", record, "");
|
||||
String lastName = CSVHelpers.extractString("Lastname", record, "");
|
||||
|
||||
String combinedName = String.format("%s %s", firstName, lastName);
|
||||
|
||||
StringBuilder noteBuilder = new StringBuilder();
|
||||
if (!program.isEmpty()) noteBuilder.append(program).append('\n');
|
||||
if (!addedAt.isEmpty()) noteBuilder.append(addedAt).append('\n');
|
||||
if (!combinedName.isEmpty()) noteBuilder.append(combinedName).append('\n');
|
||||
String note = noteBuilder.toString();
|
||||
|
||||
// The ID is called reference
|
||||
String cardId = CSVHelpers.extractString("Reference", record, "");
|
||||
if(cardId.isEmpty())
|
||||
{
|
||||
throw new FormatException("No card ID listed, but is required");
|
||||
}
|
||||
|
||||
// Sadly, Fidme exports don't contain the card type
|
||||
// I guess they have an online DB of all the different companies and what type they use
|
||||
// TODO: Hook this into our own loyalty card DB if we ever get one
|
||||
BarcodeFormat barcodeType = null;
|
||||
|
||||
// No favourite data in the export either
|
||||
int starStatus = 0;
|
||||
|
||||
helper.insertLoyaltyCard(database, store, note, null, BigDecimal.valueOf(0), null, cardId, null, barcodeType, null, starStatus);
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,13 @@
|
||||
package protect.card_locker;
|
||||
package protect.card_locker.importexport;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStreamWriter;
|
||||
|
||||
import protect.card_locker.DBHelper;
|
||||
import protect.card_locker.DataFormat;
|
||||
|
||||
public class MultiFormatExporter
|
||||
{
|
||||
private static final String TAG = "Catima";
|
||||
@@ -25,9 +28,12 @@ public class MultiFormatExporter
|
||||
|
||||
switch(format)
|
||||
{
|
||||
case CSV:
|
||||
case Catima:
|
||||
exporter = new CsvDatabaseExporter();
|
||||
break;
|
||||
default:
|
||||
Log.e(TAG, "Failed to export data, unknown format " + format.name());
|
||||
break;
|
||||
}
|
||||
|
||||
if(exporter != null)
|
||||
@@ -1,9 +1,16 @@
|
||||
package protect.card_locker;
|
||||
package protect.card_locker.importexport;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import org.json.JSONException;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.InputStream;
|
||||
import java.text.ParseException;
|
||||
|
||||
import protect.card_locker.DBHelper;
|
||||
import protect.card_locker.DataFormat;
|
||||
import protect.card_locker.FormatException;
|
||||
|
||||
public class MultiFormatImporter
|
||||
{
|
||||
@@ -20,43 +27,40 @@ public class MultiFormatImporter
|
||||
* false otherwise. If false, no data was written to
|
||||
* the database.
|
||||
*/
|
||||
public static boolean importData(DBHelper db, InputStreamReader input, DataFormat format)
|
||||
public static boolean importData(DBHelper db, InputStream input, DataFormat format)
|
||||
{
|
||||
DatabaseImporter importer = null;
|
||||
|
||||
switch(format)
|
||||
{
|
||||
case CSV:
|
||||
case Catima:
|
||||
importer = new CsvDatabaseImporter();
|
||||
break;
|
||||
case Fidme:
|
||||
importer = new FidmeImporter();
|
||||
break;
|
||||
case VoucherVault:
|
||||
importer = new VoucherVaultImporter();
|
||||
break;
|
||||
}
|
||||
|
||||
if(importer != null)
|
||||
if (importer != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
importer.importData(db, input);
|
||||
return true;
|
||||
}
|
||||
catch(IOException e)
|
||||
catch(IOException | FormatException | InterruptedException | JSONException | ParseException e)
|
||||
{
|
||||
Log.e(TAG, "Failed to input data", e);
|
||||
}
|
||||
catch(FormatException e)
|
||||
{
|
||||
Log.e(TAG, "Failed to input data", e);
|
||||
}
|
||||
catch(InterruptedException e)
|
||||
{
|
||||
Log.e(TAG, "Failed to input data", e);
|
||||
Log.e(TAG, "Failed to import data", e);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.e(TAG, "Unsupported data format imported: " + format.name());
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
package protect.card_locker.importexport;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.graphics.Color;
|
||||
|
||||
import com.google.zxing.BarcodeFormat;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.math.BigDecimal;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.text.ParseException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Currency;
|
||||
import java.util.Date;
|
||||
import java.util.TimeZone;
|
||||
|
||||
import protect.card_locker.DBHelper;
|
||||
import protect.card_locker.FormatException;
|
||||
|
||||
/**
|
||||
* Class for importing a database from CSV (Comma Separate Values)
|
||||
* formatted data.
|
||||
*
|
||||
* The database's loyalty cards are expected to appear in the CSV data.
|
||||
* A header is expected for the each table showing the names of the columns.
|
||||
*/
|
||||
public class VoucherVaultImporter implements DatabaseImporter
|
||||
{
|
||||
public void importData(DBHelper db, InputStream input) throws IOException, FormatException, JSONException, ParseException {
|
||||
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8));
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
String line;
|
||||
while ((line = bufferedReader.readLine()) != null) {
|
||||
sb.append(line);
|
||||
}
|
||||
JSONArray jsonArray = new JSONArray(sb.toString());
|
||||
|
||||
SQLiteDatabase database = db.getWritableDatabase();
|
||||
database.beginTransaction();
|
||||
|
||||
// See https://github.com/tim-smart/vouchervault/issues/4#issuecomment-788226503 for more info
|
||||
for (int i = 0; i < jsonArray.length(); i++) {
|
||||
JSONObject jsonCard = jsonArray.getJSONObject(i);
|
||||
|
||||
String store = jsonCard.getString("description");
|
||||
|
||||
Date expiry = null;
|
||||
if (!jsonCard.isNull("expires")) {
|
||||
@SuppressLint("SimpleDateFormat") SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS");
|
||||
dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
|
||||
expiry = dateFormat.parse(jsonCard.getString("expires"));
|
||||
}
|
||||
|
||||
BigDecimal balance;
|
||||
if (!jsonCard.isNull("balance")) {
|
||||
balance = new BigDecimal(String.valueOf(jsonCard.getDouble("balance")));
|
||||
} else {
|
||||
balance = new BigDecimal("0");
|
||||
}
|
||||
|
||||
Currency balanceType = Currency.getInstance("USD");
|
||||
|
||||
String cardId = jsonCard.getString("code");
|
||||
|
||||
BarcodeFormat barcodeType = null;
|
||||
|
||||
String codeTypeFromJSON = jsonCard.getString("codeType");
|
||||
switch (codeTypeFromJSON) {
|
||||
case "CODE128":
|
||||
barcodeType = BarcodeFormat.CODE_128;
|
||||
break;
|
||||
case "CODE39":
|
||||
barcodeType = BarcodeFormat.CODE_39;
|
||||
break;
|
||||
case "EAN13":
|
||||
barcodeType = BarcodeFormat.EAN_13;
|
||||
break;
|
||||
case "PDF417":
|
||||
barcodeType = BarcodeFormat.PDF_417;
|
||||
break;
|
||||
case "QR":
|
||||
barcodeType = BarcodeFormat.QR_CODE;
|
||||
break;
|
||||
case "TEXT":
|
||||
break;
|
||||
default:
|
||||
throw new FormatException("Unknown barcode type found: " + codeTypeFromJSON);
|
||||
}
|
||||
|
||||
int headerColor;
|
||||
|
||||
String colorFromJSON = jsonCard.getString("color");
|
||||
switch (colorFromJSON) {
|
||||
case "GREY":
|
||||
headerColor = Color.GRAY;
|
||||
break;
|
||||
case "BLUE":
|
||||
headerColor = Color.BLUE;
|
||||
break;
|
||||
case "GREEN":
|
||||
headerColor = Color.GREEN;
|
||||
break;
|
||||
case "ORANGE":
|
||||
headerColor = Color.rgb(255, 165, 0);
|
||||
break;
|
||||
case "PURPLE":
|
||||
headerColor = Color.rgb(128, 0, 128);
|
||||
break;
|
||||
case "RED":
|
||||
headerColor = Color.RED;
|
||||
break;
|
||||
case "YELLOW":
|
||||
headerColor = Color.YELLOW;
|
||||
break;
|
||||
default:
|
||||
throw new FormatException("Unknown colour type foun: " + colorFromJSON);
|
||||
}
|
||||
|
||||
db.insertLoyaltyCard(store, "", expiry, balance, balanceType, cardId, null, barcodeType, headerColor, 0);
|
||||
}
|
||||
|
||||
database.setTransactionSuccessful();
|
||||
database.endTransaction();
|
||||
database.close();
|
||||
|
||||
bufferedReader.close();
|
||||
}
|
||||
}
|
||||
@@ -2,11 +2,11 @@ package protect.card_locker.preferences;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.preference.PreferenceManager;
|
||||
|
||||
import androidx.annotation.IntegerRes;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.appcompat.app.AppCompatDelegate;
|
||||
|
||||
import androidx.preference.PreferenceManager;
|
||||
import protect.card_locker.R;
|
||||
|
||||
public class Settings
|
||||
@@ -61,29 +61,34 @@ public class Settings
|
||||
return AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM;
|
||||
}
|
||||
|
||||
public int getCardTitleListFontSize()
|
||||
public double getFontSizeScale()
|
||||
{
|
||||
return getInt(R.string.settings_key_card_title_list_font_size, R.integer.settings_card_title_list_font_size_sp);
|
||||
return getInt(R.string.settings_key_max_font_size_scale, R.integer.settings_max_font_size_scale_pct) / 100.0;
|
||||
}
|
||||
|
||||
public int getCardNoteListFontSize()
|
||||
public int getSmallFont()
|
||||
{
|
||||
return getInt(R.string.settings_key_card_note_list_font_size, R.integer.settings_card_note_list_font_size_sp);
|
||||
return 14;
|
||||
}
|
||||
|
||||
public int getCardTitleFontSize()
|
||||
public int getMediumFont()
|
||||
{
|
||||
return getInt(R.string.settings_key_card_title_font_size, R.integer.settings_card_title_font_size_sp);
|
||||
return 28;
|
||||
}
|
||||
|
||||
public int getCardIdFontSize()
|
||||
public int getLargeFont()
|
||||
{
|
||||
return getInt(R.string.settings_key_card_id_font_size, R.integer.settings_card_id_font_size_sp);
|
||||
return 40;
|
||||
}
|
||||
|
||||
public int getCardNoteFontSize()
|
||||
public int getFontSizeMin(int fontSize)
|
||||
{
|
||||
return getInt(R.string.settings_key_card_note_font_size, R.integer.settings_card_note_font_size_sp);
|
||||
return (int) (Math.round(fontSize / 2.0) - 1);
|
||||
}
|
||||
|
||||
public int getFontSizeMax(int fontSize)
|
||||
{
|
||||
return (int) Math.round(fontSize * getFontSizeScale());
|
||||
}
|
||||
|
||||
public boolean useMaxBrightnessDisplayingBarcode()
|
||||
@@ -95,4 +100,14 @@ public class Settings
|
||||
{
|
||||
return getBoolean(R.string.settings_key_lock_barcode_orientation, false);
|
||||
}
|
||||
|
||||
public boolean getKeepScreenOn()
|
||||
{
|
||||
return getBoolean(R.string.settings_key_keep_screen_on, true);
|
||||
}
|
||||
|
||||
public boolean getDisableLockscreenWhileViewingCard()
|
||||
{
|
||||
return getBoolean(R.string.settings_key_disable_lockscreen_while_viewing_card, true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
package protect.card_locker.preferences;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.preference.Preference;
|
||||
import android.preference.PreferenceFragment;
|
||||
import android.view.MenuItem;
|
||||
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.app.AppCompatDelegate;
|
||||
import android.view.MenuItem;
|
||||
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
import androidx.preference.Preference;
|
||||
import androidx.preference.PreferenceFragmentCompat;
|
||||
import nl.invissvenska.numberpickerpreference.NumberDialogPreference;
|
||||
import nl.invissvenska.numberpickerpreference.NumberPickerPreferenceDialogFragment;
|
||||
import protect.card_locker.R;
|
||||
|
||||
public class SettingsActivity extends AppCompatActivity
|
||||
@@ -16,16 +20,17 @@ public class SettingsActivity extends AppCompatActivity
|
||||
protected void onCreate(Bundle savedInstanceState)
|
||||
{
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.settings_activity);
|
||||
|
||||
ActionBar actionBar = getSupportActionBar();
|
||||
if(actionBar != null)
|
||||
{
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
actionBar.setDisplayHomeAsUpEnabled(true);
|
||||
}
|
||||
|
||||
// Display the fragment as the main content.
|
||||
getFragmentManager().beginTransaction()
|
||||
.replace(android.R.id.content, new SettingsFragment())
|
||||
getSupportFragmentManager().beginTransaction()
|
||||
.replace(R.id.settings_container, new SettingsFragment())
|
||||
.commit();
|
||||
}
|
||||
|
||||
@@ -43,17 +48,18 @@ public class SettingsActivity extends AppCompatActivity
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
public static class SettingsFragment extends PreferenceFragment
|
||||
public static class SettingsFragment extends PreferenceFragmentCompat
|
||||
{
|
||||
private static final String DIALOG_FRAGMENT_TAG = "SettingsFragment";
|
||||
@Override
|
||||
public void onCreate(final Bundle savedInstanceState)
|
||||
public void onCreatePreferences(Bundle savedInstanceState, String rootKey)
|
||||
{
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
// Load the preferences from an XML resource
|
||||
addPreferencesFromResource(R.xml.preferences);
|
||||
|
||||
findPreference(getResources().getString(R.string.settings_key_theme)).setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener()
|
||||
Preference themePreference = findPreference(getResources().getString(R.string.settings_key_theme));
|
||||
assert themePreference != null;
|
||||
themePreference.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener()
|
||||
{
|
||||
@Override
|
||||
public boolean onPreferenceChange(Preference preference, Object o)
|
||||
@@ -71,11 +77,37 @@ public class SettingsActivity extends AppCompatActivity
|
||||
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM);
|
||||
}
|
||||
|
||||
getActivity().recreate();
|
||||
FragmentActivity activity = getActivity();
|
||||
if (activity != null) {
|
||||
activity.recreate();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisplayPreferenceDialog(Preference preference)
|
||||
{
|
||||
if (preference instanceof NumberDialogPreference)
|
||||
{
|
||||
NumberDialogPreference dialogPreference = (NumberDialogPreference) preference;
|
||||
DialogFragment dialogFragment = NumberPickerPreferenceDialogFragment
|
||||
.newInstance(
|
||||
dialogPreference.getKey(),
|
||||
dialogPreference.getMinValue(),
|
||||
dialogPreference.getMaxValue(),
|
||||
dialogPreference.getStepValue(),
|
||||
dialogPreference.getUnitText()
|
||||
);
|
||||
dialogFragment.setTargetFragment(this, 0);
|
||||
dialogFragment.show(getParentFragmentManager(), DIALOG_FRAGMENT_TAG);
|
||||
}
|
||||
else
|
||||
{
|
||||
super.onDisplayPreferenceDialog(preference);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
22
app/src/main/res/animator/flip_left_in.xml
Normal file
@@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<set xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<objectAnimator
|
||||
android:duration="0"
|
||||
android:propertyName="alpha"
|
||||
android:valueFrom="1.0"
|
||||
android:valueTo="0.0" />
|
||||
|
||||
<objectAnimator
|
||||
android:duration="@integer/full_rotation_duration"
|
||||
android:interpolator="@android:interpolator/accelerate_decelerate"
|
||||
android:propertyName="rotationY"
|
||||
android:valueFrom="-180"
|
||||
android:valueTo="0" />
|
||||
|
||||
<objectAnimator
|
||||
android:duration="1"
|
||||
android:propertyName="alpha"
|
||||
android:startOffset="@integer/half_rotation_duration"
|
||||
android:valueFrom="0.0"
|
||||
android:valueTo="1.0" />
|
||||
</set>
|
||||
16
app/src/main/res/animator/flip_left_out.xml
Normal file
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<set xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<objectAnimator
|
||||
android:duration="@integer/full_rotation_duration"
|
||||
android:interpolator="@android:interpolator/accelerate_decelerate"
|
||||
android:propertyName="rotationY"
|
||||
android:valueFrom="0"
|
||||
android:valueTo="180" />
|
||||
|
||||
<objectAnimator
|
||||
android:duration="1"
|
||||
android:propertyName="alpha"
|
||||
android:startOffset="@integer/half_rotation_duration"
|
||||
android:valueFrom="1.0"
|
||||
android:valueTo="0.0" />
|
||||
</set>
|
||||
22
app/src/main/res/animator/flip_right_in.xml
Normal file
@@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<set xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<objectAnimator
|
||||
android:duration="0"
|
||||
android:propertyName="alpha"
|
||||
android:valueFrom="1.0"
|
||||
android:valueTo="0.0" />
|
||||
|
||||
<objectAnimator
|
||||
android:duration="@integer/full_rotation_duration"
|
||||
android:interpolator="@android:interpolator/accelerate_decelerate"
|
||||
android:propertyName="rotationY"
|
||||
android:valueFrom="180"
|
||||
android:valueTo="0" />
|
||||
|
||||
<objectAnimator
|
||||
android:duration="1"
|
||||
android:propertyName="alpha"
|
||||
android:startOffset="@integer/half_rotation_duration"
|
||||
android:valueFrom="0.0"
|
||||
android:valueTo="1.0" />
|
||||
</set>
|
||||
16
app/src/main/res/animator/flip_right_out.xml
Normal file
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<set xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<objectAnimator
|
||||
android:duration="@integer/full_rotation_duration"
|
||||
android:interpolator="@android:interpolator/accelerate_decelerate"
|
||||
android:propertyName="rotationY"
|
||||
android:valueFrom="0"
|
||||
android:valueTo="-180" />
|
||||
|
||||
<objectAnimator
|
||||
android:duration="1"
|
||||
android:propertyName="alpha"
|
||||
android:startOffset="@integer/half_rotation_duration"
|
||||
android:valueFrom="1.0"
|
||||
android:valueTo="0.0" />
|
||||
</set>
|
||||
|
Before Width: | Height: | Size: 127 B |
|
Before Width: | Height: | Size: 233 B |
|
Before Width: | Height: | Size: 161 B |
|
Before Width: | Height: | Size: 163 B |
|
Before Width: | Height: | Size: 172 B |
|
Before Width: | Height: | Size: 306 B |
|
Before Width: | Height: | Size: 303 B |
|
Before Width: | Height: | Size: 219 B |
|
Before Width: | Height: | Size: 582 B |
|
Before Width: | Height: | Size: 554 B |
|
Before Width: | Height: | Size: 336 B |
|
Before Width: | Height: | Size: 439 B |
|
Before Width: | Height: | Size: 368 B |
|
Before Width: | Height: | Size: 88 B |
|
Before Width: | Height: | Size: 155 B |
|
Before Width: | Height: | Size: 115 B |
|
Before Width: | Height: | Size: 135 B |
|
Before Width: | Height: | Size: 132 B |
|
Before Width: | Height: | Size: 200 B |
|
Before Width: | Height: | Size: 198 B |
|
Before Width: | Height: | Size: 165 B |
|
Before Width: | Height: | Size: 410 B |
|
Before Width: | Height: | Size: 397 B |
|
Before Width: | Height: | Size: 234 B |
|
Before Width: | Height: | Size: 299 B |
|
Before Width: | Height: | Size: 303 B |
|
Before Width: | Height: | Size: 97 B |
|
Before Width: | Height: | Size: 225 B |
|
Before Width: | Height: | Size: 151 B |
|
Before Width: | Height: | Size: 207 B |
|
Before Width: | Height: | Size: 202 B |
|
Before Width: | Height: | Size: 354 B |
|
Before Width: | Height: | Size: 343 B |
|
Before Width: | Height: | Size: 239 B |
|
Before Width: | Height: | Size: 783 B |
|
Before Width: | Height: | Size: 758 B |
|
Before Width: | Height: | Size: 421 B |
|
Before Width: | Height: | Size: 564 B |
|
Before Width: | Height: | Size: 466 B |
|
Before Width: | Height: | Size: 97 B |
|
Before Width: | Height: | Size: 398 B |
|
Before Width: | Height: | Size: 194 B |
|
Before Width: | Height: | Size: 252 B |
|
Before Width: | Height: | Size: 252 B |
|
Before Width: | Height: | Size: 512 B |
|
Before Width: | Height: | Size: 494 B |
|
Before Width: | Height: | Size: 302 B |
|
Before Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 829 B |
|
Before Width: | Height: | Size: 809 B |