Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4f36030e38 | ||
|
|
90c2c03d12 | ||
|
|
3f853be51a | ||
|
|
835a348fad | ||
|
|
198a35fc15 | ||
|
|
6be1b25c38 | ||
|
|
ac632e3aac | ||
|
|
ddc5a14558 | ||
|
|
8b1ffe3e5a | ||
|
|
e9ba66848f | ||
|
|
7bdef27a50 | ||
|
|
ccd16a0f95 | ||
|
|
a385678de1 | ||
|
|
43c597f6fb | ||
|
|
ecccf34418 | ||
|
|
4027ce2cc6 | ||
|
|
209e6c5c30 | ||
|
|
5e29f3fbe9 | ||
|
|
ba3d63277a | ||
|
|
fe34b7209d | ||
|
|
0efdca7b4a | ||
|
|
827fbedc1f | ||
|
|
107b5bafd4 | ||
|
|
19a71480fa | ||
|
|
909e97a591 | ||
|
|
bc2be96871 | ||
|
|
d834621d99 | ||
|
|
ad83be1a60 | ||
|
|
d65ea02b49 | ||
|
|
c8d1357ceb | ||
|
|
4c05d046e6 | ||
|
|
4423ee62c8 | ||
|
|
bc69f99f8b | ||
|
|
6b088031fd | ||
|
|
b27d03e9c0 | ||
|
|
9a13838f28 | ||
|
|
cce2a460f6 | ||
|
|
36352ecde2 | ||
|
|
9a0bc324cb | ||
|
|
6dabafd6a3 | ||
|
|
dde4b1e0bd | ||
|
|
8a8c7bd1ff | ||
|
|
a2aed42e25 | ||
|
|
a38b7b647f | ||
|
|
76e82ed842 | ||
|
|
fd54bc604e | ||
|
|
59b62ecbc4 | ||
|
|
de6a2863f3 | ||
|
|
4b2eb80612 | ||
|
|
2de40d2277 | ||
|
|
7673b7cb0d | ||
|
|
097c5e4453 | ||
|
|
1fab30089e | ||
|
|
f9bf16dd47 | ||
|
|
7a59126312 | ||
|
|
694c854650 | ||
|
|
b1cd12ec77 | ||
|
|
0c27b15025 | ||
|
|
5de950bdbf | ||
|
|
ae9e304ef6 | ||
|
|
c4cc14554b | ||
|
|
d6afccf562 | ||
|
|
a90bdad117 | ||
|
|
52d4f082f2 | ||
|
|
1c63f5b3ac | ||
|
|
21985b4533 | ||
|
|
012ac644aa | ||
|
|
19f19739fd | ||
|
|
f01f9fcd91 | ||
|
|
4177b248c3 | ||
|
|
e4182a265b | ||
|
|
98df5a447c | ||
|
|
a0a1405359 | ||
|
|
76922c421c | ||
|
|
cac9dd16da | ||
|
|
a1f9ba1056 | ||
|
|
0b67c272b6 | ||
|
|
9b8be5305d | ||
|
|
a026ade13b | ||
|
|
b1771bc5ea | ||
|
|
5199460669 | ||
|
|
fd040e1660 | ||
|
|
2e1528364e | ||
|
|
d21af142bb | ||
|
|
8395e135f9 | ||
|
|
c0caa0ce44 | ||
|
|
96927ef9b2 | ||
|
|
84d9a3b55a | ||
|
|
ffe8e4c5ba | ||
|
|
bc7c5dca97 | ||
|
|
d9737f4df1 | ||
|
|
0e8147713a | ||
|
|
3d425a097a | ||
|
|
c26765cc63 | ||
|
|
fb6dc8ba55 | ||
|
|
70f4d166a0 | ||
|
|
6c136ce719 | ||
|
|
c6dee80c0a | ||
|
|
1f43dd4827 | ||
|
|
a366157977 | ||
|
|
6bb72672a3 | ||
|
|
04774527a0 | ||
|
|
6f3e644aa8 | ||
|
|
5e3176b703 | ||
|
|
0f0aaff284 | ||
|
|
f0d09e81b4 | ||
|
|
771258cb5c | ||
|
|
9482086666 | ||
|
|
bcad50c3e8 | ||
|
|
dbd055699e | ||
|
|
d67f8273d6 | ||
|
|
649be0ceb6 | ||
|
|
f5eb5dd8d2 | ||
|
|
eb805dad2f | ||
|
|
202bb32b29 | ||
|
|
50b8a4d329 | ||
|
|
fa15478fb4 | ||
|
|
20c2a5a8aa | ||
|
|
00b95678ed | ||
|
|
5df3517563 | ||
|
|
fd6946d2c2 | ||
|
|
c74d229e1f | ||
|
|
fa99a19db5 | ||
|
|
b083fcf213 | ||
|
|
46930ddea6 | ||
|
|
7bef77b96e | ||
|
|
c5f4c088b4 | ||
|
|
73f7210e18 | ||
|
|
469429e663 | ||
|
|
1a7ac59740 | ||
|
|
c13c59e728 | ||
|
|
bc17e25717 | ||
|
|
5017bd4c8d | ||
|
|
a44a913eca | ||
|
|
007e83e243 | ||
|
|
de8f97c246 | ||
|
|
1287591e53 | ||
|
|
8c17238572 | ||
|
|
5dbb36e6a9 | ||
|
|
ca7dfb3a33 | ||
|
|
419b7d5765 | ||
|
|
fa38866669 | ||
|
|
19ca521f59 | ||
|
|
be657d86cf | ||
|
|
5199a4a526 | ||
|
|
ff48a543c9 | ||
|
|
2eadd01a87 | ||
|
|
dd2abe7d89 | ||
|
|
8e05caac70 | ||
|
|
46aeced09f | ||
|
|
3681c5d011 | ||
|
|
20b4f7aac5 | ||
|
|
bc6e350271 | ||
|
|
c9c1b2f5bf | ||
|
|
1dbc0f0fa1 | ||
|
|
ab8170ee72 | ||
|
|
551464dc6f | ||
|
|
c3aac0a1e1 | ||
|
|
d678e1cfd0 | ||
|
|
a445ade877 | ||
|
|
1fd0b7637f | ||
|
|
fdc88df897 | ||
|
|
d0f133ae00 | ||
|
|
67646261e7 | ||
|
|
faeae9e904 | ||
|
|
77b21a9e83 | ||
|
|
cbe7860a3d | ||
|
|
fc29267d3d | ||
|
|
890506328b | ||
|
|
04036b6cd6 | ||
|
|
2602021c1c | ||
|
|
17a0bdc0a9 | ||
|
|
d71a220eca | ||
|
|
0c2e711d79 | ||
|
|
ca342a83d3 | ||
|
|
3c44ff56eb | ||
|
|
bcae64c0d0 | ||
|
|
c2f673729b | ||
|
|
2043e0f349 | ||
|
|
b93eb5963e | ||
|
|
5189fc9c5a | ||
|
|
2d311ff92a | ||
|
|
14bb08003d | ||
|
|
42f9887030 | ||
|
|
a59d241b50 | ||
|
|
9f31160c7b | ||
|
|
2eb4e51c17 | ||
|
|
53a554e7a2 | ||
|
|
7f6887716a | ||
|
|
5ef93802ce | ||
|
|
c1eff08bb9 | ||
|
|
8798d44ec7 | ||
|
|
d4e31ff97e | ||
|
|
ae8b08a85a | ||
|
|
09430d776e | ||
|
|
ef88c3179d | ||
|
|
c732ec726b | ||
|
|
801c768df4 | ||
|
|
552a2f6d57 | ||
|
|
7fa8810197 | ||
|
|
254af25bbb | ||
|
|
a27656c174 | ||
|
|
2ca55ebccc | ||
|
|
ec1d40942d | ||
|
|
7852e5ce9d | ||
|
|
f36a81d4a0 | ||
|
|
d86ea4073c | ||
|
|
a69530621d | ||
|
|
3c0f29a768 | ||
|
|
ccf5157fa7 | ||
|
|
c1ebb1c154 | ||
|
|
a4778bcc02 | ||
|
|
24d2c6d8d6 | ||
|
|
0f3a07f602 | ||
|
|
751d8932f8 | ||
|
|
9c2d063c24 | ||
|
|
0f9bbf5b2f | ||
|
|
530280981e | ||
|
|
ddfbb305c7 | ||
|
|
8d1a29922b | ||
|
|
c710f7c6ab | ||
|
|
111c587eef | ||
|
|
264e494487 | ||
|
|
8135b2a3dc | ||
|
|
dce62b70d3 | ||
|
|
533812c638 | ||
|
|
5dc5bed281 | ||
|
|
2a13561962 | ||
|
|
1476f41cda | ||
|
|
0da1ffbe02 | ||
|
|
980679b6a3 | ||
|
|
28cbb55299 | ||
|
|
1a4e7d100a | ||
|
|
265e43b439 | ||
|
|
03a58c3412 | ||
|
|
327cad48f0 | ||
|
|
8e64fe2f56 | ||
|
|
368d4dbb66 | ||
|
|
c8c3c094db | ||
|
|
aa4aefe35f | ||
|
|
536b3d77c1 | ||
|
|
bbf2a80981 | ||
|
|
ae99214d6c | ||
|
|
73b5a0969c | ||
|
|
bf5dc96ac9 | ||
|
|
301d9c2513 | ||
|
|
0a11611b8f | ||
|
|
249e310e43 | ||
|
|
6279c5731a | ||
|
|
df98637951 | ||
|
|
08b3558e46 | ||
|
|
648ef6fdf8 | ||
|
|
807f075a51 | ||
|
|
27789beeed | ||
|
|
97bee27827 | ||
|
|
3fee49301e | ||
|
|
8a6073291b | ||
|
|
47151de4a4 | ||
|
|
248c4edca1 | ||
|
|
2c7386d536 | ||
|
|
e19cad4c87 | ||
|
|
585e0f3372 | ||
|
|
2b7f611774 | ||
|
|
05b2a0d9c9 | ||
|
|
e7287eb088 | ||
|
|
4b45ad3fe7 | ||
|
|
449d8fdbbd | ||
|
|
f44cff9912 | ||
|
|
24730dfde3 | ||
|
|
be6539d41f | ||
|
|
60dd40554e | ||
|
|
837119b6ec | ||
|
|
abe11b82e5 | ||
|
|
4fc1e36d04 | ||
|
|
54aa15e1be | ||
|
|
876d8fc7b3 | ||
|
|
7bdcdaaa5d | ||
|
|
fd44728e89 | ||
|
|
8242520a0b | ||
|
|
2e7e9a942b | ||
|
|
dcc9d5b3ff | ||
|
|
607aab854c | ||
|
|
cc2b2f076b | ||
|
|
90e9723de5 | ||
|
|
ab6787e60b | ||
|
|
698686a45a | ||
|
|
95bdaa1252 | ||
|
|
f0c02e5156 | ||
|
|
a8c50308d0 | ||
|
|
6b359d4c65 | ||
|
|
863caed387 | ||
|
|
e4f17b9bdf | ||
|
|
c2071d8d81 | ||
|
|
4758c2d337 | ||
|
|
91336fe15c | ||
|
|
2c437f3771 | ||
|
|
cb082b20fe | ||
|
|
bce3630686 | ||
|
|
4feb4d066c | ||
|
|
e7d84cd916 | ||
|
|
84e2917694 | ||
|
|
b87464ec40 | ||
|
|
269e0f41a6 | ||
|
|
aefbc4a296 | ||
|
|
c64984dc6a | ||
|
|
24c7ad0ba4 | ||
|
|
643a935852 | ||
|
|
4fcb836b53 | ||
|
|
48ed5f1a2c | ||
|
|
3aa2371eeb | ||
|
|
88f92943c0 | ||
|
|
39710d4ab9 | ||
|
|
93f7cd3cb8 | ||
|
|
69baa3b824 | ||
|
|
bb05e38278 | ||
|
|
554f5a5e73 | ||
|
|
2dcf7772f2 | ||
|
|
3c7012e3c4 | ||
|
|
08e11c3c40 | ||
|
|
31854dab0c | ||
|
|
4f12c3e270 | ||
|
|
3ec43dabce | ||
|
|
0d4f536f71 | ||
|
|
b064a293fb | ||
|
|
d124c29919 | ||
|
|
2e89f1530a | ||
|
|
ef7e2a4b0d | ||
|
|
f680533819 | ||
|
|
a8c977c40e | ||
|
|
1e89b22953 | ||
|
|
11f028e0f3 | ||
|
|
1f7f4a23b5 | ||
|
|
41581d07ec | ||
|
|
dc819a6e1a | ||
|
|
5b51072f1a | ||
|
|
1b10e82cb2 | ||
|
|
eeae1eb0ad | ||
|
|
21c29ca373 | ||
|
|
8bcb72e91b | ||
|
|
8f345fbfbe | ||
|
|
2c88dbca02 | ||
|
|
bd1c629c85 | ||
|
|
f89e6612c4 | ||
|
|
7a0cd0ce19 | ||
|
|
8544affa1e | ||
|
|
48d3588341 | ||
|
|
783f6e4892 | ||
|
|
032cd64daf | ||
|
|
96626e6b00 | ||
|
|
11ae65ec3e | ||
|
|
91e3acb647 | ||
|
|
40c2f5947b | ||
|
|
58b9466cfc | ||
|
|
12210e1768 | ||
|
|
ad2ac457c9 | ||
|
|
634b6e8d2b | ||
|
|
6dd47f4dd0 | ||
|
|
8426f66cfb | ||
|
|
3dfa84a33e | ||
|
|
38f658e253 | ||
|
|
287d99e2b4 | ||
|
|
5cfcde9d4a | ||
|
|
b2730259fc | ||
|
|
61a2d3023c | ||
|
|
239ac7bd1a | ||
|
|
4048314cf7 | ||
|
|
3dbd772847 | ||
|
|
3a08465c35 | ||
|
|
92e3669ee5 | ||
|
|
8b297f4768 | ||
|
|
42b7f1179b | ||
|
|
9d6854c282 | ||
|
|
d943b7deff | ||
|
|
a138da9d9b | ||
|
|
194338a0d2 | ||
|
|
7eb8fdcce1 | ||
|
|
2bf1796340 | ||
|
|
cc42015da2 | ||
|
|
648c39c439 | ||
|
|
8c25b05cda | ||
|
|
780bebc9d1 | ||
|
|
bf7eb7efd5 | ||
|
|
d00d08bae3 | ||
|
|
379256a7fe | ||
|
|
180339269f | ||
|
|
49af17cba8 | ||
|
|
a03a187d2d | ||
|
|
84806e70d7 | ||
|
|
058de5a141 | ||
|
|
d330fbd195 | ||
|
|
f5bc6f664f | ||
|
|
464279807c | ||
|
|
f7e5ef473a | ||
|
|
ff733ef59e | ||
|
|
35b110a472 | ||
|
|
58eabf4754 | ||
|
|
1f273a48a5 | ||
|
|
785a64725d | ||
|
|
7a42e1de73 | ||
|
|
9856ab7b6b | ||
|
|
746ff143d8 | ||
|
|
b0d4d94973 | ||
|
|
c604ad5ef2 | ||
|
|
91c2b324ac | ||
|
|
df59fd7446 | ||
|
|
449fd1f26c | ||
|
|
72d839c6e2 | ||
|
|
ab8c993a81 | ||
|
|
5f5bd913d6 | ||
|
|
92fe4dd55f | ||
|
|
dab95176f0 | ||
|
|
650503ba70 | ||
|
|
61078f214e | ||
|
|
a4b639edfe | ||
|
|
f37878dbb8 | ||
|
|
10fde7c3d6 | ||
|
|
a098fcb48c | ||
|
|
f162977640 | ||
|
|
66cab58d7b | ||
|
|
e99e2d7dcc | ||
|
|
9045a27a5d | ||
|
|
9e1208c72b | ||
|
|
90410f232a | ||
|
|
6769fa7e37 | ||
|
|
72444f5081 | ||
|
|
9a3ca4ffa3 | ||
|
|
f572923720 | ||
|
|
4971f58f44 | ||
|
|
0bf24080a8 | ||
|
|
04a21f71ef | ||
|
|
e22a215863 | ||
|
|
881829e7b2 | ||
|
|
2ad261f3b6 | ||
|
|
e0fdda78b3 | ||
|
|
9c3710c56d | ||
|
|
ddcab46d47 |
+225
@@ -1,5 +1,230 @@
|
|||||||
# Change Log
|
# Change Log
|
||||||
|
|
||||||
|
## 1.69.1
|
||||||
|
* Update translations
|
||||||
|
|
||||||
|
## 1.69.0
|
||||||
|
* Add an option to show crosshairs
|
||||||
|
* Add preference for codes that should not be saved
|
||||||
|
* Add option to restrict searching to a specific format
|
||||||
|
* Pin history items
|
||||||
|
* Improve preference icons
|
||||||
|
* Optimize vector drawables
|
||||||
|
* Fix beeping when phone is set to Vibrate mode
|
||||||
|
|
||||||
|
## 1.68.7
|
||||||
|
* Add icons to preferences screen
|
||||||
|
* Update history listing on resume
|
||||||
|
* Fix quiet zone for recreated barcodes
|
||||||
|
* Fix automatic screen brightening
|
||||||
|
* Migrate to CameraX
|
||||||
|
|
||||||
|
## 1.68.6
|
||||||
|
* Update ZXingCpp
|
||||||
|
* Ask for confirmation before deleting a scan from the result screen
|
||||||
|
|
||||||
|
## 1.68.5
|
||||||
|
* Downgrade ZXingCpp for compatibilty
|
||||||
|
|
||||||
|
## 1.68.4
|
||||||
|
* Fix generating 1D barcodes
|
||||||
|
|
||||||
|
## 1.68.3
|
||||||
|
* Stability improvement
|
||||||
|
|
||||||
|
## 1.68.2
|
||||||
|
* Fix barcode regeneration
|
||||||
|
|
||||||
|
## 1.68.1
|
||||||
|
* Improve UI of search barcode dialog
|
||||||
|
* Stability improvements
|
||||||
|
* Update ZXingCpp
|
||||||
|
* Update Russian translation
|
||||||
|
|
||||||
|
## 1.68.0
|
||||||
|
* Add automated actions
|
||||||
|
* Improve managing profiles
|
||||||
|
|
||||||
|
## 1.67.1
|
||||||
|
* Fix opening file:// Uris
|
||||||
|
* Fix auto-return/immediate opening
|
||||||
|
* Improve Bluetooth stability
|
||||||
|
|
||||||
|
## 1.67.0
|
||||||
|
* Add preference profiles
|
||||||
|
* Support regex when searching for a barcode
|
||||||
|
* Improve feedback when searching for a barcode
|
||||||
|
* Update translations
|
||||||
|
|
||||||
|
## 1.66.2
|
||||||
|
* Remove forcing `?content=` on forwarding URL
|
||||||
|
* Update Spanish translation
|
||||||
|
|
||||||
|
## 1.66.1
|
||||||
|
* Show elements of invalid Wifi configurations too
|
||||||
|
* Update translations
|
||||||
|
|
||||||
|
## 1.66.0
|
||||||
|
* Support deep links to encode content via web links
|
||||||
|
* Parse International Driver Licenses (IDL) in PDF417 barcodes
|
||||||
|
* Add WiFis from system settings
|
||||||
|
* Show parsed data items
|
||||||
|
* Update translations
|
||||||
|
|
||||||
|
## 1.65.2
|
||||||
|
* Sanity check ROI metrics
|
||||||
|
|
||||||
|
## 1.65.1
|
||||||
|
* Improve simple mode
|
||||||
|
* Remember quiet zone settings
|
||||||
|
* Reduce height for 1D barcodes
|
||||||
|
* Add execute argument for binaryeye://encode
|
||||||
|
* Update Russian translation
|
||||||
|
|
||||||
|
## 1.65.0
|
||||||
|
* Add a welcome screen to choose a mode
|
||||||
|
* Add a deep link to encode a barcode
|
||||||
|
* Remember zoom level of generated barcodes
|
||||||
|
* Remove Human Readable Interpretation (HRI) decoration
|
||||||
|
* Update Italian translation
|
||||||
|
|
||||||
|
## 1.64.2
|
||||||
|
* Separate crop window settings for image scanning
|
||||||
|
* Fix initializing dataMask
|
||||||
|
* Update Russian translation
|
||||||
|
|
||||||
|
## 1.64.1
|
||||||
|
* Handle empty input for recreation
|
||||||
|
* Fix setting up database
|
||||||
|
|
||||||
|
## 1.64.0
|
||||||
|
* Find barcode by content
|
||||||
|
* Immediately replace toasts
|
||||||
|
* Use detected barcode for recreation
|
||||||
|
* Show data mask of QR Codes
|
||||||
|
* Fix deep linking
|
||||||
|
* Replace margin with quiet zone switch when generating barcodes
|
||||||
|
* Update Russian translation
|
||||||
|
* Update Portuguese (Brazil) translation
|
||||||
|
|
||||||
|
## 1.63.13
|
||||||
|
* Fix UTF-8 conversion of binary escape sequences when creating barcodes
|
||||||
|
* Fix scan order for intervals under one second
|
||||||
|
* Add arabic translation
|
||||||
|
* Update French translation
|
||||||
|
|
||||||
|
## 1.63.12
|
||||||
|
* Fix magnifying barcode recreation
|
||||||
|
|
||||||
|
## 1.63.11
|
||||||
|
* Update parcel tracking URL
|
||||||
|
* Update multiple translations
|
||||||
|
* Only ask for barcode size when exporting/sharing
|
||||||
|
|
||||||
|
## 1.63.10
|
||||||
|
* Use maximum brightness when showing a generated barcode
|
||||||
|
* Allow file input from share menu
|
||||||
|
* Update multiple translations
|
||||||
|
|
||||||
|
## 1.63.9
|
||||||
|
* Export/share as JPEG
|
||||||
|
* Ignore "URL:" before URLs in barcode content
|
||||||
|
* Fix exporting/sharing multiple history items
|
||||||
|
* Update multiple translations
|
||||||
|
|
||||||
|
## 1.63.8
|
||||||
|
* Add an option to show a checksum for the decoded content
|
||||||
|
* Add support for encoding escape sequences for non-printable characters
|
||||||
|
* Update Italian translation
|
||||||
|
|
||||||
|
## 1.63.7
|
||||||
|
* Update ZXingCpp for Aztec Runes
|
||||||
|
* Update Italian translation
|
||||||
|
|
||||||
|
## 1.63.6
|
||||||
|
* Add history shortcut
|
||||||
|
* Return Codabar start/stop codes
|
||||||
|
* Update French, Russian, Spanish and Ukrainian translations
|
||||||
|
|
||||||
|
## 1.63.5
|
||||||
|
* Add an option to ignore any duplicates
|
||||||
|
* Fix invalid bluetooth connection
|
||||||
|
* Update Russian translation
|
||||||
|
* Update Ukrainian translation
|
||||||
|
|
||||||
|
## 1.63.4
|
||||||
|
* Update Chinese translation
|
||||||
|
* Improve support for old devices
|
||||||
|
|
||||||
|
## 1.63.3
|
||||||
|
* Fix SCAN Intent
|
||||||
|
|
||||||
|
## 1.63.2
|
||||||
|
* Add DX Film Edge barcode format
|
||||||
|
* Update Turkish translation
|
||||||
|
* Update Ukrainian translation
|
||||||
|
|
||||||
|
## 1.63.1
|
||||||
|
* Include scan label in exports/shares
|
||||||
|
* Update Russian translation
|
||||||
|
|
||||||
|
## 1.63.0
|
||||||
|
* Edit scan label in decoding screen
|
||||||
|
* Add a button to add generated codes to the history
|
||||||
|
* Improve scanning from images
|
||||||
|
* Improve asking for camera permission
|
||||||
|
* Fix margin for recreated barcodes
|
||||||
|
* Limit duration of error beep to 1s
|
||||||
|
* Update Ukrainian translation
|
||||||
|
* Update Spanish translation
|
||||||
|
|
||||||
|
## 1.62.3
|
||||||
|
* Extend selection with single taps in history listing
|
||||||
|
* Update Brazilian Portuguese translation
|
||||||
|
|
||||||
|
## 1.62.2
|
||||||
|
* Include rMQR Code for updates
|
||||||
|
|
||||||
|
## 1.62.1
|
||||||
|
* Make rMQR Code selectable
|
||||||
|
|
||||||
|
## 1.62.0
|
||||||
|
* Add support for rMQR (rectangular Micro QR) Code
|
||||||
|
* Share history as a file
|
||||||
|
* Only share selected scans from history
|
||||||
|
|
||||||
|
## 1.61.2
|
||||||
|
* Fix quick settings tile for Android 14
|
||||||
|
* Fix Xiaomi MIUI lockscreen barcode shortcut
|
||||||
|
* Update French translation
|
||||||
|
* Update Czech translation
|
||||||
|
* Update Russian translation
|
||||||
|
|
||||||
|
## 1.61.1
|
||||||
|
* Share scanned contents as a file
|
||||||
|
* Recreate binary barcodes too
|
||||||
|
* Add Bulgarian translation and Brave search engine
|
||||||
|
* Update Russian translation
|
||||||
|
* Fix general format restriction from preferences
|
||||||
|
|
||||||
|
## 1.61.0
|
||||||
|
* Support encoding binary data
|
||||||
|
* Add margin parameter for encoding
|
||||||
|
* Add copying binary data as hex dump
|
||||||
|
* Fix parsing TYPE argument in VCARDs
|
||||||
|
* Update Russian translation
|
||||||
|
* Update Brazilian Portuguese translation
|
||||||
|
* Update Ukrainian translation
|
||||||
|
* Update Chinese translation
|
||||||
|
|
||||||
|
## 1.60.3
|
||||||
|
* Fix saving custom language
|
||||||
|
* Update Brazilian Portuguese translation
|
||||||
|
* Update Chinese translation
|
||||||
|
* Update Czech localisation
|
||||||
|
* Update Russian translation
|
||||||
|
* Update Italian language
|
||||||
|
|
||||||
## 1.60.2
|
## 1.60.2
|
||||||
* Expand escape sequences in encoding input
|
* Expand escape sequences in encoding input
|
||||||
* Update Taiwan translation
|
* Update Taiwan translation
|
||||||
|
|||||||
@@ -16,5 +16,10 @@ to squash when merging into master:
|
|||||||
Then write a [good commit message][commit_messages] to keep the history
|
Then write a [good commit message][commit_messages] to keep the history
|
||||||
meaningful and useful. One feature, one commit.
|
meaningful and useful. One feature, one commit.
|
||||||
|
|
||||||
|
## Translations
|
||||||
|
|
||||||
|
For translations use [Toolate][toolate].
|
||||||
|
|
||||||
[android_best_practices]: https://developer.android.com/distribute/best-practices/develop/
|
[android_best_practices]: https://developer.android.com/distribute/best-practices/develop/
|
||||||
[commit_messages]: https://git-scm.com/book/en/v2/Distributed-Git-Contributing-to-a-Project#_commit_guidelines
|
[commit_messages]: https://git-scm.com/book/en/v2/Distributed-Git-Contributing-to-a-Project#_commit_guidelines
|
||||||
|
[toolate]: https://toolate.othing.xyz/projects/binaryeye/
|
||||||
|
|||||||
@@ -28,11 +28,36 @@ testscan:
|
|||||||
-c android.intent.category.BROWSABLE \
|
-c android.intent.category.BROWSABLE \
|
||||||
-d 'binaryeye://scan/?ret=http%3A%2F%2Fmarkusfisch.de%2F%3Fresult%3D{RESULT}'
|
-d 'binaryeye://scan/?ret=http%3A%2F%2Fmarkusfisch.de%2F%3Fresult%3D{RESULT}'
|
||||||
|
|
||||||
|
testencode:
|
||||||
|
adb shell am start -a android.intent.action.VIEW \
|
||||||
|
-c android.intent.category.BROWSABLE \
|
||||||
|
-d 'binaryeye://encode?content=Test\&format=QR_CODE'
|
||||||
|
|
||||||
|
testencodenotqr:
|
||||||
|
adb shell am start -a android.intent.action.VIEW \
|
||||||
|
-c android.intent.category.BROWSABLE \
|
||||||
|
-d 'binaryeye://encode?content=Test\&format=DATA_MATRIX'
|
||||||
|
|
||||||
|
testencodeexec:
|
||||||
|
adb shell am start -a android.intent.action.VIEW \
|
||||||
|
-c android.intent.category.BROWSABLE \
|
||||||
|
-d 'binaryeye://encode?content=Test\&format=QR_CODE\&execute'
|
||||||
|
|
||||||
|
testencodeurl:
|
||||||
|
adb shell am start -a android.intent.action.VIEW \
|
||||||
|
-c android.intent.category.BROWSABLE \
|
||||||
|
-d 'http://markusfisch.de/encode?content=Test\&format=QR_CODE'
|
||||||
|
|
||||||
testurl:
|
testurl:
|
||||||
adb shell am start -a android.intent.action.VIEW \
|
adb shell am start -a android.intent.action.VIEW \
|
||||||
-c android.intent.category.BROWSABLE \
|
-c android.intent.category.BROWSABLE \
|
||||||
-d 'http://markusfisch.de/BinaryEye?ret=http%3A%2F%2Fmarkusfisch.de%2F%3Fresult%3D{RESULT}'
|
-d 'http://markusfisch.de/BinaryEye?ret=http%3A%2F%2Fmarkusfisch.de%2F%3Fresult%3D{RESULT}'
|
||||||
|
|
||||||
|
testxiaomi:
|
||||||
|
adb shell am start -a android.intent.action.VIEW \
|
||||||
|
-c android.intent.category.BROWSABLE \
|
||||||
|
-d 'market://details?id=com.xiaomi.scanner'
|
||||||
|
|
||||||
install:
|
install:
|
||||||
adb $(TARGET) install -r app/build/outputs/apk/debug/app-debug.apk
|
adb $(TARGET) install -r app/build/outputs/apk/debug/app-debug.apk
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -15,7 +15,7 @@ Before Android Q, this feature requires using
|
|||||||
[WifiManager.getConfiguredNetworks][getConfiguredNetworks]
|
[WifiManager.getConfiguredNetworks][getConfiguredNetworks]
|
||||||
which requires `ACCESS_FINE_LOCATION`.
|
which requires `ACCESS_FINE_LOCATION`.
|
||||||
|
|
||||||
On Android Q an better, this permission is not requested nor required.
|
On Android Q and better, this permission is not requested nor required.
|
||||||
|
|
||||||
### [BLUETOOTH][BLUETOOTH], [BLUETOOTH_ADMIN][BLUETOOTH_ADMIN] and [BLUETOOTH_CONNECT][BLUETOOTH_CONNECT]
|
### [BLUETOOTH][BLUETOOTH], [BLUETOOTH_ADMIN][BLUETOOTH_ADMIN] and [BLUETOOTH_CONNECT][BLUETOOTH_CONNECT]
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
Yet another barcode scanner for Android. As if there weren't [enough][play].
|
Yet another barcode scanner for Android. As if there weren't [enough][play].
|
||||||
|
|
||||||
This one is free, without any ads and open source.
|
This one is free, without any ads, and open source.
|
||||||
|
|
||||||
Works in portrait and landscape orientation, can read inverted codes,
|
Works in portrait and landscape orientation, can read inverted codes,
|
||||||
comes in Material Design and can also generate barcodes.
|
comes in Material Design and can also generate barcodes.
|
||||||
@@ -10,6 +10,23 @@ comes in Material Design and can also generate barcodes.
|
|||||||
Binary Eye uses the [ZXing-C++][zxing_cpp] ("Zebra Crossing") barcode
|
Binary Eye uses the [ZXing-C++][zxing_cpp] ("Zebra Crossing") barcode
|
||||||
scanning library.
|
scanning library.
|
||||||
|
|
||||||
|
If you find this app useful and wish to support its continued development,
|
||||||
|
you can [buy me a coffee](https://www.buymeacoffee.com/markusfisch) or
|
||||||
|
send some Bitcoin decimals to `bc1q2guk2rpll587aymrfadkdtpq32448x5khk5j8z`.
|
||||||
|
|
||||||
|
<a href="https://www.buymeacoffee.com/markusfisch" target="_blank"><img
|
||||||
|
src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png"
|
||||||
|
alt="Buy Me A Coffee"
|
||||||
|
style="height: 60px !important;width: 217px !important;"/></a> <a
|
||||||
|
href="https://liberapay.com/markusfisch/" target="_blank"><img
|
||||||
|
src="https://liberapay.com/assets/widgets/donate.svg"
|
||||||
|
alt="Liberapay"
|
||||||
|
style="height: 60px !important"/></a> <a
|
||||||
|
href="https://ko-fi.com/markusfisch" target="_blank"><img
|
||||||
|
src="https://storage.ko-fi.com/cdn/brandasset/kofi_s_tag_dark.png"
|
||||||
|
alt="Ko-fi"
|
||||||
|
style="height: 60px !important"/></a>
|
||||||
|
|
||||||
## Screenshots
|
## Screenshots
|
||||||
|
|
||||||
<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/screencap-scanning.png"
|
<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/screencap-scanning.png"
|
||||||
@@ -36,6 +53,7 @@ ZXing can read the following barcode formats:
|
|||||||
* [CODE 93][code_93]
|
* [CODE 93][code_93]
|
||||||
* [CODE 128][code_128]
|
* [CODE 128][code_128]
|
||||||
* [DATA MATRIX][data_matrix]
|
* [DATA MATRIX][data_matrix]
|
||||||
|
* [DX FILM EDGE][dx_film_edge]
|
||||||
* [EAN 8][ean_8]
|
* [EAN 8][ean_8]
|
||||||
* [EAN 13][ean_13]
|
* [EAN 13][ean_13]
|
||||||
* [ITF][itf]
|
* [ITF][itf]
|
||||||
@@ -43,6 +61,7 @@ ZXing can read the following barcode formats:
|
|||||||
* [PDF417][pdf417]
|
* [PDF417][pdf417]
|
||||||
* [QR CODE][qr_code]
|
* [QR CODE][qr_code]
|
||||||
* [Micro QR Code][micro_qr_code]
|
* [Micro QR Code][micro_qr_code]
|
||||||
|
* [rMQR Code][rmqr_code]
|
||||||
* [RSS 14][rss]
|
* [RSS 14][rss]
|
||||||
* [RSS EXPANDED][rss]
|
* [RSS EXPANDED][rss]
|
||||||
* [UPC A][upc_a]
|
* [UPC A][upc_a]
|
||||||
@@ -64,14 +83,14 @@ ZXing can generate the following barcode formats:
|
|||||||
* [QR CODE][qr_code]
|
* [QR CODE][qr_code]
|
||||||
* [UPC A][upc_a]
|
* [UPC A][upc_a]
|
||||||
|
|
||||||
## Deep Links and Intents
|
## Deep Links
|
||||||
|
|
||||||
### Deep Links
|
|
||||||
|
|
||||||
You can invoke Binary Eye with a web URI intent from anything that can
|
You can invoke Binary Eye with a web URI intent from anything that can
|
||||||
open URIs. There are two options:
|
open URIs.
|
||||||
|
|
||||||
1. [binaryeye://scan](binaryeye://scan)
|
### Decoding
|
||||||
|
|
||||||
|
1. [binaryeye://scan](binaryeye://scan) (note: GitHub does not render this as clickable link)
|
||||||
2. [http(s)://markusfisch.de/BinaryEye](http://markusfisch.de/BinaryEye)
|
2. [http(s)://markusfisch.de/BinaryEye](http://markusfisch.de/BinaryEye)
|
||||||
|
|
||||||
If you want to get the scanned contents, you can add a `ret` query
|
If you want to get the scanned contents, you can add a `ret` query
|
||||||
@@ -85,10 +104,32 @@ Supported symbols are:
|
|||||||
* `RESULT_BYTES` - raw result as a hex string
|
* `RESULT_BYTES` - raw result as a hex string
|
||||||
* `FORMAT` - barcode format
|
* `FORMAT` - barcode format
|
||||||
|
|
||||||
|
### Encoding
|
||||||
|
|
||||||
|
1. [binaryeye://encode](binaryeye://encode)
|
||||||
|
|
||||||
|
You can use the URL arguments `content` and `format` to automatically
|
||||||
|
preset this data. For example:
|
||||||
|
|
||||||
|
1. [binaryeye://encode?content=Test&format=QR_CODE](binaryeye://encode?content=Test&format=QR_CODE)
|
||||||
|
2. [http(s)://markusfisch.de/encode?content=Test&format=QR_CODE](http://markusfisch.de/encode?content=Test&format=QR_CODE)
|
||||||
|
3. [http(s)://markusfisch.de/encode?content=Test&format=DATA_MATRIX](http://markusfisch.de/encode?content=Test2&format=DATA_MATRIX&execute)
|
||||||
|
|
||||||
|
If you want the code to be generated immediately, add `execute=true` (or
|
||||||
|
just `execute`).
|
||||||
|
|
||||||
|
## Intents
|
||||||
|
|
||||||
|
You can also use Binary Eye from other apps by using an [Intent][intent].
|
||||||
|
|
||||||
|
If you prefer to integrate a barcode scanner into your app, take a look at
|
||||||
|
[BarcodeScannerView][barcode_scanner_view] (if you also want to use
|
||||||
|
[ZXing-C++][zxing_cpp]) or read how to
|
||||||
|
[scan barcodes with ML Kit on Android][mlkit_barcode_scanning].
|
||||||
|
|
||||||
### SCAN Intent
|
### SCAN Intent
|
||||||
|
|
||||||
You can also use Binary Eye from other apps by using the
|
Use the `com.google.zxing.client.android.SCAN` `Intent` with
|
||||||
`com.google.zxing.client.android.SCAN` Intent with
|
|
||||||
[startActivityForResult()][start_activity] like this:
|
[startActivityForResult()][start_activity] like this:
|
||||||
|
|
||||||
```kotlin
|
```kotlin
|
||||||
@@ -120,10 +161,12 @@ If you're using AndroidX, this would be the new,
|
|||||||
[recommended way][intent_result]:
|
[recommended way][intent_result]:
|
||||||
|
|
||||||
```kotlin
|
```kotlin
|
||||||
class YourActivity : Activity() {
|
class YourActivity : AppCompatActivity() {
|
||||||
private val resultLauncher = registerForActivityResult(StartActivityForResult()) { result ->
|
private val resultLauncher = registerForActivityResult(
|
||||||
if (result.resultCode == Activity.RESULT_OK) {
|
ActivityResultContracts.StartActivityForResult()
|
||||||
val scan = result.data?.getStringExtra("SCAN_RESULT")
|
) { result ->
|
||||||
|
if (result.resultCode == RESULT_OK) {
|
||||||
|
val content = result.data?.getStringExtra("SCAN_RESULT")
|
||||||
…
|
…
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -134,6 +177,21 @@ class YourActivity : Activity() {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Forwarding scans via Bluetooth
|
||||||
|
|
||||||
|
**Note:** The companion apps aren't developed nor maintained by the
|
||||||
|
author of Binary Eye.
|
||||||
|
|
||||||
|
In order to set up Bluetooth forwarding to a computer:
|
||||||
|
|
||||||
|
1. Pair your devices (phone and pc) if not paired already
|
||||||
|
2. Download and run a companion app:
|
||||||
|
- Windows: https://github.com/KamaleiZestri/BinaryReceptorWindows
|
||||||
|
- Linux: https://github.com/sean666888/bin_eye_bt_receiver
|
||||||
|
3. In the settings of Binary Eye, enable "Forward scans with Bluetooth"
|
||||||
|
and select the appropriate target host device that is running the
|
||||||
|
companion application.
|
||||||
|
|
||||||
[play]: https://play.google.com/store/search?q=barcode%20scanner&c=apps
|
[play]: https://play.google.com/store/search?q=barcode%20scanner&c=apps
|
||||||
[zxing_cpp]: https://github.com/zxing-cpp/zxing-cpp
|
[zxing_cpp]: https://github.com/zxing-cpp/zxing-cpp
|
||||||
[kotlin]: http://kotlinlang.org/
|
[kotlin]: http://kotlinlang.org/
|
||||||
@@ -143,6 +201,7 @@ class YourActivity : Activity() {
|
|||||||
[code_93]: https://en.wikipedia.org/wiki/Code_93
|
[code_93]: https://en.wikipedia.org/wiki/Code_93
|
||||||
[code_128]: https://en.wikipedia.org/wiki/Code_128
|
[code_128]: https://en.wikipedia.org/wiki/Code_128
|
||||||
[data_matrix]: https://en.wikipedia.org/wiki/Data_Matrix
|
[data_matrix]: https://en.wikipedia.org/wiki/Data_Matrix
|
||||||
|
[dx_film_edge]: https://en.wikipedia.org/wiki/DX_encoding
|
||||||
[ean_8]: https://en.wikipedia.org/wiki/EAN-8
|
[ean_8]: https://en.wikipedia.org/wiki/EAN-8
|
||||||
[ean_13]: https://en.wikipedia.org/wiki/International_Article_Number
|
[ean_13]: https://en.wikipedia.org/wiki/International_Article_Number
|
||||||
[itf]: https://en.wikipedia.org/wiki/Interleaved_2_of_5
|
[itf]: https://en.wikipedia.org/wiki/Interleaved_2_of_5
|
||||||
@@ -150,10 +209,14 @@ class YourActivity : Activity() {
|
|||||||
[pdf417]: https://en.wikipedia.org/wiki/PDF417
|
[pdf417]: https://en.wikipedia.org/wiki/PDF417
|
||||||
[qr_code]: https://en.wikipedia.org/wiki/QR_code
|
[qr_code]: https://en.wikipedia.org/wiki/QR_code
|
||||||
[micro_qr_code]: https://en.wikipedia.org/wiki/QR_code#Micro_QR_code
|
[micro_qr_code]: https://en.wikipedia.org/wiki/QR_code#Micro_QR_code
|
||||||
|
[rmqr_code]: https://www.qrcode.com/en/codes/rmqr.html
|
||||||
[rss]: https://en.wikipedia.org/wiki/GS1_DataBar
|
[rss]: https://en.wikipedia.org/wiki/GS1_DataBar
|
||||||
[upc_a]: https://en.wikipedia.org/wiki/Universal_Product_Code
|
[upc_a]: https://en.wikipedia.org/wiki/Universal_Product_Code
|
||||||
[upc_e]: https://en.wikipedia.org/wiki/Universal_Product_Code#UPC-E
|
[upc_e]: https://en.wikipedia.org/wiki/Universal_Product_Code#UPC-E
|
||||||
[upc_ean]: https://en.wikipedia.org/wiki/Universal_Product_Code#EAN-13
|
[upc_ean]: https://en.wikipedia.org/wiki/Universal_Product_Code#EAN-13
|
||||||
|
[intent]: https://developer.android.com/reference/android/content/Intent
|
||||||
|
[barcode_scanner_view]: https://github.com/markusfisch/BarcodeScannerView
|
||||||
|
[mlkit_barcode_scanning]: https://developers.google.com/ml-kit/vision/barcode-scanning/android
|
||||||
[start_activity]: https://developer.android.com/reference/android/app/Activity#startActivityForResult(android.content.Intent,%20int)
|
[start_activity]: https://developer.android.com/reference/android/app/Activity#startActivityForResult(android.content.Intent,%20int)
|
||||||
[on_activity_result]: https://developer.android.com/reference/android/app/Activity#onActivityResult(int,%20int,%20android.content.Intent)
|
[on_activity_result]: https://developer.android.com/reference/android/app/Activity#onActivityResult(int,%20int,%20android.content.Intent)
|
||||||
[intent_result]: https://developer.android.com/training/basics/intents/result
|
[intent_result]: https://developer.android.com/training/basics/intents/result
|
||||||
|
|||||||
+42
-19
@@ -1,15 +1,15 @@
|
|||||||
apply plugin: 'com.android.application'
|
apply plugin: 'com.android.application'
|
||||||
apply plugin: 'kotlin-android'
|
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace 'de.markusfisch.android.binaryeye'
|
namespace "de.markusfisch.android.binaryeye"
|
||||||
compileSdkVersion sdk_version
|
compileSdk sdk_version
|
||||||
defaultConfig {
|
|
||||||
minSdkVersion 9
|
|
||||||
targetSdkVersion sdk_version
|
|
||||||
|
|
||||||
versionCode 119
|
defaultConfig {
|
||||||
versionName '1.60.2'
|
minSdk 21
|
||||||
|
targetSdk sdk_version
|
||||||
|
|
||||||
|
versionCode 163
|
||||||
|
versionName '1.69.1'
|
||||||
}
|
}
|
||||||
|
|
||||||
signingConfigs {
|
signingConfigs {
|
||||||
@@ -34,13 +34,17 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
release {
|
release {
|
||||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||||
minifyEnabled true
|
minifyEnabled true
|
||||||
shrinkResources true
|
shrinkResources true
|
||||||
signingConfig signingConfigs.release
|
signingConfig signingConfigs.release
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
buildFeatures {
|
||||||
|
buildConfig true
|
||||||
|
}
|
||||||
|
|
||||||
bundle {
|
bundle {
|
||||||
language {
|
language {
|
||||||
// To make the app bundle contain all language resources
|
// To make the app bundle contain all language resources
|
||||||
@@ -52,18 +56,37 @@ android {
|
|||||||
enableSplit = false
|
enableSplit = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Required since Gradle 8.
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||||
|
targetCompatibility = JavaVersion.VERSION_1_8
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
testImplementation 'junit:junit:4.13.2'
|
// Kotlin
|
||||||
|
|
||||||
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
||||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.1"
|
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.10.2"
|
||||||
implementation "com.android.support:appcompat-v7:$support_version"
|
implementation "androidx.core:core-ktx:1.17.0"
|
||||||
implementation "com.android.support:design:$support_version"
|
|
||||||
implementation "com.android.support:preference-v7:$support_version"
|
// Support
|
||||||
implementation "com.android.support:preference-v14:$support_version"
|
implementation "androidx.appcompat:appcompat:1.7.1"
|
||||||
implementation 'com.github.markusfisch:CameraView:1.9.1'
|
implementation "com.google.android.material:material:1.13.0"
|
||||||
implementation 'com.github.markusfisch:ScalingImageView:1.4.1'
|
implementation "androidx.preference:preference:1.2.1"
|
||||||
implementation 'com.github.markusfisch:zxing-cpp:v2.0.0.1'
|
|
||||||
|
// External
|
||||||
|
implementation 'androidx.camera:camera-core:1.4.2'
|
||||||
|
implementation 'androidx.camera:camera-camera2:1.4.2'
|
||||||
|
implementation 'androidx.camera:camera-lifecycle:1.4.2'
|
||||||
|
implementation 'androidx.camera:camera-view:1.4.2'
|
||||||
|
implementation 'com.github.markusfisch:ScalingImageView:1.4.3'
|
||||||
|
implementation 'com.github.markusfisch:zxing-cpp:v3.0.2.1'
|
||||||
|
|
||||||
|
// Testing
|
||||||
|
testImplementation 'junit:junit:4.13.2'
|
||||||
|
testImplementation 'org.robolectric:robolectric:4.16.1'
|
||||||
|
|
||||||
|
// Debugging
|
||||||
|
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.14'
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
Vendored
-2
@@ -3,5 +3,3 @@
|
|||||||
-dontwarn android.support.v8.**
|
-dontwarn android.support.v8.**
|
||||||
|
|
||||||
-keep class android.support.v7.widget.SearchView { *; }
|
-keep class android.support.v7.widget.SearchView { *; }
|
||||||
|
|
||||||
-keep class de.markusfisch.android.zxingcpp.** { *; }
|
|
||||||
|
|||||||
@@ -19,8 +19,9 @@
|
|||||||
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE"/>
|
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE"/>
|
||||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
|
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
|
||||||
<uses-feature android:name="android.hardware.camera"/>
|
<uses-feature android:name="android.hardware.camera"/>
|
||||||
<uses-feature android:name="android.hardware.camera.autofocus"/>
|
<uses-feature android:name="android.hardware.camera.autofocus"
|
||||||
<uses-sdk tools:overrideLibrary="android.support.v14.preference"/>
|
android:required="false" />
|
||||||
|
<uses-sdk tools:overrideLibrary="androidx.preference"/>
|
||||||
<queries>
|
<queries>
|
||||||
<intent>
|
<intent>
|
||||||
<action android:name="android.intent.action.VIEW"/>
|
<action android:name="android.intent.action.VIEW"/>
|
||||||
@@ -62,12 +63,28 @@
|
|||||||
<intent-filter android:label="@string/compose_barcode">
|
<intent-filter android:label="@string/compose_barcode">
|
||||||
<action android:name="android.intent.action.SEND"/>
|
<action android:name="android.intent.action.SEND"/>
|
||||||
<category android:name="android.intent.category.DEFAULT"/>
|
<category android:name="android.intent.category.DEFAULT"/>
|
||||||
<data android:mimeType="text/plain"/>
|
<data android:mimeType="text/*"/>
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="com.google.zxing.client.android.SCAN"/>
|
<action android:name="com.google.zxing.client.android.SCAN"/>
|
||||||
<category android:name="android.intent.category.DEFAULT"/>
|
<category android:name="android.intent.category.DEFAULT"/>
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.VIEW"/>
|
||||||
|
<category android:name="android.intent.category.DEFAULT"/>
|
||||||
|
<category android:name="android.intent.category.BROWSABLE"/>
|
||||||
|
<data android:host="details" android:scheme="market"
|
||||||
|
android:pathSuffix="id=com.xiaomi.scanner"/>
|
||||||
|
</intent-filter>
|
||||||
|
<intent-filter android:autoVerify="true">
|
||||||
|
<action android:name="android.intent.action.VIEW"/>
|
||||||
|
<category android:name="android.intent.category.DEFAULT"/>
|
||||||
|
<category android:name="android.intent.category.BROWSABLE"/>
|
||||||
|
<data android:host="markusfisch.de"/>
|
||||||
|
<data android:pathPrefix="/BinaryEye"/>
|
||||||
|
<data android:scheme="http"/>
|
||||||
|
<data android:scheme="https"/>
|
||||||
|
</intent-filter>
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.VIEW"/>
|
<action android:name="android.intent.action.VIEW"/>
|
||||||
<category android:name="android.intent.category.DEFAULT"/>
|
<category android:name="android.intent.category.DEFAULT"/>
|
||||||
@@ -92,20 +109,64 @@
|
|||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
<activity
|
<activity
|
||||||
android:name=".activity.MainActivity"
|
android:name=".activity.WelcomeActivity"
|
||||||
android:exported="true"/>
|
android:exported="true"/>
|
||||||
|
<activity
|
||||||
|
android:name=".activity.PreferencesActivity"
|
||||||
|
android:exported="true"/>
|
||||||
|
<activity
|
||||||
|
android:name=".activity.HistoryActivity"
|
||||||
|
android:exported="true"/>
|
||||||
|
<activity
|
||||||
|
android:name=".activity.EncodeActivity"
|
||||||
|
android:exported="true">
|
||||||
|
<intent-filter android:autoVerify="true">
|
||||||
|
<action android:name="android.intent.action.VIEW"/>
|
||||||
|
<category android:name="android.intent.category.DEFAULT"/>
|
||||||
|
<category android:name="android.intent.category.BROWSABLE"/>
|
||||||
|
<data android:host="markusfisch.de"/>
|
||||||
|
<data android:pathPrefix="/encode"/>
|
||||||
|
<data android:scheme="http"/>
|
||||||
|
<data android:scheme="https"/>
|
||||||
|
</intent-filter>
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.VIEW"/>
|
||||||
|
<category android:name="android.intent.category.DEFAULT"/>
|
||||||
|
<category android:name="android.intent.category.BROWSABLE"/>
|
||||||
|
<data android:scheme="binaryeye" android:host="encode"/>
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
<activity
|
||||||
|
android:name=".activity.DecodeActivity"
|
||||||
|
android:exported="false"/>
|
||||||
|
<activity
|
||||||
|
android:name=".activity.BarcodeActivity"
|
||||||
|
android:exported="false"/>
|
||||||
|
<activity
|
||||||
|
android:name=".activity.ProfilesActivity"
|
||||||
|
android:exported="false"/>
|
||||||
|
<activity
|
||||||
|
android:name=".activity.AutomatedActionsActivity"
|
||||||
|
android:exported="false"/>
|
||||||
|
<activity
|
||||||
|
android:name=".activity.IgnoreCodesActivity"
|
||||||
|
android:exported="false"/>
|
||||||
|
<activity
|
||||||
|
android:name=".activity.NetworkSuggestionsActivity"
|
||||||
|
android:exported="false"/>
|
||||||
<service
|
<service
|
||||||
android:name=".service.ScanTileService"
|
android:name=".service.ScanTileService"
|
||||||
android:icon="@drawable/ic_action_scan"
|
android:icon="@drawable/ic_action_scan"
|
||||||
android:label="@string/scan_code"
|
android:label="@string/scan_code"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
|
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE"
|
||||||
|
tools:targetApi="n">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.service.quicksettings.action.QS_TILE"/>
|
<action android:name="android.service.quicksettings.action.QS_TILE"/>
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</service>
|
</service>
|
||||||
<provider
|
<provider
|
||||||
android:name="android.support.v4.content.FileProvider"
|
android:name="androidx.core.content.FileProvider"
|
||||||
android:authorities="${applicationId}.provider"
|
android:authorities="${applicationId}.provider"
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
android:grantUriPermissions="true">
|
android:grantUriPermissions="true">
|
||||||
|
|||||||
+11
-9
@@ -6,15 +6,17 @@ import de.markusfisch.android.binaryeye.content.execShareIntent
|
|||||||
import de.markusfisch.android.binaryeye.content.openUrl
|
import de.markusfisch.android.binaryeye.content.openUrl
|
||||||
import de.markusfisch.android.binaryeye.widget.toast
|
import de.markusfisch.android.binaryeye.widget.toast
|
||||||
|
|
||||||
interface IAction {
|
abstract class Action {
|
||||||
val iconResId: Int
|
abstract val iconResId: Int
|
||||||
val titleResId: Int
|
abstract val titleResId: Int
|
||||||
|
|
||||||
fun canExecuteOn(data: ByteArray): Boolean
|
var fired: Boolean = false
|
||||||
suspend fun execute(context: Context, data: ByteArray)
|
|
||||||
|
abstract fun canExecuteOn(data: ByteArray): Boolean
|
||||||
|
abstract suspend fun execute(context: Context, data: ByteArray)
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract class IntentAction : IAction {
|
abstract class IntentAction : Action() {
|
||||||
abstract val errorMsg: Int
|
abstract val errorMsg: Int
|
||||||
|
|
||||||
final override suspend fun execute(context: Context, data: ByteArray) {
|
final override suspend fun execute(context: Context, data: ByteArray) {
|
||||||
@@ -22,7 +24,7 @@ abstract class IntentAction : IAction {
|
|||||||
if (intent == null) {
|
if (intent == null) {
|
||||||
context.toast(errorMsg)
|
context.toast(errorMsg)
|
||||||
} else {
|
} else {
|
||||||
context.execShareIntent(intent)
|
fired = context.execShareIntent(intent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -32,7 +34,7 @@ abstract class IntentAction : IAction {
|
|||||||
): Intent?
|
): Intent?
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract class SchemeAction : IAction {
|
abstract class SchemeAction : Action() {
|
||||||
abstract val scheme: String
|
abstract val scheme: String
|
||||||
open val buildRegex: Boolean = false
|
open val buildRegex: Boolean = false
|
||||||
|
|
||||||
@@ -50,6 +52,6 @@ abstract class SchemeAction : IAction {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final override suspend fun execute(context: Context, data: ByteArray) {
|
final override suspend fun execute(context: Context, data: ByteArray) {
|
||||||
context.openUrl(String(data))
|
fired = context.openUrl(String(data))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -12,9 +12,9 @@ import de.markusfisch.android.binaryeye.actions.web.WebAction
|
|||||||
import de.markusfisch.android.binaryeye.actions.wifi.WifiAction
|
import de.markusfisch.android.binaryeye.actions.wifi.WifiAction
|
||||||
|
|
||||||
object ActionRegistry {
|
object ActionRegistry {
|
||||||
val DEFAULT_ACTION: IAction = OpenOrSearchAction
|
val DEFAULT_ACTION: Action = OpenOrSearchAction
|
||||||
|
|
||||||
private val REGISTRY: Set<IAction> = setOf(
|
private val REGISTRY: Set<Action> = setOf(
|
||||||
MailAction,
|
MailAction,
|
||||||
MatMsgAction,
|
MatMsgAction,
|
||||||
OtpauthAction,
|
OtpauthAction,
|
||||||
@@ -28,7 +28,7 @@ object ActionRegistry {
|
|||||||
WebAction
|
WebAction
|
||||||
)
|
)
|
||||||
|
|
||||||
fun getAction(data: ByteArray): IAction = REGISTRY.find {
|
fun getAction(data: ByteArray): Action = REGISTRY.find {
|
||||||
it.canExecuteOn(data)
|
it.canExecuteOn(data)
|
||||||
} ?: DEFAULT_ACTION
|
} ?: DEFAULT_ACTION
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ package de.markusfisch.android.binaryeye.actions.mail
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import androidx.core.net.toUri
|
||||||
import de.markusfisch.android.binaryeye.R
|
import de.markusfisch.android.binaryeye.R
|
||||||
import de.markusfisch.android.binaryeye.actions.IntentAction
|
import de.markusfisch.android.binaryeye.actions.IntentAction
|
||||||
|
|
||||||
@@ -24,7 +24,7 @@ object MailAction : IntentAction() {
|
|||||||
)?.groupValues?.get(1) ?: return null
|
)?.groupValues?.get(1) ?: return null
|
||||||
return Intent(
|
return Intent(
|
||||||
Intent.ACTION_SENDTO,
|
Intent.ACTION_SENDTO,
|
||||||
Uri.parse("mailto:$mailWithMessage")
|
"mailto:$mailWithMessage".toUri()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ package de.markusfisch.android.binaryeye.actions.mail
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import androidx.core.net.toUri
|
||||||
import de.markusfisch.android.binaryeye.R
|
import de.markusfisch.android.binaryeye.R
|
||||||
import de.markusfisch.android.binaryeye.actions.IntentAction
|
import de.markusfisch.android.binaryeye.actions.IntentAction
|
||||||
|
|
||||||
@@ -26,7 +26,7 @@ object MatMsgAction : IntentAction() {
|
|||||||
}
|
}
|
||||||
return Intent(
|
return Intent(
|
||||||
Intent.ACTION_SENDTO,
|
Intent.ACTION_SENDTO,
|
||||||
Uri.parse("mailto:")
|
"mailto:".toUri()
|
||||||
).apply {
|
).apply {
|
||||||
mm.to?.let {
|
mm.to?.let {
|
||||||
putExtra(Intent.EXTRA_EMAIL, arrayOf(mm.to))
|
putExtra(Intent.EXTRA_EMAIL, arrayOf(mm.to))
|
||||||
|
|||||||
+49
-46
@@ -2,62 +2,65 @@ package de.markusfisch.android.binaryeye.actions.search
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import de.markusfisch.android.binaryeye.R
|
import de.markusfisch.android.binaryeye.R
|
||||||
import de.markusfisch.android.binaryeye.actions.IAction
|
import de.markusfisch.android.binaryeye.actions.Action
|
||||||
import de.markusfisch.android.binaryeye.app.alertDialog
|
import de.markusfisch.android.binaryeye.app.alertDialog
|
||||||
import de.markusfisch.android.binaryeye.app.prefs
|
import de.markusfisch.android.binaryeye.app.prefs
|
||||||
import de.markusfisch.android.binaryeye.content.openUrl
|
import de.markusfisch.android.binaryeye.content.openUrl
|
||||||
import de.markusfisch.android.binaryeye.net.urlEncode
|
import de.markusfisch.android.binaryeye.net.urlEncode
|
||||||
|
|
||||||
object OpenOrSearchAction : IAction {
|
object OpenOrSearchAction : Action() {
|
||||||
override val iconResId: Int = R.drawable.ic_action_search
|
override val iconResId: Int = R.drawable.ic_action_search
|
||||||
override val titleResId: Int = R.string.search_web
|
override val titleResId: Int = R.string.search_web
|
||||||
|
|
||||||
override fun canExecuteOn(data: ByteArray): Boolean = false
|
override fun canExecuteOn(data: ByteArray): Boolean = false
|
||||||
|
|
||||||
override suspend fun execute(context: Context, data: ByteArray) {
|
override suspend fun execute(context: Context, data: ByteArray) {
|
||||||
view(context, String(data), true)
|
fired = openUrlOrSearch(context, String(data))
|
||||||
}
|
}
|
||||||
|
}
|
||||||
private suspend fun view(
|
|
||||||
context: Context,
|
private suspend fun openUrlOrSearch(
|
||||||
url: String,
|
context: Context,
|
||||||
search: Boolean
|
url: String,
|
||||||
) {
|
search: Boolean = true
|
||||||
if (!context.openUrl(url, silent = true) && search) {
|
): Boolean {
|
||||||
openSearch(context, url)
|
if (context.openUrl(url, silent = true)) {
|
||||||
}
|
return true
|
||||||
}
|
}
|
||||||
|
if (search) {
|
||||||
private suspend fun openSearch(context: Context, query: String) {
|
return prependSearchUrl(context, url)?.let {
|
||||||
val defaultSearchUrl = prefs.defaultSearchUrl
|
openUrlOrSearch(context, it, false)
|
||||||
if (defaultSearchUrl.isNotEmpty()) {
|
} ?: false
|
||||||
view(
|
}
|
||||||
context,
|
return false
|
||||||
defaultSearchUrl + query.urlEncode(),
|
}
|
||||||
false
|
|
||||||
)
|
private suspend fun prependSearchUrl(
|
||||||
return
|
context: Context,
|
||||||
}
|
query: String
|
||||||
val names = context.resources.getStringArray(
|
): String? {
|
||||||
R.array.search_engines_names
|
val defaultSearchUrl = prefs.defaultSearchUrl
|
||||||
).toMutableList()
|
if (defaultSearchUrl.isNotEmpty()) {
|
||||||
val urls = context.resources.getStringArray(
|
return defaultSearchUrl + query.urlEncode()
|
||||||
R.array.search_engines_values
|
}
|
||||||
).toMutableList()
|
val names = context.resources.getStringArray(
|
||||||
// Remove the "Always ask" entry. The arrays search_engines_*
|
R.array.search_engines_names
|
||||||
// are used in the preferences too.
|
).toMutableList()
|
||||||
names.removeFirst()
|
val urls = context.resources.getStringArray(
|
||||||
urls.removeFirst()
|
R.array.search_engines_values
|
||||||
if (prefs.openWithUrl.isNotEmpty()) {
|
).toMutableList()
|
||||||
names.add(prefs.openWithUrl)
|
// Remove the "Always ask" entry. The arrays search_engines_*
|
||||||
urls.add(prefs.openWithUrl)
|
// are used in the preferences too.
|
||||||
}
|
names.removeAt(0)
|
||||||
val queryUri = alertDialog<String>(context) { resume ->
|
urls.removeAt(0)
|
||||||
setTitle(R.string.pick_search_engine)
|
if (prefs.openWithUrl.isNotEmpty()) {
|
||||||
setItems(names.toTypedArray()) { _, which ->
|
names.add(prefs.openWithUrl)
|
||||||
resume(urls[which] + query.urlEncode())
|
urls.add(prefs.openWithUrl)
|
||||||
}
|
}
|
||||||
} ?: return
|
return alertDialog(context) { resume ->
|
||||||
view(context, queryUri, false)
|
setTitle(R.string.pick_search_engine)
|
||||||
|
setItems(names.toTypedArray()) { _, which ->
|
||||||
|
resume(urls[which] + query.urlEncode())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ package de.markusfisch.android.binaryeye.actions.sms
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import androidx.core.net.toUri
|
||||||
import de.markusfisch.android.binaryeye.R
|
import de.markusfisch.android.binaryeye.R
|
||||||
import de.markusfisch.android.binaryeye.actions.IntentAction
|
import de.markusfisch.android.binaryeye.actions.IntentAction
|
||||||
|
|
||||||
@@ -27,7 +27,7 @@ object SmsAction : IntentAction() {
|
|||||||
} ?: return null
|
} ?: return null
|
||||||
return Intent(
|
return Intent(
|
||||||
Intent.ACTION_SENDTO,
|
Intent.ACTION_SENDTO,
|
||||||
Uri.parse("smsto:$number")
|
"smsto:$number".toUri()
|
||||||
).apply {
|
).apply {
|
||||||
if (message.isNotEmpty()) {
|
if (message.isNotEmpty()) {
|
||||||
putExtra("sms_body", message)
|
putExtra("sms_body", message)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
package de.markusfisch.android.binaryeye.actions.vtype
|
package de.markusfisch.android.binaryeye.actions.vtype
|
||||||
|
|
||||||
import java.util.*
|
import java.util.Locale
|
||||||
|
|
||||||
object VTypeParser {
|
object VTypeParser {
|
||||||
private const val BACKSLASH_R_LEGACY =
|
private const val BACKSLASH_R_LEGACY =
|
||||||
@@ -39,6 +39,6 @@ data class VTypeProperty(
|
|||||||
} ?: this.info.firstOrNull()
|
} ?: this.info.firstOrNull()
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val typeRegex = """^(?:TYPE=)(.+?)[:;]?.*$""".toRegex(RegexOption.IGNORE_CASE)
|
private val typeRegex = """^TYPE=([A-Z_]+)[:;]?.*$""".toRegex(RegexOption.IGNORE_CASE)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+35
-32
@@ -3,12 +3,11 @@ package de.markusfisch.android.binaryeye.actions.vtype.vcard
|
|||||||
import android.content.ContentValues
|
import android.content.ContentValues
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Build
|
|
||||||
import android.provider.ContactsContract
|
import android.provider.ContactsContract
|
||||||
import de.markusfisch.android.binaryeye.R
|
import de.markusfisch.android.binaryeye.R
|
||||||
import de.markusfisch.android.binaryeye.actions.IntentAction
|
import de.markusfisch.android.binaryeye.actions.IntentAction
|
||||||
import de.markusfisch.android.binaryeye.actions.vtype.VTypeParser
|
import de.markusfisch.android.binaryeye.actions.vtype.VTypeParser
|
||||||
import java.util.*
|
import java.util.Locale
|
||||||
|
|
||||||
object VCardAction : IntentAction() {
|
object VCardAction : IntentAction() {
|
||||||
override val iconResId: Int
|
override val iconResId: Int
|
||||||
@@ -45,10 +44,13 @@ object VCardAction : IntentAction() {
|
|||||||
val (extraPhoneType, extraPhone) = when (index) {
|
val (extraPhoneType, extraPhone) = when (index) {
|
||||||
0 -> ContactsContract.Intents.Insert.PHONE_TYPE to
|
0 -> ContactsContract.Intents.Insert.PHONE_TYPE to
|
||||||
ContactsContract.Intents.Insert.PHONE
|
ContactsContract.Intents.Insert.PHONE
|
||||||
|
|
||||||
1 -> ContactsContract.Intents.Insert.SECONDARY_PHONE_TYPE to
|
1 -> ContactsContract.Intents.Insert.SECONDARY_PHONE_TYPE to
|
||||||
ContactsContract.Intents.Insert.SECONDARY_PHONE
|
ContactsContract.Intents.Insert.SECONDARY_PHONE
|
||||||
|
|
||||||
2 -> ContactsContract.Intents.Insert.TERTIARY_PHONE_TYPE to
|
2 -> ContactsContract.Intents.Insert.TERTIARY_PHONE_TYPE to
|
||||||
ContactsContract.Intents.Insert.TERTIARY_PHONE_TYPE
|
ContactsContract.Intents.Insert.TERTIARY_PHONE_TYPE
|
||||||
|
|
||||||
else -> return@forEachIndexed
|
else -> return@forEachIndexed
|
||||||
}
|
}
|
||||||
phoneProperty.firstTypeOrFirstInfo?.also {
|
phoneProperty.firstTypeOrFirstInfo?.also {
|
||||||
@@ -65,10 +67,13 @@ object VCardAction : IntentAction() {
|
|||||||
val (extraMailType, extraMail) = when (index) {
|
val (extraMailType, extraMail) = when (index) {
|
||||||
0 -> ContactsContract.Intents.Insert.EMAIL_TYPE to
|
0 -> ContactsContract.Intents.Insert.EMAIL_TYPE to
|
||||||
ContactsContract.Intents.Insert.EMAIL
|
ContactsContract.Intents.Insert.EMAIL
|
||||||
|
|
||||||
1 -> ContactsContract.Intents.Insert.SECONDARY_EMAIL_TYPE to
|
1 -> ContactsContract.Intents.Insert.SECONDARY_EMAIL_TYPE to
|
||||||
ContactsContract.Intents.Insert.SECONDARY_EMAIL
|
ContactsContract.Intents.Insert.SECONDARY_EMAIL
|
||||||
|
|
||||||
2 -> ContactsContract.Intents.Insert.TERTIARY_EMAIL_TYPE to
|
2 -> ContactsContract.Intents.Insert.TERTIARY_EMAIL_TYPE to
|
||||||
ContactsContract.Intents.Insert.TERTIARY_EMAIL
|
ContactsContract.Intents.Insert.TERTIARY_EMAIL
|
||||||
|
|
||||||
else -> return@forEachIndexed
|
else -> return@forEachIndexed
|
||||||
}
|
}
|
||||||
mailProperty.firstTypeOrFirstInfo?.also {
|
mailProperty.firstTypeOrFirstInfo?.also {
|
||||||
@@ -92,38 +97,36 @@ object VCardAction : IntentAction() {
|
|||||||
}
|
}
|
||||||
putExtra(ContactsContract.Intents.Insert.POSTAL, addr.value.locationFormat)
|
putExtra(ContactsContract.Intents.Insert.POSTAL, addr.value.locationFormat)
|
||||||
}
|
}
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
|
val urls = info["URL"]?.mapNotNull {
|
||||||
val urls = info["URL"]?.mapNotNull {
|
it.value.takeIf { url -> url.isNotBlank() }?.let { url ->
|
||||||
it.value.takeIf { url -> url.isNotBlank() }?.let { url ->
|
ContentValues(2).apply {
|
||||||
ContentValues(2).apply {
|
put(
|
||||||
put(
|
ContactsContract.Data.MIMETYPE,
|
||||||
ContactsContract.Data.MIMETYPE,
|
ContactsContract.CommonDataKinds.Website.CONTENT_ITEM_TYPE
|
||||||
ContactsContract.CommonDataKinds.Website.CONTENT_ITEM_TYPE
|
)
|
||||||
)
|
put(ContactsContract.CommonDataKinds.Website.URL, url)
|
||||||
put(ContactsContract.CommonDataKinds.Website.URL, url)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val bDays = info["BDAY"]?.mapNotNull {
|
|
||||||
it.value.takeIf { day -> day.isNotBlank() }?.let { day ->
|
|
||||||
ContentValues(3).apply {
|
|
||||||
put(
|
|
||||||
ContactsContract.Data.MIMETYPE,
|
|
||||||
ContactsContract.CommonDataKinds.Event.CONTENT_ITEM_TYPE
|
|
||||||
)
|
|
||||||
put(
|
|
||||||
ContactsContract.CommonDataKinds.Event.TYPE,
|
|
||||||
ContactsContract.CommonDataKinds.Event.TYPE_BIRTHDAY
|
|
||||||
)
|
|
||||||
put(ContactsContract.CommonDataKinds.Event.START_DATE, day)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
putParcelableArrayListExtra(
|
|
||||||
ContactsContract.Intents.Insert.DATA,
|
|
||||||
ArrayList(listOfNotNull(urls, bDays).flatten())
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
val bDays = info["BDAY"]?.mapNotNull {
|
||||||
|
it.value.takeIf { day -> day.isNotBlank() }?.let { day ->
|
||||||
|
ContentValues(3).apply {
|
||||||
|
put(
|
||||||
|
ContactsContract.Data.MIMETYPE,
|
||||||
|
ContactsContract.CommonDataKinds.Event.CONTENT_ITEM_TYPE
|
||||||
|
)
|
||||||
|
put(
|
||||||
|
ContactsContract.CommonDataKinds.Event.TYPE,
|
||||||
|
ContactsContract.CommonDataKinds.Event.TYPE_BIRTHDAY
|
||||||
|
)
|
||||||
|
put(ContactsContract.CommonDataKinds.Event.START_DATE, day)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
putParcelableArrayListExtra(
|
||||||
|
ContactsContract.Intents.Insert.DATA,
|
||||||
|
ArrayList(listOfNotNull(urls, bDays).flatten())
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -184,7 +187,7 @@ object VCardAction : IntentAction() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private val String.addressType: Int?
|
private val String.addressType: Int?
|
||||||
get() = when (this.toUpperCase(Locale.US)) {
|
get() = when (this.uppercase()) {
|
||||||
"HOME" -> ContactsContract.CommonDataKinds.StructuredPostal.TYPE_HOME
|
"HOME" -> ContactsContract.CommonDataKinds.StructuredPostal.TYPE_HOME
|
||||||
"WORK" -> ContactsContract.CommonDataKinds.StructuredPostal.TYPE_WORK
|
"WORK" -> ContactsContract.CommonDataKinds.StructuredPostal.TYPE_WORK
|
||||||
"OTHER" -> ContactsContract.CommonDataKinds.StructuredPostal.TYPE_OTHER
|
"OTHER" -> ContactsContract.CommonDataKinds.StructuredPostal.TYPE_OTHER
|
||||||
|
|||||||
+3
-8
@@ -2,14 +2,13 @@ package de.markusfisch.android.binaryeye.actions.vtype.vevent
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Build
|
|
||||||
import android.provider.CalendarContract
|
import android.provider.CalendarContract
|
||||||
import android.support.annotation.RequiresApi
|
|
||||||
import de.markusfisch.android.binaryeye.R
|
import de.markusfisch.android.binaryeye.R
|
||||||
import de.markusfisch.android.binaryeye.actions.IntentAction
|
import de.markusfisch.android.binaryeye.actions.IntentAction
|
||||||
import de.markusfisch.android.binaryeye.actions.vtype.VTypeParser
|
import de.markusfisch.android.binaryeye.actions.vtype.VTypeParser
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.*
|
import java.util.Date
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
object VEventAction : IntentAction() {
|
object VEventAction : IntentAction() {
|
||||||
override val iconResId: Int
|
override val iconResId: Int
|
||||||
@@ -20,14 +19,10 @@ object VEventAction : IntentAction() {
|
|||||||
get() = R.string.vevent_failed
|
get() = R.string.vevent_failed
|
||||||
|
|
||||||
override fun canExecuteOn(data: ByteArray): Boolean {
|
override fun canExecuteOn(data: ByteArray): Boolean {
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
val type = VTypeParser.parseVType(String(data))
|
val type = VTypeParser.parseVType(String(data))
|
||||||
return type == "VEVENT" || type == "VCALENDAR"
|
return type == "VEVENT" || type == "VCALENDAR"
|
||||||
}
|
}
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
|
|
||||||
override suspend fun createIntent(context: Context, data: ByteArray): Intent? {
|
override suspend fun createIntent(context: Context, data: ByteArray): Intent? {
|
||||||
val info = VTypeParser.parseMap(String(data))
|
val info = VTypeParser.parseMap(String(data))
|
||||||
|
|
||||||
@@ -87,6 +82,6 @@ private fun List<String>.simpleFindParse(date: String): Date? {
|
|||||||
|
|
||||||
private fun SimpleDateFormat.simpleParse(date: String): Date? = try {
|
private fun SimpleDateFormat.simpleParse(date: String): Date? = try {
|
||||||
parse(date)
|
parse(date)
|
||||||
} catch (e: Exception) {
|
} catch (_: Exception) {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,12 +2,12 @@ package de.markusfisch.android.binaryeye.actions.web
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import de.markusfisch.android.binaryeye.R
|
import de.markusfisch.android.binaryeye.R
|
||||||
import de.markusfisch.android.binaryeye.actions.IAction
|
import de.markusfisch.android.binaryeye.actions.Action
|
||||||
import de.markusfisch.android.binaryeye.content.openUrl
|
import de.markusfisch.android.binaryeye.content.openUrl
|
||||||
|
|
||||||
object WebAction : IAction {
|
object WebAction : Action() {
|
||||||
private val colloquialRegex =
|
private val colloquialRegex =
|
||||||
"^(http[s]*://)*[A-Za-z0-9]{3,}\\.[A-Za-z]{2,}[^ \t\r\n]*$".toRegex()
|
"^(URL:[ ]*)*(http[s]*://)*[A-Za-z0-9-]{3,}\\.[A-Za-z]{2,}[^ \t\r\n]*$".toRegex()
|
||||||
|
|
||||||
override val iconResId: Int = R.drawable.ic_action_open
|
override val iconResId: Int = R.drawable.ic_action_open
|
||||||
override val titleResId: Int = R.string.open_url
|
override val titleResId: Int = R.string.open_url
|
||||||
@@ -18,9 +18,12 @@ object WebAction : IAction {
|
|||||||
|
|
||||||
override suspend fun execute(context: Context, data: ByteArray) {
|
override suspend fun execute(context: Context, data: ByteArray) {
|
||||||
var url = String(data).trim()
|
var url = String(data).trim()
|
||||||
|
if (url.startsWith("URL:")) {
|
||||||
|
url = url.substring(4).trim()
|
||||||
|
}
|
||||||
if (!url.startsWith("http") && !url.startsWith("ftp")) {
|
if (!url.startsWith("http") && !url.startsWith("ftp")) {
|
||||||
url = "http://${url}"
|
url = "http://${url}"
|
||||||
}
|
}
|
||||||
context.openUrl(url)
|
fired = context.openUrl(url)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,32 +2,39 @@ package de.markusfisch.android.binaryeye.actions.wifi
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import de.markusfisch.android.binaryeye.R
|
import de.markusfisch.android.binaryeye.R
|
||||||
import de.markusfisch.android.binaryeye.actions.IAction
|
import de.markusfisch.android.binaryeye.actions.Action
|
||||||
import de.markusfisch.android.binaryeye.widget.toast
|
import de.markusfisch.android.binaryeye.widget.toast
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
object WifiAction : IAction {
|
object WifiAction : Action() {
|
||||||
override val iconResId = R.drawable.ic_action_wifi
|
override val iconResId = R.drawable.ic_action_wifi
|
||||||
override val titleResId = R.string.connect_to_wifi
|
override val titleResId = R.string.connect_to_wifi
|
||||||
|
|
||||||
var password: String? = null
|
var password: String? = null
|
||||||
|
|
||||||
override fun canExecuteOn(data: ByteArray): Boolean =
|
override fun canExecuteOn(data: ByteArray): Boolean {
|
||||||
WifiConnector.parse(String(data)) {
|
// Just check if the data can be parsed as the app should also
|
||||||
password = it
|
// show invalid configurations so users can still access them.
|
||||||
} != null
|
val inputMap = WifiConnector.parseMap(String(data)) ?: return false
|
||||||
|
password = inputMap["P"]
|
||||||
|
return !inputMap["S"].isNullOrEmpty()
|
||||||
|
}
|
||||||
|
|
||||||
override suspend fun execute(
|
override suspend fun execute(context: Context, data: ByteArray) {
|
||||||
context: Context,
|
withContext(Dispatchers.IO) {
|
||||||
data: ByteArray
|
fired = WifiConnector.parse(String(data))?.let {
|
||||||
) = withContext(Dispatchers.IO) {
|
WifiConnector.addNetwork(context, it)
|
||||||
val wifiConfig = WifiConnector.parse(
|
} ?: false
|
||||||
String(data)
|
withContext(Dispatchers.Main) {
|
||||||
) ?: return@withContext
|
context.toast(
|
||||||
val message = WifiConnector.addNetwork(context, wifiConfig)
|
if (fired) {
|
||||||
withContext(Dispatchers.Main) {
|
R.string.wifi_added
|
||||||
context.toast(message)
|
} else {
|
||||||
|
R.string.wifi_config_failed
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+80
-60
@@ -1,14 +1,15 @@
|
|||||||
package de.markusfisch.android.binaryeye.actions.wifi
|
package de.markusfisch.android.binaryeye.actions.wifi
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
import android.net.wifi.WifiConfiguration
|
import android.net.wifi.WifiConfiguration
|
||||||
import android.net.wifi.WifiEnterpriseConfig
|
import android.net.wifi.WifiEnterpriseConfig
|
||||||
import android.net.wifi.WifiManager
|
import android.net.wifi.WifiManager
|
||||||
import android.net.wifi.WifiNetworkSuggestion
|
import android.net.wifi.WifiNetworkSuggestion
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.support.annotation.RequiresApi
|
import android.provider.Settings
|
||||||
import de.markusfisch.android.binaryeye.R
|
import androidx.annotation.RequiresApi
|
||||||
import java.util.*
|
import java.util.Locale
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Normal:
|
* Normal:
|
||||||
@@ -35,44 +36,47 @@ import java.util.*
|
|||||||
* The fields can appear in any order. Only "S:" is required.
|
* The fields can appear in any order. Only "S:" is required.
|
||||||
*/
|
*/
|
||||||
object WifiConnector {
|
object WifiConnector {
|
||||||
fun parse(
|
fun parse(input: String): Any? {
|
||||||
input: String,
|
|
||||||
passwordBlock: ((password: String?) -> Unit)? = null
|
|
||||||
): Any? {
|
|
||||||
val inputMap = parseMap(input) ?: return null
|
val inputMap = parseMap(input) ?: return null
|
||||||
val parsedData = SimpleDataAccessor.of(inputMap) ?: return null
|
val parsedData = SimpleDataAccessor.of(inputMap) ?: return null
|
||||||
passwordBlock?.apply {
|
return try {
|
||||||
invoke(parsedData.password)
|
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q) {
|
||||||
}
|
// WifiConfiguration is deprecated in Android Q but
|
||||||
return if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q) {
|
// because it's only possible in R to query network
|
||||||
// WifiConfiguration is deprecated in Android Q but
|
// suggestions, we still use the deprecated API on Q.
|
||||||
// because it's only possible in R to query network
|
@Suppress("DEPRECATION")
|
||||||
// suggestions, we still use the deprecated API on Q.
|
WifiConfiguration().apply(parsedData)
|
||||||
@Suppress("DEPRECATION")
|
} else {
|
||||||
WifiConfiguration().apply(parsedData)
|
WifiNetworkSuggestion.Builder().apply(parsedData)
|
||||||
} else {
|
}
|
||||||
WifiNetworkSuggestion.Builder().apply(parsedData)
|
} catch (_: IllegalArgumentException) {
|
||||||
|
// Some arguments may be out of range, e.g. SSIDs cannot
|
||||||
|
// be longer than 32 characters in some versions of Android.
|
||||||
|
// See: https://github.com/markusfisch/BinaryEye/issues/402
|
||||||
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun addNetwork(context: Context, config: Any): Int {
|
fun addNetwork(context: Context, config: Any): Boolean {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R &&
|
||||||
|
config is WifiNetworkSuggestion.Builder
|
||||||
|
) {
|
||||||
|
context.suggestNetworkFromBuilder(config)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
val wifiManager = context.applicationContext.getSystemService(
|
val wifiManager = context.applicationContext.getSystemService(
|
||||||
Context.WIFI_SERVICE
|
Context.WIFI_SERVICE
|
||||||
) as WifiManager
|
) as WifiManager
|
||||||
// WifiConfiguration is deprecated in Android Q.
|
// WifiConfiguration is deprecated in Android Q.
|
||||||
@Suppress("DEPRECATION")
|
@Suppress("DEPRECATION")
|
||||||
return if ((config is WifiConfiguration &&
|
return (config is WifiConfiguration &&
|
||||||
wifiManager.enableWifi() &&
|
wifiManager.enableWifi() &&
|
||||||
wifiManager.removeOldNetwork(config) &&
|
wifiManager.removeOldNetwork(config) &&
|
||||||
wifiManager.enableNewNetwork(config)) ||
|
wifiManager.enableNewNetwork(config)) ||
|
||||||
(Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q &&
|
(Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q &&
|
||||||
config is WifiNetworkSuggestion.Builder &&
|
config is WifiNetworkSuggestion.Builder &&
|
||||||
wifiManager.addNetworkFromBuilder(config))
|
wifiManager.addNetworkFromBuilder(config))
|
||||||
) {
|
|
||||||
R.string.wifi_added
|
|
||||||
} else {
|
|
||||||
R.string.wifi_config_failed
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun parseMap(input: String): Map<String, String>? {
|
internal fun parseMap(input: String): Map<String, String>? {
|
||||||
@@ -86,7 +90,7 @@ object WifiConnector {
|
|||||||
input.trimAndTerminate()
|
input.trimAndTerminate()
|
||||||
)?.groupValues?.get(1)?.let { pairs ->
|
)?.groupValues?.get(1)?.let { pairs ->
|
||||||
pairRegex.findAll(pairs).map { pair ->
|
pairRegex.findAll(pairs).map { pair ->
|
||||||
pair.groupValues[1].uppercase(Locale.US) to pair.groupValues[2]
|
pair.groupValues[1].uppercase(Locale.US) to pair.groupValues[2].unescape
|
||||||
}.toMap()
|
}.toMap()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -96,47 +100,43 @@ object WifiConnector {
|
|||||||
) {
|
) {
|
||||||
// This should be private but because of testing it isn't possible.
|
// This should be private but because of testing it isn't possible.
|
||||||
internal val ssid: String
|
internal val ssid: String
|
||||||
get() = inputMap.getValue("S").unescape
|
get() = inputMap.getValue("S")
|
||||||
internal val securityType: String
|
internal val securityType: String
|
||||||
get() = inputMap["T"]?.unescape ?: ""
|
get() = inputMap["T"] ?: ""
|
||||||
internal val password: String?
|
internal val password: String?
|
||||||
get() = inputMap["P"]?.unescape
|
get() = inputMap["P"]
|
||||||
internal val hidden: Boolean
|
internal val hidden: Boolean
|
||||||
get() = inputMap["H"]?.unescape == "true"
|
get() = inputMap["H"] == "true"
|
||||||
internal val anonymousIdentity: String
|
internal val anonymousIdentity: String
|
||||||
get() = inputMap["AI"]?.unescape ?: inputMap["A"]?.unescape ?: ""
|
get() = inputMap["AI"] ?: inputMap["A"] ?: ""
|
||||||
internal val identity: String
|
internal val identity: String
|
||||||
get() = inputMap["I"]?.unescape ?: ""
|
get() = inputMap["I"] ?: ""
|
||||||
internal val eapMethod: Int?
|
internal val eapMethod: Int?
|
||||||
get() = if (inputMap["E"].isNullOrEmpty()) {
|
get() = if (inputMap["E"].isNullOrEmpty()) {
|
||||||
requireSdk(Build.VERSION_CODES.JELLY_BEAN_MR2) {
|
WifiEnterpriseConfig.Eap.NONE
|
||||||
WifiEnterpriseConfig.Eap.NONE
|
|
||||||
}
|
|
||||||
} else when (inputMap["E"]?.uppercase()) {
|
} else when (inputMap["E"]?.uppercase()) {
|
||||||
"AKA" -> requireSdk(Build.VERSION_CODES.LOLLIPOP) { WifiEnterpriseConfig.Eap.AKA }
|
"AKA" -> WifiEnterpriseConfig.Eap.AKA
|
||||||
"AKA_PRIME" -> requireSdk(Build.VERSION_CODES.M) { WifiEnterpriseConfig.Eap.AKA_PRIME }
|
"AKA_PRIME" -> requireSdk(Build.VERSION_CODES.M) { WifiEnterpriseConfig.Eap.AKA_PRIME }
|
||||||
"NONE" -> requireSdk(Build.VERSION_CODES.JELLY_BEAN_MR2) { WifiEnterpriseConfig.Eap.NONE }
|
"NONE" -> WifiEnterpriseConfig.Eap.NONE
|
||||||
"PEAP" -> requireSdk(Build.VERSION_CODES.JELLY_BEAN_MR2) { WifiEnterpriseConfig.Eap.PEAP }
|
"PEAP" -> WifiEnterpriseConfig.Eap.PEAP
|
||||||
"PWD" -> requireSdk(Build.VERSION_CODES.JELLY_BEAN_MR2) { WifiEnterpriseConfig.Eap.PWD }
|
"PWD" -> WifiEnterpriseConfig.Eap.PWD
|
||||||
"SIM" -> requireSdk(Build.VERSION_CODES.LOLLIPOP) { WifiEnterpriseConfig.Eap.SIM }
|
"SIM" -> WifiEnterpriseConfig.Eap.SIM
|
||||||
"TLS" -> requireSdk(Build.VERSION_CODES.JELLY_BEAN_MR2) { WifiEnterpriseConfig.Eap.TLS }
|
"TLS" -> WifiEnterpriseConfig.Eap.TLS
|
||||||
"TTLS" -> requireSdk(Build.VERSION_CODES.JELLY_BEAN_MR2) { WifiEnterpriseConfig.Eap.TTLS }
|
"TTLS" -> WifiEnterpriseConfig.Eap.TTLS
|
||||||
"UNAUTH_TLS" -> requireSdk(Build.VERSION_CODES.N) { WifiEnterpriseConfig.Eap.UNAUTH_TLS }
|
"UNAUTH_TLS" -> requireSdk(Build.VERSION_CODES.N) { WifiEnterpriseConfig.Eap.UNAUTH_TLS }
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
internal val phase2Method: Int?
|
internal val phase2Method: Int?
|
||||||
get() = if (inputMap["PH2"].isNullOrEmpty()) {
|
get() = if (inputMap["PH2"].isNullOrEmpty()) {
|
||||||
requireSdk(Build.VERSION_CODES.JELLY_BEAN_MR2) {
|
WifiEnterpriseConfig.Phase2.NONE
|
||||||
WifiEnterpriseConfig.Phase2.NONE
|
|
||||||
}
|
|
||||||
} else when (inputMap["PH2"]?.uppercase()) {
|
} else when (inputMap["PH2"]?.uppercase()) {
|
||||||
"AKA" -> requireSdk(Build.VERSION_CODES.O) { WifiEnterpriseConfig.Phase2.AKA }
|
"AKA" -> requireSdk(Build.VERSION_CODES.O) { WifiEnterpriseConfig.Phase2.AKA }
|
||||||
"AKA_PRIME" -> requireSdk(Build.VERSION_CODES.O) { WifiEnterpriseConfig.Phase2.AKA_PRIME }
|
"AKA_PRIME" -> requireSdk(Build.VERSION_CODES.O) { WifiEnterpriseConfig.Phase2.AKA_PRIME }
|
||||||
"GTC" -> requireSdk(Build.VERSION_CODES.JELLY_BEAN_MR2) { WifiEnterpriseConfig.Phase2.GTC }
|
"GTC" -> WifiEnterpriseConfig.Phase2.GTC
|
||||||
"MSCHAP" -> requireSdk(Build.VERSION_CODES.JELLY_BEAN_MR2) { WifiEnterpriseConfig.Phase2.MSCHAP }
|
"MSCHAP" -> WifiEnterpriseConfig.Phase2.MSCHAP
|
||||||
"MSCHAPV2" -> requireSdk(Build.VERSION_CODES.JELLY_BEAN_MR2) { WifiEnterpriseConfig.Phase2.MSCHAPV2 }
|
"MSCHAPV2" -> WifiEnterpriseConfig.Phase2.MSCHAPV2
|
||||||
"NONE" -> requireSdk(Build.VERSION_CODES.JELLY_BEAN_MR2) { WifiEnterpriseConfig.Phase2.NONE }
|
"NONE" -> WifiEnterpriseConfig.Phase2.NONE
|
||||||
"PAP" -> requireSdk(Build.VERSION_CODES.JELLY_BEAN_MR2) { WifiEnterpriseConfig.Phase2.PAP }
|
"PAP" -> WifiEnterpriseConfig.Phase2.PAP
|
||||||
"SIM" -> requireSdk(Build.VERSION_CODES.O) { WifiEnterpriseConfig.Phase2.SIM }
|
"SIM" -> requireSdk(Build.VERSION_CODES.O) { WifiEnterpriseConfig.Phase2.SIM }
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
@@ -169,6 +169,7 @@ object WifiConnector {
|
|||||||
"WPA", "WPA2" -> {
|
"WPA", "WPA2" -> {
|
||||||
data.password?.let { setWpa2Passphrase(it) }
|
data.password?.let { setWpa2Passphrase(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
"WPA2-EAP" -> {
|
"WPA2-EAP" -> {
|
||||||
setWpa2EnterpriseConfig(WifiEnterpriseConfig().apply {
|
setWpa2EnterpriseConfig(WifiEnterpriseConfig().apply {
|
||||||
identity = data.identity
|
identity = data.identity
|
||||||
@@ -178,9 +179,11 @@ object WifiConnector {
|
|||||||
data.phase2Method?.let { phase2Method = it }
|
data.phase2Method?.let { phase2Method = it }
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
"WPA3" -> {
|
"WPA3" -> {
|
||||||
data.password?.let { setWpa3Passphrase(it) }
|
data.password?.let { setWpa3Passphrase(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
"WPA3-EAP" -> {
|
"WPA3-EAP" -> {
|
||||||
setWpa3EnterpriseConfig(WifiEnterpriseConfig().apply {
|
setWpa3EnterpriseConfig(WifiEnterpriseConfig().apply {
|
||||||
identity = data.identity
|
identity = data.identity
|
||||||
@@ -191,7 +194,7 @@ object WifiConnector {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: IllegalArgumentException) {
|
} catch (_: IllegalArgumentException) {
|
||||||
// Don't accept passphrases that aren't ASCII encodeable.
|
// Don't accept passphrases that aren't ASCII encodeable.
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
@@ -240,6 +243,7 @@ object WifiConnector {
|
|||||||
allowedGroupCiphers.set(WifiConfiguration.GroupCipher.WEP40)
|
allowedGroupCiphers.set(WifiConfiguration.GroupCipher.WEP40)
|
||||||
allowedGroupCiphers.set(WifiConfiguration.GroupCipher.WEP104)
|
allowedGroupCiphers.set(WifiConfiguration.GroupCipher.WEP104)
|
||||||
}
|
}
|
||||||
|
|
||||||
"WPA" -> {
|
"WPA" -> {
|
||||||
passwordWithQuotes?.let {
|
passwordWithQuotes?.let {
|
||||||
preSharedKey = it
|
preSharedKey = it
|
||||||
@@ -256,7 +260,8 @@ object WifiConnector {
|
|||||||
allowedGroupCiphers.set(WifiConfiguration.GroupCipher.TKIP)
|
allowedGroupCiphers.set(WifiConfiguration.GroupCipher.TKIP)
|
||||||
allowedGroupCiphers.set(WifiConfiguration.GroupCipher.CCMP)
|
allowedGroupCiphers.set(WifiConfiguration.GroupCipher.CCMP)
|
||||||
}
|
}
|
||||||
"WPA2-EAP" -> requireSdk(Build.VERSION_CODES.JELLY_BEAN_MR2) {
|
|
||||||
|
"WPA2-EAP" -> {
|
||||||
passwordWithQuotes?.let {
|
passwordWithQuotes?.let {
|
||||||
preSharedKey = it
|
preSharedKey = it
|
||||||
} ?: return null
|
} ?: return null
|
||||||
@@ -276,7 +281,7 @@ object WifiConnector {
|
|||||||
data.eapMethod ?: return null // non valid eapMethod
|
data.eapMethod ?: return null // non valid eapMethod
|
||||||
enterpriseConfig.phase2Method =
|
enterpriseConfig.phase2Method =
|
||||||
data.phase2Method ?: return null // non valid phase2Method
|
data.phase2Method ?: return null // non valid phase2Method
|
||||||
} ?: return null // api isn't high enough
|
}
|
||||||
}
|
}
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
@@ -301,6 +306,21 @@ private fun WifiManager.addNetworkFromBuilder(
|
|||||||
return result == WifiManager.STATUS_NETWORK_SUGGESTIONS_SUCCESS
|
return result == WifiManager.STATUS_NETWORK_SUGGESTIONS_SUCCESS
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.R)
|
||||||
|
private fun Context.suggestNetworkFromBuilder(
|
||||||
|
builder: WifiNetworkSuggestion.Builder
|
||||||
|
) {
|
||||||
|
val suggestion = builder.build()
|
||||||
|
startActivity(
|
||||||
|
Intent(Settings.ACTION_WIFI_ADD_NETWORKS).apply {
|
||||||
|
putParcelableArrayListExtra(
|
||||||
|
Settings.EXTRA_WIFI_NETWORK_LIST,
|
||||||
|
arrayListOf(suggestion)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// Make sure the string ends with a semicolon.
|
// Make sure the string ends with a semicolon.
|
||||||
private fun String.trimAndTerminate(): String {
|
private fun String.trimAndTerminate(): String {
|
||||||
var trimmed = trim()
|
var trimmed = trim()
|
||||||
@@ -354,7 +374,7 @@ private fun WifiManager.removeOldNetwork(
|
|||||||
}?.networkId?.also {
|
}?.networkId?.also {
|
||||||
removeNetwork(it)
|
removeNetwork(it)
|
||||||
}
|
}
|
||||||
} catch (e: SecurityException) {
|
} catch (_: SecurityException) {
|
||||||
// The user didn't allow ACCESS_FINE_LOCATION which is
|
// The user didn't allow ACCESS_FINE_LOCATION which is
|
||||||
// required to access configuredNetworks and that's fine.
|
// required to access configuredNetworks and that's fine.
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
package de.markusfisch.android.binaryeye.activity
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.view.Menu
|
||||||
|
import android.view.MenuInflater
|
||||||
|
import androidx.annotation.LayoutRes
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.core.view.isNotEmpty
|
||||||
|
import de.markusfisch.android.binaryeye.app.PERMISSION_LOCATION
|
||||||
|
import de.markusfisch.android.binaryeye.app.PERMISSION_WRITE
|
||||||
|
import de.markusfisch.android.binaryeye.app.applyLocale
|
||||||
|
import de.markusfisch.android.binaryeye.app.permissionGrantedCallback
|
||||||
|
import de.markusfisch.android.binaryeye.app.prefs
|
||||||
|
import de.markusfisch.android.binaryeye.view.initBars
|
||||||
|
|
||||||
|
abstract class AbstractBaseActivity : AppCompatActivity() {
|
||||||
|
override fun attachBaseContext(base: Context?) {
|
||||||
|
base?.applyLocale(prefs.customLocale)
|
||||||
|
super.attachBaseContext(base)
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun setScreenContentView(@LayoutRes layoutId: Int) {
|
||||||
|
setContentView(layoutId)
|
||||||
|
initBars()
|
||||||
|
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSupportNavigateUp(): Boolean {
|
||||||
|
finish()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onRequestPermissionsResult(
|
||||||
|
requestCode: Int,
|
||||||
|
permissions: Array<String>,
|
||||||
|
grantResults: IntArray
|
||||||
|
) {
|
||||||
|
super.onRequestPermissionsResult(
|
||||||
|
requestCode,
|
||||||
|
permissions,
|
||||||
|
grantResults
|
||||||
|
)
|
||||||
|
when (requestCode) {
|
||||||
|
PERMISSION_LOCATION, PERMISSION_WRITE -> {
|
||||||
|
if (grantResults.isNotEmpty() &&
|
||||||
|
grantResults[0] == PackageManager.PERMISSION_GRANTED
|
||||||
|
) {
|
||||||
|
permissionGrantedCallback?.invoke()
|
||||||
|
permissionGrantedCallback = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||||
|
onCreateOptionsMenu(menu, menuInflater)
|
||||||
|
return menu.isNotEmpty() || super.onCreateOptionsMenu(menu)
|
||||||
|
}
|
||||||
|
|
||||||
|
protected open fun onCreateOptionsMenu(
|
||||||
|
menu: Menu,
|
||||||
|
inflater: MenuInflater
|
||||||
|
) = Unit
|
||||||
|
}
|
||||||
+304
@@ -0,0 +1,304 @@
|
|||||||
|
package de.markusfisch.android.binaryeye.activity
|
||||||
|
|
||||||
|
import android.app.AlertDialog
|
||||||
|
import android.app.Dialog
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.ArrayAdapter
|
||||||
|
import android.widget.EditText
|
||||||
|
import android.widget.ListView
|
||||||
|
import android.widget.RadioGroup
|
||||||
|
import android.widget.Spinner
|
||||||
|
import android.widget.TextView
|
||||||
|
import de.markusfisch.android.binaryeye.R
|
||||||
|
import de.markusfisch.android.binaryeye.adapter.prettifyFormatName
|
||||||
|
import de.markusfisch.android.binaryeye.app.prefs
|
||||||
|
import de.markusfisch.android.binaryeye.automation.AutomatedAction
|
||||||
|
import de.markusfisch.android.binaryeye.view.setPaddingFromWindowInsets
|
||||||
|
import de.markusfisch.android.binaryeye.view.systemBarListViewScrollListener
|
||||||
|
|
||||||
|
class AutomatedActionsActivity : AbstractBaseActivity() {
|
||||||
|
private val actions = ArrayList<AutomatedAction>()
|
||||||
|
|
||||||
|
private lateinit var adapter: ActionsAdapter
|
||||||
|
|
||||||
|
override fun onCreate(state: Bundle?) {
|
||||||
|
super.onCreate(state)
|
||||||
|
setScreenContentView(R.layout.activity_automated_actions)
|
||||||
|
setTitle(R.string.automated_actions)
|
||||||
|
|
||||||
|
actions.clear()
|
||||||
|
actions.addAll(prefs.automatedActions)
|
||||||
|
|
||||||
|
val listView = findViewById<ListView>(R.id.actions)
|
||||||
|
listView.emptyView = findViewById(R.id.no_actions)
|
||||||
|
adapter = ActionsAdapter(this, actions)
|
||||||
|
listView.adapter = adapter
|
||||||
|
listView.setOnScrollListener(systemBarListViewScrollListener)
|
||||||
|
listView.setOnItemClickListener { _, _, position, _ ->
|
||||||
|
editAction(position)
|
||||||
|
}
|
||||||
|
listView.setOnItemLongClickListener { _, _, position, _ ->
|
||||||
|
confirmRemoveAction(position)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
findViewById<View>(R.id.add).setOnClickListener {
|
||||||
|
editAction(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
findViewById<View>(R.id.inset_layout).setPaddingFromWindowInsets()
|
||||||
|
listView.setPaddingFromWindowInsets()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun editAction(position: Int?) {
|
||||||
|
val action = position?.let { actions[it] }
|
||||||
|
val view = layoutInflater.inflate(
|
||||||
|
R.layout.dialog_automated_action,
|
||||||
|
null
|
||||||
|
)
|
||||||
|
val regexView = view.findViewById<EditText>(R.id.regex)
|
||||||
|
val formatView = view.findViewById<Spinner>(R.id.format)
|
||||||
|
val urlView = view.findViewById<EditText>(R.id.url_template)
|
||||||
|
val typeGroup = view.findViewById<RadioGroup>(R.id.action_type)
|
||||||
|
val urlSection = view.findViewById<View>(R.id.url_section)
|
||||||
|
val intentSection = view.findViewById<View>(R.id.intent_section)
|
||||||
|
val intentUriView = view.findViewById<EditText>(
|
||||||
|
R.id.intent_uri_template
|
||||||
|
)
|
||||||
|
val names = resources.getStringArray(
|
||||||
|
R.array.barcode_formats_names
|
||||||
|
).toMutableList().apply {
|
||||||
|
add(0, getString(R.string.all_formats))
|
||||||
|
}
|
||||||
|
val values = resources.getStringArray(
|
||||||
|
R.array.barcode_formats_values
|
||||||
|
).toMutableList().apply {
|
||||||
|
add(0, "")
|
||||||
|
}
|
||||||
|
formatView.adapter = ArrayAdapter(
|
||||||
|
this,
|
||||||
|
android.R.layout.simple_spinner_item,
|
||||||
|
names
|
||||||
|
).apply {
|
||||||
|
setDropDownViewResource(
|
||||||
|
android.R.layout.simple_spinner_dropdown_item
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action != null) {
|
||||||
|
regexView.setText(action.pattern)
|
||||||
|
formatView.setSelection(
|
||||||
|
values.indexOf(action.format ?: "").coerceAtLeast(0)
|
||||||
|
)
|
||||||
|
when (action.type) {
|
||||||
|
AutomatedAction.Type.Intent -> {
|
||||||
|
urlView.setText(action.urlTemplate ?: "")
|
||||||
|
}
|
||||||
|
|
||||||
|
AutomatedAction.Type.CustomIntent -> {
|
||||||
|
intentUriView.setText(action.intentUriTemplate ?: "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
formatView.setSelection(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateTypeSections(checkedId: Int) {
|
||||||
|
val isIntent = checkedId == R.id.action_type_intent
|
||||||
|
urlSection.visibility = if (isIntent) View.VISIBLE else View.GONE
|
||||||
|
intentSection.visibility = if (isIntent) View.GONE else View.VISIBLE
|
||||||
|
}
|
||||||
|
|
||||||
|
typeGroup.setOnCheckedChangeListener { _, checkedId ->
|
||||||
|
updateTypeSections(checkedId)
|
||||||
|
}
|
||||||
|
val initialTypeId = if (
|
||||||
|
action?.type == AutomatedAction.Type.CustomIntent
|
||||||
|
) {
|
||||||
|
R.id.action_type_custom_intent
|
||||||
|
} else {
|
||||||
|
R.id.action_type_intent
|
||||||
|
}
|
||||||
|
typeGroup.check(initialTypeId)
|
||||||
|
updateTypeSections(initialTypeId)
|
||||||
|
|
||||||
|
val dialog = AlertDialog.Builder(this)
|
||||||
|
.setTitle(
|
||||||
|
if (action == null) {
|
||||||
|
R.string.automated_action_add
|
||||||
|
} else {
|
||||||
|
R.string.automated_action_edit
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.setView(view)
|
||||||
|
.setPositiveButton(android.R.string.ok, null)
|
||||||
|
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
||||||
|
.create()
|
||||||
|
|
||||||
|
dialog.setOnShowListener {
|
||||||
|
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener {
|
||||||
|
addOrUpdateAction(
|
||||||
|
position,
|
||||||
|
regexView,
|
||||||
|
formatView,
|
||||||
|
values,
|
||||||
|
urlView,
|
||||||
|
typeGroup,
|
||||||
|
intentUriView,
|
||||||
|
dialog
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dialog.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun addOrUpdateAction(
|
||||||
|
position: Int?,
|
||||||
|
regexView: EditText,
|
||||||
|
formatView: Spinner,
|
||||||
|
values: List<String>,
|
||||||
|
urlView: EditText,
|
||||||
|
typeGroup: RadioGroup,
|
||||||
|
intentUriView: EditText,
|
||||||
|
dialog: Dialog,
|
||||||
|
) {
|
||||||
|
val pattern = regexView.text.toString().trim()
|
||||||
|
if (pattern.isEmpty()) {
|
||||||
|
regexView.error = getString(R.string.automated_action_regex_required)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
Regex(pattern)
|
||||||
|
} catch (_: Exception) {
|
||||||
|
regexView.error = getString(R.string.automated_action_regex_invalid)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val isIntent = typeGroup.checkedRadioButtonId == R.id.action_type_intent
|
||||||
|
val format = formatView.selectedItemPosition.let {
|
||||||
|
values[it].ifEmpty { null }
|
||||||
|
}
|
||||||
|
val newAction = if (isIntent) {
|
||||||
|
val urlTemplate = urlView.text.toString().trim()
|
||||||
|
if (urlTemplate.isEmpty()) {
|
||||||
|
urlView.error = getString(R.string.automated_action_url_required)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
AutomatedAction(
|
||||||
|
pattern = pattern,
|
||||||
|
type = AutomatedAction.Type.Intent,
|
||||||
|
format = format,
|
||||||
|
urlTemplate = urlTemplate
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
val intentUriTemplate = intentUriView.text.toString().trim()
|
||||||
|
if (intentUriTemplate.isEmpty()) {
|
||||||
|
intentUriView.error = getString(
|
||||||
|
R.string.automated_action_intent_uri_required
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!intentUriTemplate.startsWith(
|
||||||
|
"intent:",
|
||||||
|
ignoreCase = true
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
intentUriView.error = getString(
|
||||||
|
R.string.automated_action_intent_uri_invalid
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
AutomatedAction(
|
||||||
|
pattern = pattern,
|
||||||
|
type = AutomatedAction.Type.CustomIntent,
|
||||||
|
format = format,
|
||||||
|
intentUriTemplate = intentUriTemplate
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (position == null) {
|
||||||
|
actions.add(newAction)
|
||||||
|
} else {
|
||||||
|
actions[position] = newAction
|
||||||
|
}
|
||||||
|
prefs.setAutomatedActions(actions)
|
||||||
|
adapter.notifyDataSetChanged()
|
||||||
|
dialog.dismiss()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun confirmRemoveAction(position: Int) {
|
||||||
|
AlertDialog.Builder(this)
|
||||||
|
.setMessage(R.string.automated_action_remove_confirm)
|
||||||
|
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||||
|
actions.removeAt(position)
|
||||||
|
prefs.setAutomatedActions(actions)
|
||||||
|
adapter.notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
private class ActionsAdapter(
|
||||||
|
context: Context,
|
||||||
|
private val items: List<AutomatedAction>
|
||||||
|
) : ArrayAdapter<AutomatedAction>(
|
||||||
|
context,
|
||||||
|
android.R.layout.simple_list_item_2,
|
||||||
|
android.R.id.text1,
|
||||||
|
items
|
||||||
|
) {
|
||||||
|
override fun getView(
|
||||||
|
position: Int,
|
||||||
|
convertView: View?,
|
||||||
|
parent: ViewGroup
|
||||||
|
): View {
|
||||||
|
val view = super.getView(position, convertView, parent)
|
||||||
|
val action = items[position]
|
||||||
|
getViewHolder(view).apply {
|
||||||
|
title.text = action.pattern
|
||||||
|
subtitle.text = when (action.type) {
|
||||||
|
AutomatedAction.Type.Intent -> if (!action.urlTemplate.isNullOrEmpty()) {
|
||||||
|
context.getString(
|
||||||
|
R.string.automated_action_list_intent,
|
||||||
|
action.urlTemplate
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
context.getString(R.string.automated_action_list_intent_empty)
|
||||||
|
}
|
||||||
|
|
||||||
|
AutomatedAction.Type.CustomIntent ->
|
||||||
|
if (!action.intentUriTemplate.isNullOrEmpty()) {
|
||||||
|
context.getString(
|
||||||
|
R.string.automated_action_list_custom_intent,
|
||||||
|
action.intentUriTemplate
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
context.getString(
|
||||||
|
R.string.automated_action_list_custom_intent_empty
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
action.format?.let { format ->
|
||||||
|
subtitle.text = "${subtitle.text} (${format.prettifyFormatName()})"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getViewHolder(
|
||||||
|
view: View
|
||||||
|
): ViewHolder = view.tag as ViewHolder? ?: ViewHolder(
|
||||||
|
view.findViewById(android.R.id.text1),
|
||||||
|
view.findViewById(android.R.id.text2),
|
||||||
|
).also {
|
||||||
|
view.tag = it
|
||||||
|
}
|
||||||
|
|
||||||
|
private data class ViewHolder(
|
||||||
|
val title: TextView,
|
||||||
|
val subtitle: TextView,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,594 @@
|
|||||||
|
package de.markusfisch.android.binaryeye.activity
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.app.AlertDialog
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.Matrix
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.Menu
|
||||||
|
import android.view.MenuInflater
|
||||||
|
import android.view.MenuItem
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.EditText
|
||||||
|
import android.widget.ImageView
|
||||||
|
import android.widget.SeekBar
|
||||||
|
import android.widget.TextView
|
||||||
|
import de.markusfisch.android.binaryeye.R
|
||||||
|
import de.markusfisch.android.binaryeye.app.db
|
||||||
|
import de.markusfisch.android.binaryeye.app.hasWritePermission
|
||||||
|
import de.markusfisch.android.binaryeye.app.prefs
|
||||||
|
import de.markusfisch.android.binaryeye.content.Barcode
|
||||||
|
import de.markusfisch.android.binaryeye.content.BarcodeColors
|
||||||
|
import de.markusfisch.android.binaryeye.content.BitMatrixBarcode
|
||||||
|
import de.markusfisch.android.binaryeye.content.ContentBarcode
|
||||||
|
import de.markusfisch.android.binaryeye.content.copyToClipboard
|
||||||
|
import de.markusfisch.android.binaryeye.content.shareFile
|
||||||
|
import de.markusfisch.android.binaryeye.content.shareText
|
||||||
|
import de.markusfisch.android.binaryeye.database.toScan
|
||||||
|
import de.markusfisch.android.binaryeye.io.addSuffixIfNotGiven
|
||||||
|
import de.markusfisch.android.binaryeye.io.toSaveResult
|
||||||
|
import de.markusfisch.android.binaryeye.io.writeExternalFile
|
||||||
|
import de.markusfisch.android.binaryeye.net.createEncodeDeeplink
|
||||||
|
import de.markusfisch.android.binaryeye.os.getScreenBrightness
|
||||||
|
import de.markusfisch.android.binaryeye.os.setScreenBrightness
|
||||||
|
import de.markusfisch.android.binaryeye.view.doOnApplyWindowInsets
|
||||||
|
import de.markusfisch.android.binaryeye.view.setPaddingFromWindowInsets
|
||||||
|
import de.markusfisch.android.binaryeye.widget.ConfinedScalingImageView
|
||||||
|
import de.markusfisch.android.binaryeye.widget.toast
|
||||||
|
import de.markusfisch.android.zxingcpp.ZxingCpp
|
||||||
|
import de.markusfisch.android.zxingcpp.ZxingCpp.BarcodeFormat
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import java.io.File
|
||||||
|
import java.io.FileOutputStream
|
||||||
|
import java.io.IOException
|
||||||
|
import java.io.OutputStream
|
||||||
|
import java.util.Locale
|
||||||
|
import kotlin.math.max
|
||||||
|
import kotlin.math.min
|
||||||
|
|
||||||
|
class BarcodeActivity : AbstractBaseActivity() {
|
||||||
|
private enum class FileType {
|
||||||
|
PNG, JPG, SVG, TXT
|
||||||
|
}
|
||||||
|
|
||||||
|
private val parentJob = Job()
|
||||||
|
private val scope = CoroutineScope(Dispatchers.IO + parentJob)
|
||||||
|
|
||||||
|
private lateinit var imageView: ConfinedScalingImageView
|
||||||
|
private lateinit var barcode: Barcode<*>
|
||||||
|
|
||||||
|
private var contentView: View? = null
|
||||||
|
private var addToHistoryItem: MenuItem? = null
|
||||||
|
private var brightenScreenItem: MenuItem? = null
|
||||||
|
private var currentBrightness = -1f
|
||||||
|
|
||||||
|
override fun onCreate(state: Bundle?) {
|
||||||
|
super.onCreate(state)
|
||||||
|
setScreenContentView(R.layout.activity_barcode)
|
||||||
|
setTitle(R.string.view_barcode)
|
||||||
|
contentView = findViewById(android.R.id.content)
|
||||||
|
|
||||||
|
// Make `imageView` available for `onPause()`.
|
||||||
|
imageView = findViewById(R.id.barcode)
|
||||||
|
|
||||||
|
val bitmap: Bitmap
|
||||||
|
try {
|
||||||
|
barcode = intent?.extras?.toBarcode() ?: throw IllegalArgumentException(
|
||||||
|
"Illegal arguments"
|
||||||
|
)
|
||||||
|
// Catch exceptions from encoding.
|
||||||
|
bitmap = barcode.bitmap()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
var message = e.message
|
||||||
|
if (message.isNullOrEmpty()) {
|
||||||
|
message = getString(R.string.error_encoding_barcode)
|
||||||
|
}
|
||||||
|
toast(message)
|
||||||
|
finish()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
imageView.setImageBitmap(bitmap)
|
||||||
|
imageView.runAfterLayout = {
|
||||||
|
val scale = prefs.previewScale
|
||||||
|
if (scale > 0) {
|
||||||
|
imageView.setScale(
|
||||||
|
scale,
|
||||||
|
bitmap.width / 2f,
|
||||||
|
bitmap.height / 2f
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
imageView.minWidth = min(
|
||||||
|
imageView.minWidth / 2f,
|
||||||
|
max(bitmap.width, bitmap.height).toFloat()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
findViewById<View>(R.id.share).setOnClickListener {
|
||||||
|
it.context?.apply {
|
||||||
|
pickFileType(R.string.share_as) { fileType ->
|
||||||
|
shareAs(fileType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
findViewById<View>(R.id.inset_layout).setPaddingFromWindowInsets()
|
||||||
|
imageView.doOnApplyWindowInsets { _, insets ->
|
||||||
|
imageView.insets.set(insets)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Bundle.toBarcode(): Barcode<*> {
|
||||||
|
val content =
|
||||||
|
getString(CONTENT_TEXT) ?: getByteArray(CONTENT_RAW) ?: throw IllegalArgumentException(
|
||||||
|
"content cannot be null"
|
||||||
|
)
|
||||||
|
val barcodeFormat = BarcodeFormat.valueOf(
|
||||||
|
getString(FORMAT) ?: throw IllegalArgumentException(
|
||||||
|
"format cannot be null"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
val colors = BarcodeColors.entries[getInt(COLORS)]
|
||||||
|
val data = getByteArray(BIT_MATRIX_DATA)
|
||||||
|
return if (data != null) {
|
||||||
|
BitMatrixBarcode(
|
||||||
|
ZxingCpp.BitMatrix(
|
||||||
|
getInt(BIT_MATRIX_WIDTH),
|
||||||
|
getInt(BIT_MATRIX_HEIGHT),
|
||||||
|
data
|
||||||
|
),
|
||||||
|
content,
|
||||||
|
barcodeFormat,
|
||||||
|
colors
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
ContentBarcode(
|
||||||
|
content,
|
||||||
|
barcodeFormat,
|
||||||
|
getInt(EC_LEVEL),
|
||||||
|
getInt(MARGIN),
|
||||||
|
colors
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
if (prefs.brightenScreen) {
|
||||||
|
// Post to make sure brightenScreenItem is initialized.
|
||||||
|
contentView?.postDelayed({
|
||||||
|
brightenScreen()
|
||||||
|
}, 500)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPause() {
|
||||||
|
super.onPause()
|
||||||
|
if (currentBrightness > -1f) {
|
||||||
|
restoreScreenBrightness()
|
||||||
|
}
|
||||||
|
if (imageView.isInBounds()) {
|
||||||
|
prefs.previewScale = imageView.getScale()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
super.onDestroy()
|
||||||
|
parentJob.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||||
|
inflater.inflate(R.menu.fragment_barcode, menu)
|
||||||
|
addToHistoryItem = menu.findItem(R.id.add_to_history)
|
||||||
|
brightenScreenItem = menu.findItem(R.id.brighten_screen)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||||
|
return when (item.itemId) {
|
||||||
|
R.id.add_to_history -> {
|
||||||
|
readAndAddToHistory(
|
||||||
|
barcode.bitmap(),
|
||||||
|
barcode.format
|
||||||
|
)
|
||||||
|
addToHistoryItem?.isVisible = false
|
||||||
|
toast(R.string.added_to_history)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
R.id.copy_to_clipboard -> {
|
||||||
|
copyToClipboard(barcode.text())
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
R.id.copy_as_deeplink -> {
|
||||||
|
copyToClipboard(deeplinkToCopy())
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
R.id.export_to_file -> {
|
||||||
|
pickFileType(R.string.export_as) { fileType ->
|
||||||
|
when (fileType) {
|
||||||
|
FileType.PNG,
|
||||||
|
FileType.JPG -> askForSize { size ->
|
||||||
|
askForFileNameAndSave(fileType, size)
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> askForFileNameAndSave(fileType, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
R.id.brighten_screen -> {
|
||||||
|
prefs.brightenScreen = if (currentBrightness > -1f) {
|
||||||
|
restoreScreenBrightness()
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
brightenScreen()
|
||||||
|
true
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> super.onOptionsItemSelected(item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun copyToClipboard(text: String, isSensitive: Boolean = false) {
|
||||||
|
(this as android.content.Context).copyToClipboard(text, isSensitive)
|
||||||
|
toast(R.string.copied_to_clipboard)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun deeplinkToCopy() = createEncodeDeeplink(
|
||||||
|
format = barcode.format.name,
|
||||||
|
content = barcode.textOrHex(),
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun Context.pickFileType(
|
||||||
|
title: Int,
|
||||||
|
action: (FileType) -> Unit
|
||||||
|
) {
|
||||||
|
val fileTypes = FileType.entries.toTypedArray()
|
||||||
|
AlertDialog.Builder(this)
|
||||||
|
.setTitle(title)
|
||||||
|
.setItems(fileTypes.map { it.name }.toTypedArray()) { _, which ->
|
||||||
|
action(fileTypes[which])
|
||||||
|
}
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dialogs do not have a parent view.
|
||||||
|
@SuppressLint("InflateParams")
|
||||||
|
private fun askForFileNameAndSave(fileType: FileType, bitmapSize: Int) {
|
||||||
|
// Write permission is only required before Android Q.
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q &&
|
||||||
|
!hasWritePermission {
|
||||||
|
askForFileNameAndSave(fileType, bitmapSize)
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val view = layoutInflater.inflate(R.layout.dialog_save_file, null)
|
||||||
|
val editText = view.findViewById<EditText>(R.id.file_name)
|
||||||
|
editText.setText(
|
||||||
|
encodeFileName("${barcode.format}_${barcode.content}")
|
||||||
|
)
|
||||||
|
AlertDialog.Builder(this)
|
||||||
|
.setView(view)
|
||||||
|
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||||
|
val fileName = editText.text.toString()
|
||||||
|
when (fileType) {
|
||||||
|
FileType.PNG -> saveAs(
|
||||||
|
addSuffixIfNotGiven(fileName, ".png"),
|
||||||
|
MIME_PNG
|
||||||
|
) {
|
||||||
|
barcode.bitmap(bitmapSize).saveAsPng(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
FileType.JPG -> saveAs(
|
||||||
|
addSuffixIfNotGiven(fileName, ".jpg"),
|
||||||
|
MIME_JPG
|
||||||
|
) {
|
||||||
|
barcode.bitmap(bitmapSize).saveAsJpg(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
FileType.SVG -> saveAs(
|
||||||
|
addSuffixIfNotGiven(fileName, ".svg"),
|
||||||
|
MIME_SVG
|
||||||
|
) {
|
||||||
|
it.write(barcode.svg().toByteArray())
|
||||||
|
}
|
||||||
|
|
||||||
|
FileType.TXT -> saveAs(
|
||||||
|
addSuffixIfNotGiven(fileName, ".txt"),
|
||||||
|
MIME_TXT
|
||||||
|
) {
|
||||||
|
it.write(barcode.text().toByteArray())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||||
|
}
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dialogs do not have a parent view.
|
||||||
|
@SuppressLint("InflateParams")
|
||||||
|
private fun askForSize(write: (size: Int) -> Unit) {
|
||||||
|
val view = layoutInflater.inflate(R.layout.dialog_size, null)
|
||||||
|
val sizeView = view.findViewById<TextView>(R.id.size_display)
|
||||||
|
val sizeBarView = view.findViewById<SeekBar>(R.id.size_bar)
|
||||||
|
sizeBarView.initSizeBar(sizeView)
|
||||||
|
AlertDialog.Builder(this)
|
||||||
|
.setView(view)
|
||||||
|
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||||
|
write(getSize(sizeBarView.progress))
|
||||||
|
}
|
||||||
|
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||||
|
}
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun SeekBar.initSizeBar(sizeView: TextView) {
|
||||||
|
sizeView.updateSize(progress)
|
||||||
|
setOnSeekBarChangeListener(
|
||||||
|
object : SeekBar.OnSeekBarChangeListener {
|
||||||
|
override fun onProgressChanged(
|
||||||
|
seekBar: SeekBar,
|
||||||
|
progressValue: Int,
|
||||||
|
fromUser: Boolean
|
||||||
|
) {
|
||||||
|
sizeView.updateSize(progressValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStartTrackingTouch(seekBar: SeekBar) {}
|
||||||
|
|
||||||
|
override fun onStopTrackingTouch(seekBar: SeekBar) {}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun TextView.updateSize(power: Int) {
|
||||||
|
val size = getSize(power)
|
||||||
|
text = if (size > 0) {
|
||||||
|
getString(R.string.size_width_by_height, size, size)
|
||||||
|
} else {
|
||||||
|
getString(R.string.size_no_magnification)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun saveAs(
|
||||||
|
fileName: String,
|
||||||
|
mimeType: String,
|
||||||
|
write: (outputStream: OutputStream) -> Unit
|
||||||
|
) {
|
||||||
|
scope.launch(Dispatchers.IO) {
|
||||||
|
val message = writeExternalFile(
|
||||||
|
fileName, mimeType, write
|
||||||
|
).toSaveResult()
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
toast(message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Context.shareAs(fileType: FileType) {
|
||||||
|
when (fileType) {
|
||||||
|
FileType.PNG -> askForSize {
|
||||||
|
share(barcode.bitmap(it), MIME_PNG, "png")
|
||||||
|
}
|
||||||
|
|
||||||
|
FileType.JPG -> askForSize {
|
||||||
|
share(barcode.bitmap(it), MIME_JPG, "jpg")
|
||||||
|
}
|
||||||
|
|
||||||
|
FileType.SVG -> shareText(barcode.svg(), MIME_SVG)
|
||||||
|
FileType.TXT -> shareText(barcode.text())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Context.share(bitmap: Bitmap, mimeType: String, ext: String) {
|
||||||
|
scope.launch(Dispatchers.IO) {
|
||||||
|
val file = File(
|
||||||
|
externalCacheDir,
|
||||||
|
"shared_barcode.$ext"
|
||||||
|
)
|
||||||
|
val success = try {
|
||||||
|
FileOutputStream(file).use {
|
||||||
|
when (mimeType) {
|
||||||
|
MIME_PNG -> bitmap.saveAsPng(it)
|
||||||
|
MIME_JPG -> bitmap.saveAsJpg(it)
|
||||||
|
else -> throw IllegalArgumentException(
|
||||||
|
"Invalid mime type"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
true
|
||||||
|
} catch (_: IOException) {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
if (success) {
|
||||||
|
shareFile(file, mimeType)
|
||||||
|
} else {
|
||||||
|
toast(R.string.error_saving_file)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun brightenScreen() {
|
||||||
|
if (isFinishing || isDestroyed) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
currentBrightness = getScreenBrightness()
|
||||||
|
setScreenBrightness(1f)
|
||||||
|
brightenScreenItem?.isChecked = true
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun restoreScreenBrightness() {
|
||||||
|
if (currentBrightness > -1f) {
|
||||||
|
setScreenBrightness(currentBrightness)
|
||||||
|
currentBrightness = -1f
|
||||||
|
brightenScreenItem?.isChecked = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val BIT_MATRIX_WIDTH = "bit_matrix_width"
|
||||||
|
private const val BIT_MATRIX_HEIGHT = "bit_matrix_height"
|
||||||
|
private const val BIT_MATRIX_DATA = "bit_matrix_data"
|
||||||
|
private const val CONTENT_TEXT = "content_text"
|
||||||
|
private const val CONTENT_RAW = "content_raw"
|
||||||
|
private const val FORMAT = "format"
|
||||||
|
private const val EC_LEVEL = "ec_level"
|
||||||
|
private const val MARGIN = "margin"
|
||||||
|
private const val COLORS = "colors"
|
||||||
|
private const val MIME_PNG = "image/png"
|
||||||
|
private const val MIME_JPG = "image/jpeg"
|
||||||
|
private const val MIME_SVG = "image/svg+xmg"
|
||||||
|
private const val MIME_TXT = "text/plain"
|
||||||
|
|
||||||
|
fun newIntent(
|
||||||
|
context: Context,
|
||||||
|
barcode: Barcode<*>,
|
||||||
|
colors: Int = 0
|
||||||
|
): Intent {
|
||||||
|
val args = Bundle()
|
||||||
|
when (barcode) {
|
||||||
|
is ContentBarcode -> args.putEcLevelMargin(
|
||||||
|
barcode.ecLevel,
|
||||||
|
barcode.margin
|
||||||
|
)
|
||||||
|
|
||||||
|
is BitMatrixBarcode -> {
|
||||||
|
args.putInt(BIT_MATRIX_WIDTH, barcode.bitMatrix.width)
|
||||||
|
args.putInt(BIT_MATRIX_HEIGHT, barcode.bitMatrix.height)
|
||||||
|
args.putByteArray(BIT_MATRIX_DATA, barcode.bitMatrix.data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
args.putContentFormatColors(
|
||||||
|
barcode.content,
|
||||||
|
barcode.format,
|
||||||
|
colors
|
||||||
|
)
|
||||||
|
return Intent(context, BarcodeActivity::class.java).apply {
|
||||||
|
putExtras(args)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <T> newIntent(
|
||||||
|
context: Context,
|
||||||
|
content: T,
|
||||||
|
format: BarcodeFormat,
|
||||||
|
ecLevel: Int = -1,
|
||||||
|
margin: Int = 0,
|
||||||
|
colors: Int = 0
|
||||||
|
): Intent {
|
||||||
|
val args = Bundle()
|
||||||
|
args.putContentFormatColors(content, format, colors)
|
||||||
|
args.putEcLevelMargin(ecLevel, margin)
|
||||||
|
return Intent(context, BarcodeActivity::class.java).apply {
|
||||||
|
putExtras(args)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun <T> Bundle.putContentFormatColors(
|
||||||
|
content: T,
|
||||||
|
format: BarcodeFormat,
|
||||||
|
colors: Int
|
||||||
|
) {
|
||||||
|
when (content) {
|
||||||
|
is String -> {
|
||||||
|
putString(CONTENT_TEXT, content)
|
||||||
|
}
|
||||||
|
|
||||||
|
is ByteArray -> {
|
||||||
|
putByteArray(CONTENT_RAW, content)
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
throw IllegalArgumentException(
|
||||||
|
"content must be a String of a ByteArray"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
putString(FORMAT, format.name)
|
||||||
|
putInt(COLORS, colors)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Bundle.putEcLevelMargin(
|
||||||
|
ecLevel: Int,
|
||||||
|
margin: Int
|
||||||
|
) {
|
||||||
|
putInt(EC_LEVEL, ecLevel)
|
||||||
|
putInt(MARGIN, margin)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun ImageView.setScale(scale: Float, x: Float, y: Float) {
|
||||||
|
imageMatrix = Matrix().apply {
|
||||||
|
setTranslate(-x, -y)
|
||||||
|
postScale(scale, scale)
|
||||||
|
postTranslate(x, y)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun ImageView.getScale(): Float {
|
||||||
|
val values = FloatArray(9)
|
||||||
|
imageMatrix.getValues(values)
|
||||||
|
return values[Matrix.MSCALE_X]
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Bitmap.saveAsPng(outputStream: OutputStream, quality: Int = 90) =
|
||||||
|
compress(Bitmap.CompressFormat.PNG, quality, outputStream)
|
||||||
|
|
||||||
|
private fun Bitmap.saveAsJpg(outputStream: OutputStream, quality: Int = 90) =
|
||||||
|
compress(Bitmap.CompressFormat.JPEG, quality, outputStream)
|
||||||
|
|
||||||
|
private val fileNameCharacters = "[^A-Za-z0-9]".toRegex()
|
||||||
|
private fun encodeFileName(name: String): String = fileNameCharacters
|
||||||
|
.replace(name, "_")
|
||||||
|
.take(16)
|
||||||
|
.trim('_')
|
||||||
|
.lowercase(Locale.getDefault())
|
||||||
|
|
||||||
|
private fun getSize(step: Int) = when (step) {
|
||||||
|
0 -> 0
|
||||||
|
else -> 128 shl (step - 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun readAndAddToHistory(
|
||||||
|
bitmap: Bitmap,
|
||||||
|
format: BarcodeFormat
|
||||||
|
) {
|
||||||
|
ZxingCpp.readBitmap(
|
||||||
|
bitmap,
|
||||||
|
0, 0,
|
||||||
|
bitmap.width, bitmap.height,
|
||||||
|
0,
|
||||||
|
ZxingCpp.ReaderOptions(
|
||||||
|
tryHarder = true,
|
||||||
|
tryRotate = true,
|
||||||
|
tryInvert = true,
|
||||||
|
tryDownscale = true,
|
||||||
|
maxNumberOfSymbols = 1,
|
||||||
|
formats = setOf(format)
|
||||||
|
)
|
||||||
|
)?.first {
|
||||||
|
val scan = it.toScan()
|
||||||
|
if (!prefs.shouldIgnoreHistoryContent(scan.text, scan.format.name)) {
|
||||||
|
db.insertScan(scan)
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,866 @@
|
|||||||
|
package de.markusfisch.android.binaryeye.activity
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.app.AlertDialog
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.os.Environment
|
||||||
|
import android.provider.DocumentsContract
|
||||||
|
import android.text.Editable
|
||||||
|
import android.text.Html
|
||||||
|
import android.text.Spannable
|
||||||
|
import android.text.SpannableString
|
||||||
|
import android.text.TextWatcher
|
||||||
|
import android.text.method.LinkMovementMethod
|
||||||
|
import android.text.style.TypefaceSpan
|
||||||
|
import android.view.Menu
|
||||||
|
import android.view.MenuInflater
|
||||||
|
import android.view.MenuItem
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.EditText
|
||||||
|
import android.widget.ImageView
|
||||||
|
import android.widget.TableLayout
|
||||||
|
import android.widget.TableRow
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.core.net.toUri
|
||||||
|
import com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||||
|
import de.markusfisch.android.binaryeye.R
|
||||||
|
import de.markusfisch.android.binaryeye.actions.ActionRegistry
|
||||||
|
import de.markusfisch.android.binaryeye.actions.mail.MatMsg
|
||||||
|
import de.markusfisch.android.binaryeye.actions.mail.MatMsgAction
|
||||||
|
import de.markusfisch.android.binaryeye.actions.vtype.VTypeParser
|
||||||
|
import de.markusfisch.android.binaryeye.actions.vtype.vcard.VCardAction
|
||||||
|
import de.markusfisch.android.binaryeye.actions.vtype.vevent.VEventAction
|
||||||
|
import de.markusfisch.android.binaryeye.actions.web.WebAction
|
||||||
|
import de.markusfisch.android.binaryeye.actions.wifi.WifiAction
|
||||||
|
import de.markusfisch.android.binaryeye.actions.wifi.WifiConnector
|
||||||
|
import de.markusfisch.android.binaryeye.adapter.prettifyFormatName
|
||||||
|
import de.markusfisch.android.binaryeye.app.db
|
||||||
|
import de.markusfisch.android.binaryeye.app.hasLocationPermission
|
||||||
|
import de.markusfisch.android.binaryeye.app.hasWritePermission
|
||||||
|
import de.markusfisch.android.binaryeye.app.prefs
|
||||||
|
import de.markusfisch.android.binaryeye.content.ContentBarcode
|
||||||
|
import de.markusfisch.android.binaryeye.content.IdlParser
|
||||||
|
import de.markusfisch.android.binaryeye.content.copyToClipboard
|
||||||
|
import de.markusfisch.android.binaryeye.content.idlToRes
|
||||||
|
import de.markusfisch.android.binaryeye.content.shareAsFile
|
||||||
|
import de.markusfisch.android.binaryeye.content.shareText
|
||||||
|
import de.markusfisch.android.binaryeye.content.toBarcode
|
||||||
|
import de.markusfisch.android.binaryeye.content.toErrorCorrectionInt
|
||||||
|
import de.markusfisch.android.binaryeye.content.wipeLastShareFile
|
||||||
|
import de.markusfisch.android.binaryeye.database.Scan
|
||||||
|
import de.markusfisch.android.binaryeye.database.toBundle
|
||||||
|
import de.markusfisch.android.binaryeye.database.toScan
|
||||||
|
import de.markusfisch.android.binaryeye.io.askForFileName
|
||||||
|
import de.markusfisch.android.binaryeye.io.toSaveResult
|
||||||
|
import de.markusfisch.android.binaryeye.io.writeExternalFile
|
||||||
|
import de.markusfisch.android.binaryeye.net.createEncodeDeeplink
|
||||||
|
import de.markusfisch.android.binaryeye.view.hideSoftKeyboard
|
||||||
|
import de.markusfisch.android.binaryeye.view.setPaddingFromWindowInsets
|
||||||
|
import de.markusfisch.android.binaryeye.widget.toast
|
||||||
|
import de.markusfisch.android.zxingcpp.ZxingCpp
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import java.io.File
|
||||||
|
import java.security.MessageDigest
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
|
class DecodeActivity : AbstractBaseActivity() {
|
||||||
|
private lateinit var contentView: EditText
|
||||||
|
private lateinit var formatView: TextView
|
||||||
|
private lateinit var dataView: TableLayout
|
||||||
|
private lateinit var metaView: TableLayout
|
||||||
|
private lateinit var hexView: TextView
|
||||||
|
private lateinit var stampView: TextView
|
||||||
|
private lateinit var recreationView: ImageView
|
||||||
|
private lateinit var labelView: EditText
|
||||||
|
private lateinit var fab: FloatingActionButton
|
||||||
|
private lateinit var scan: Scan
|
||||||
|
private lateinit var format: String
|
||||||
|
|
||||||
|
private val parentJob = Job()
|
||||||
|
private val scope: CoroutineScope = CoroutineScope(
|
||||||
|
Dispatchers.Main + parentJob
|
||||||
|
)
|
||||||
|
private val dp: Float
|
||||||
|
get() = resources.displayMetrics.density
|
||||||
|
private val content: String
|
||||||
|
get() = contentView.text.toString()
|
||||||
|
|
||||||
|
private var closeAutomatically = false
|
||||||
|
private var action = ActionRegistry.DEFAULT_ACTION
|
||||||
|
private var isBinary = false
|
||||||
|
private var originalBytes: ByteArray = ByteArray(0)
|
||||||
|
private var label: String? = null
|
||||||
|
private var recreationSize = 0
|
||||||
|
|
||||||
|
override fun onActivityResult(
|
||||||
|
requestCode: Int,
|
||||||
|
resultCode: Int,
|
||||||
|
resultData: Intent?
|
||||||
|
) {
|
||||||
|
super.onActivityResult(requestCode, resultCode, resultData)
|
||||||
|
when (requestCode) {
|
||||||
|
OPEN_DOCUMENT -> {
|
||||||
|
if (resultCode == Activity.RESULT_OK) {
|
||||||
|
val uri = resultData?.data ?: return
|
||||||
|
openPickedFile(uri)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate(state: Bundle?) {
|
||||||
|
super.onCreate(state)
|
||||||
|
setScreenContentView(R.layout.activity_decode)
|
||||||
|
setTitle(R.string.content)
|
||||||
|
|
||||||
|
val justScanned = intent.hasExtra(DECODED) || intent.hasExtra(DECODED_SCAN)
|
||||||
|
closeAutomatically = prefs.closeAutomatically && justScanned
|
||||||
|
|
||||||
|
scan = intent?.extras?.getBundle(SCAN_BUNDLE)?.toScan()
|
||||||
|
?: throw IllegalArgumentException("DecodeActivity needs a Scan")
|
||||||
|
|
||||||
|
isBinary = scan.text.isEmpty()
|
||||||
|
originalBytes = scan.raw ?: scan.text.toByteArray()
|
||||||
|
format = scan.format.name
|
||||||
|
recreationSize = (200f * dp).roundToInt()
|
||||||
|
|
||||||
|
contentView = findViewById(R.id.content)
|
||||||
|
formatView = findViewById(R.id.format)
|
||||||
|
dataView = findViewById(R.id.data)
|
||||||
|
metaView = findViewById(R.id.meta)
|
||||||
|
hexView = findViewById(R.id.hex)
|
||||||
|
stampView = findViewById(R.id.stamp)
|
||||||
|
recreationView = findViewById(R.id.recreation)
|
||||||
|
labelView = findViewById(R.id.label)
|
||||||
|
fab = findViewById(R.id.open)
|
||||||
|
|
||||||
|
if (prefs.showMetaData) {
|
||||||
|
metaView.fillMetaView(scan)
|
||||||
|
} else {
|
||||||
|
metaView.visibility = View.GONE
|
||||||
|
}
|
||||||
|
|
||||||
|
if (scan.id > 0) {
|
||||||
|
scan.label?.let {
|
||||||
|
labelView.setText(it)
|
||||||
|
label = it
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
labelView.visibility = View.GONE
|
||||||
|
}
|
||||||
|
|
||||||
|
findViewById<View>(R.id.inset_layout).setPaddingFromWindowInsets()
|
||||||
|
findViewById<View>(R.id.scroll_view).setPaddingFromWindowInsets()
|
||||||
|
|
||||||
|
initContentAndFab(justScanned)
|
||||||
|
updateViewsAndFab(scan.text, originalBytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPause() {
|
||||||
|
super.onPause()
|
||||||
|
val id = scan.id
|
||||||
|
if (id > 0) {
|
||||||
|
val newLabel = labelView.text.toString().trim()
|
||||||
|
if (newLabel != (label ?: "")) {
|
||||||
|
db.renameScan(id, newLabel)
|
||||||
|
label = newLabel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (action.fired) {
|
||||||
|
maybeBackOrFinish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
super.onDestroy()
|
||||||
|
parentJob.cancel()
|
||||||
|
wipeLastShareFile()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initContentAndFab(justScanned: Boolean) {
|
||||||
|
if (isBinary) {
|
||||||
|
contentView.setText(String(originalBytes).foldNonAlNum())
|
||||||
|
contentView.isEnabled = false
|
||||||
|
fab.setImageResource(R.drawable.ic_action_save)
|
||||||
|
fab.setOnClickListener {
|
||||||
|
askForFileNameAndSave(originalBytes)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
contentView.setText(scan.text)
|
||||||
|
contentView.addTextChangedListener(object : TextWatcher {
|
||||||
|
override fun afterTextChanged(s: Editable?) {
|
||||||
|
updateViewsAndFab(s?.toString() ?: "")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun beforeTextChanged(
|
||||||
|
s: CharSequence?,
|
||||||
|
start: Int,
|
||||||
|
count: Int,
|
||||||
|
after: Int
|
||||||
|
) = Unit
|
||||||
|
|
||||||
|
override fun onTextChanged(
|
||||||
|
s: CharSequence?,
|
||||||
|
start: Int,
|
||||||
|
before: Int,
|
||||||
|
count: Int
|
||||||
|
) = Unit
|
||||||
|
})
|
||||||
|
fab.setOnClickListener {
|
||||||
|
executeAction(content)
|
||||||
|
}
|
||||||
|
if (justScanned && prefs.openImmediately) {
|
||||||
|
closeAutomatically = true
|
||||||
|
executeAction(content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateViewsAndFab(text: String, bytes: ByteArray? = null) {
|
||||||
|
val b = bytes ?: text.toByteArray()
|
||||||
|
resolveActionAndUpdateFab(b)
|
||||||
|
updateViews(text, b)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun resolveActionAndUpdateFab(bytes: ByteArray) {
|
||||||
|
if (!isBinary) {
|
||||||
|
val prevAction = action
|
||||||
|
if (!prevAction.canExecuteOn(bytes)) {
|
||||||
|
action = ActionRegistry.getAction(bytes)
|
||||||
|
}
|
||||||
|
if (prevAction !== action) {
|
||||||
|
fab.setImageResource(action.iconResId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Run this for the initial action too.
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
||||||
|
fab.setOnLongClickListener { v ->
|
||||||
|
v.context.toast(action.titleResId)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fab.tooltipText = getString(action.titleResId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateViews(text: String, bytes: ByteArray) {
|
||||||
|
formatView.text = resources.getQuantityString(
|
||||||
|
R.plurals.barcode_info,
|
||||||
|
bytes.size,
|
||||||
|
format.prettifyFormatName(),
|
||||||
|
bytes.size
|
||||||
|
)
|
||||||
|
hexView.showIf(prefs.showHexDump) { v ->
|
||||||
|
v.text = hexDump(bytes)
|
||||||
|
}
|
||||||
|
recreationView.showIf(prefs.showRecreation) { v ->
|
||||||
|
val unmodified: Boolean
|
||||||
|
val content = if (isBinary) {
|
||||||
|
if (bytes.isEmpty()) {
|
||||||
|
clearRecreation()
|
||||||
|
return@showIf
|
||||||
|
}
|
||||||
|
unmodified = bytes.contentEquals(originalBytes)
|
||||||
|
bytes
|
||||||
|
} else {
|
||||||
|
if (text.isEmpty()) {
|
||||||
|
clearRecreation()
|
||||||
|
return@showIf
|
||||||
|
}
|
||||||
|
unmodified = text == scan.text
|
||||||
|
text
|
||||||
|
}
|
||||||
|
val barcode = if (unmodified) {
|
||||||
|
scan.toBarcode()
|
||||||
|
} else {
|
||||||
|
ContentBarcode(
|
||||||
|
content,
|
||||||
|
scan.format,
|
||||||
|
scan.errorCorrectionLevel.toErrorCorrectionInt(),
|
||||||
|
margin = 1
|
||||||
|
)
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
v.setImageBitmap(barcode.bitmap(recreationSize))
|
||||||
|
v.setOnClickListener {
|
||||||
|
v.context.hideSoftKeyboard(contentView)
|
||||||
|
v.post {
|
||||||
|
startActivity(
|
||||||
|
BarcodeActivity.newIntent(this, barcode)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (_: RuntimeException) {
|
||||||
|
clearRecreation()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dataView.fillDataView(text, bytes)
|
||||||
|
stampView.setTrackingLink(bytes, format)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun clearRecreation() {
|
||||||
|
recreationView.apply {
|
||||||
|
setImageBitmap(null)
|
||||||
|
setOnClickListener(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun TableLayout.fillDataView(text: String, bytes: ByteArray) {
|
||||||
|
val items = LinkedHashMap<Any, CharSequence?>()
|
||||||
|
when (prefs.showChecksum) {
|
||||||
|
"CRC4" -> items[R.string.crc4] = String.format("%X", crc4(bytes))
|
||||||
|
"MD5" -> items[R.string.md5] = bytes.md5().toHexString().fold()
|
||||||
|
"SHA1" -> items[R.string.sha1] = bytes.sha1().toHexString().fold()
|
||||||
|
"SHA256" -> items[R.string.sha256] =
|
||||||
|
bytes.sha256().toHexString().fold()
|
||||||
|
|
||||||
|
else -> Unit
|
||||||
|
}
|
||||||
|
IdlParser.parse(String(bytes))?.let {
|
||||||
|
items.putAll(mapOf("IIN" to it.iin))
|
||||||
|
items.putAll(
|
||||||
|
it.elements.entries.associate { (id, value) ->
|
||||||
|
context.idlToRes(id) to value
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
when (action) {
|
||||||
|
is MatMsgAction -> items.putAll(
|
||||||
|
MatMsg(text).run {
|
||||||
|
mapOf(
|
||||||
|
R.string.email_to to to,
|
||||||
|
R.string.email_subject to sub,
|
||||||
|
R.string.email_body to body
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
is VCardAction,
|
||||||
|
is VEventAction -> VTypeParser.parseMap(text).let { vData ->
|
||||||
|
items.putAll(
|
||||||
|
vData.map { item ->
|
||||||
|
item.key to item.value.joinToString("\n") {
|
||||||
|
it.value
|
||||||
|
}
|
||||||
|
}.toMap()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
is WebAction -> try {
|
||||||
|
items.putAll(
|
||||||
|
text.toUri().run {
|
||||||
|
mapOf(
|
||||||
|
R.string.scheme to scheme,
|
||||||
|
R.string.host to host,
|
||||||
|
R.string.query to query
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} catch (_: Exception) {
|
||||||
|
// Ignore
|
||||||
|
}
|
||||||
|
|
||||||
|
is WifiAction -> WifiConnector.parseMap(text)?.let { wifiData ->
|
||||||
|
items.putAll(
|
||||||
|
linkedMapOf(
|
||||||
|
R.string.entry_type to getString(R.string.wifi_network),
|
||||||
|
R.string.wifi_ssid to wifiData["S"],
|
||||||
|
R.string.wifi_password to wifiData["P"],
|
||||||
|
R.string.wifi_type to wifiData["T"],
|
||||||
|
R.string.wifi_hidden to wifiData["H"],
|
||||||
|
R.string.wifi_eap to wifiData["E"],
|
||||||
|
R.string.wifi_identity to wifiData["I"],
|
||||||
|
R.string.wifi_anonymous_identity to wifiData["A"],
|
||||||
|
R.string.wifi_phase2 to wifiData["PH2"]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fill(items)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun TableLayout.fillMetaView(scan: Scan) {
|
||||||
|
val items = linkedMapOf<Any, CharSequence?>(
|
||||||
|
R.string.error_correction_level to scan.errorCorrectionLevel,
|
||||||
|
R.string.sequence_size to scan.sequenceSize.positiveToString(),
|
||||||
|
R.string.sequence_index to scan.sequenceIndex.positiveToString(),
|
||||||
|
R.string.sequence_id to scan.sequenceId,
|
||||||
|
R.string.gtin_country to scan.country,
|
||||||
|
R.string.gtin_add_on to scan.addOn,
|
||||||
|
R.string.gtin_price to scan.price,
|
||||||
|
R.string.gtin_issue_number to scan.issueNumber,
|
||||||
|
)
|
||||||
|
if (!scan.version.isNullOrBlank()) {
|
||||||
|
val versionString = if (scan.format == ZxingCpp.BarcodeFormat.QRCode) {
|
||||||
|
getString(
|
||||||
|
R.string.qr_version_and_modules,
|
||||||
|
scan.version,
|
||||||
|
(scan.version.toIntOrNull() ?: 0) * 4 + 17
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
scan.version
|
||||||
|
}
|
||||||
|
items.putAll(
|
||||||
|
linkedMapOf(
|
||||||
|
R.string.barcode_version_number to versionString
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (scan.format == ZxingCpp.BarcodeFormat.QRCode &&
|
||||||
|
scan.dataMask > -1
|
||||||
|
) {
|
||||||
|
items.putAll(
|
||||||
|
linkedMapOf(
|
||||||
|
R.string.qr_data_mask to scan.dataMask.toString()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
fill(items)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun TableLayout.fill(
|
||||||
|
items: LinkedHashMap<Any, CharSequence?>
|
||||||
|
) {
|
||||||
|
removeAllViews()
|
||||||
|
visibility = if (items.isEmpty()) View.GONE else {
|
||||||
|
val ctx = context
|
||||||
|
val spaceBetween = (16f * dp).roundToInt()
|
||||||
|
items.forEach { item ->
|
||||||
|
val text = item.value
|
||||||
|
if (!text.isNullOrBlank()) {
|
||||||
|
val tr = TableRow(ctx)
|
||||||
|
val keyView = TextView(ctx)
|
||||||
|
when (val key = item.key) {
|
||||||
|
is Int -> keyView.setText(key)
|
||||||
|
is String -> keyView.text = key
|
||||||
|
else -> keyView.text = key.toString()
|
||||||
|
}
|
||||||
|
val valueView = TextView(ctx).apply {
|
||||||
|
setPadding(spaceBetween, 0, 0, 0)
|
||||||
|
this.text = text
|
||||||
|
setOnClickListener {
|
||||||
|
copyToClipboard(text.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tr.addView(keyView)
|
||||||
|
tr.addView(valueView)
|
||||||
|
addView(tr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
View.VISIBLE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||||
|
inflater.inflate(R.menu.fragment_decode, menu)
|
||||||
|
if (scan.id > 0L) {
|
||||||
|
menu.findItem(R.id.remove).isVisible = true
|
||||||
|
}
|
||||||
|
if (action is WifiAction) {
|
||||||
|
menu.findItem(R.id.copy_password).isVisible = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||||
|
return when (item.itemId) {
|
||||||
|
R.id.copy_password -> {
|
||||||
|
copyPasswordToClipboard()
|
||||||
|
maybeBackOrFinish()
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
R.id.copy_to_clipboard -> {
|
||||||
|
copyToClipboard(textOrHex())
|
||||||
|
maybeBackOrFinish()
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
R.id.copy_as_deeplink -> {
|
||||||
|
copyToClipboard(deeplinkToCopy())
|
||||||
|
maybeBackOrFinish()
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
R.id.share -> {
|
||||||
|
shareText(textOrHex())
|
||||||
|
maybeBackOrFinish()
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
R.id.share_file -> {
|
||||||
|
val (data, name) = if (isBinary) {
|
||||||
|
Pair(originalBytes, "barcode_content.bin")
|
||||||
|
} else {
|
||||||
|
Pair(content, "barcode_content.txt")
|
||||||
|
}
|
||||||
|
shareAsFile(data, name)
|
||||||
|
maybeBackOrFinish()
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
R.id.create -> {
|
||||||
|
startActivity(
|
||||||
|
EncodeActivity.newIntent(
|
||||||
|
this,
|
||||||
|
if (isBinary) originalBytes else content,
|
||||||
|
format
|
||||||
|
)
|
||||||
|
)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
R.id.remove -> {
|
||||||
|
askToRemoveScan(scan.id)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> super.onOptionsItemSelected(item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun askToRemoveScan(id: Long) {
|
||||||
|
AlertDialog.Builder(this)
|
||||||
|
.setMessage(R.string.really_remove_scan)
|
||||||
|
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||||
|
db.removeScan(id)
|
||||||
|
backOrFinish()
|
||||||
|
}
|
||||||
|
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||||
|
}
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun textOrHex() = if (isBinary) {
|
||||||
|
originalBytes.toHexString()
|
||||||
|
} else {
|
||||||
|
content
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun deeplinkToCopy() = createEncodeDeeplink(
|
||||||
|
format = format,
|
||||||
|
content = textOrHex(),
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun copyPasswordToClipboard() {
|
||||||
|
val ac = action
|
||||||
|
if (ac is WifiAction) {
|
||||||
|
ac.password?.let { password ->
|
||||||
|
copyToClipboard(password, isSensitive = true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun copyToClipboard(text: String, isSensitive: Boolean = false) {
|
||||||
|
(this as android.content.Context).copyToClipboard(text, isSensitive)
|
||||||
|
// There's a clipboard popup from Android 13 on.
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
|
||||||
|
toast(R.string.copied_to_clipboard)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun maybeBackOrFinish() {
|
||||||
|
if (closeAutomatically) {
|
||||||
|
backOrFinish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun backOrFinish() {
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun executeAction(str: String) {
|
||||||
|
if (str.isEmpty() || openLocalDocument(str)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (action is WifiAction &&
|
||||||
|
Build.VERSION.SDK_INT < Build.VERSION_CODES.Q &&
|
||||||
|
!hasLocationPermission { executeAction(str) }
|
||||||
|
) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
scope.launch {
|
||||||
|
action.execute(this@DecodeActivity, str.toByteArray())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun openLocalDocument(str: String): Boolean {
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N ||
|
||||||
|
!str.startsWith(SCHEME_FILE)
|
||||||
|
) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// Needs to be disabled so we get the result.
|
||||||
|
closeAutomatically = false
|
||||||
|
// file:// can't be used with Scoped Storage anymore.
|
||||||
|
startActivityForResult(
|
||||||
|
Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
|
||||||
|
addCategory(Intent.CATEGORY_OPENABLE)
|
||||||
|
type = "*/*"
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
val primary = str.substring(
|
||||||
|
SCHEME_FILE.length
|
||||||
|
).localDirName() ?: Environment.getExternalStorageDirectory().path
|
||||||
|
putExtra(
|
||||||
|
DocumentsContract.EXTRA_INITIAL_URI,
|
||||||
|
DocumentsContract.buildDocumentUri(
|
||||||
|
"com.android.externalstorage.documents",
|
||||||
|
"primary:$primary"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
OPEN_DOCUMENT
|
||||||
|
)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun askForFileNameAndSave(raw: ByteArray) {
|
||||||
|
// Write permission is only required before Android Q.
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q &&
|
||||||
|
!hasWritePermission { askForFileNameAndSave(raw) }
|
||||||
|
) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
scope.launch(Dispatchers.Main) {
|
||||||
|
val name = askForFileName() ?: return@launch
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
val message = writeExternalFile(
|
||||||
|
name,
|
||||||
|
"application/octet-stream"
|
||||||
|
) {
|
||||||
|
it.write(raw)
|
||||||
|
}.toSaveResult()
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
toast(message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val DECODED_SCAN = "decoded_scan"
|
||||||
|
const val DECODED = "decoded"
|
||||||
|
private const val SCAN_BUNDLE = "scan_bundle"
|
||||||
|
private const val OPEN_DOCUMENT = 1
|
||||||
|
private const val SCHEME_FILE = "file://"
|
||||||
|
|
||||||
|
fun newIntent(context: Context, scan: Scan): Intent {
|
||||||
|
val args = Bundle()
|
||||||
|
args.putBundle(SCAN_BUNDLE, scan.toBundle())
|
||||||
|
return Intent(context, DecodeActivity::class.java).apply {
|
||||||
|
putExtra(DECODED_SCAN, true)
|
||||||
|
putExtras(args)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private inline fun <T : View> T.showIf(
|
||||||
|
visible: Boolean,
|
||||||
|
block: (T) -> Unit
|
||||||
|
) {
|
||||||
|
visibility = if (visible) {
|
||||||
|
block.invoke(this)
|
||||||
|
View.VISIBLE
|
||||||
|
} else {
|
||||||
|
View.GONE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val nonAlNum = "[^a-zA-Z0-9]".toRegex()
|
||||||
|
private val multipleDots = "…+".toRegex()
|
||||||
|
private fun String.foldNonAlNum() = replace(nonAlNum, "…")
|
||||||
|
.replace(multipleDots, "…")
|
||||||
|
|
||||||
|
private fun hexDump(bytes: ByteArray, charsPerLine: Int = 33): String {
|
||||||
|
if (charsPerLine < 4 || bytes.isEmpty()) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
val dump = StringBuilder()
|
||||||
|
val hex = StringBuilder()
|
||||||
|
val ascii = StringBuilder()
|
||||||
|
val itemsPerLine = (charsPerLine - 1) / 4
|
||||||
|
val len = bytes.size
|
||||||
|
var i = 0
|
||||||
|
while (true) {
|
||||||
|
val ord = bytes[i]
|
||||||
|
hex.append(String.format("%02X ", ord))
|
||||||
|
ascii.append(if (ord > 31) ord.toInt().toChar() else " ")
|
||||||
|
++i
|
||||||
|
val posInLine = i % itemsPerLine
|
||||||
|
val atEnd = i >= len
|
||||||
|
var atLineEnd = posInLine == 0
|
||||||
|
if (atEnd && !atLineEnd) {
|
||||||
|
for (j in posInLine until itemsPerLine) {
|
||||||
|
hex.append(" ")
|
||||||
|
}
|
||||||
|
atLineEnd = true
|
||||||
|
}
|
||||||
|
if (atLineEnd) {
|
||||||
|
dump.append(hex.toString())
|
||||||
|
dump.append(" ")
|
||||||
|
dump.append(ascii.toString())
|
||||||
|
dump.append("\n")
|
||||||
|
hex.setLength(0)
|
||||||
|
ascii.setLength(0)
|
||||||
|
}
|
||||||
|
if (atEnd) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return dump.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Int.positiveToString() = if (this > -1) this.toString() else ""
|
||||||
|
|
||||||
|
private fun TextView.setTrackingLink(bytes: ByteArray, format: String) {
|
||||||
|
val trackingLink = generateDpTrackingLink(bytes, format)
|
||||||
|
if (trackingLink != null) {
|
||||||
|
text = trackingLink.fromHtml()
|
||||||
|
isClickable = true
|
||||||
|
movementMethod = LinkMovementMethod.getInstance()
|
||||||
|
} else {
|
||||||
|
visibility = View.GONE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun generateDpTrackingLink(bytes: ByteArray, format: String): String? {
|
||||||
|
// Check for Deutsche Post Matrixcode stamp.
|
||||||
|
var isStamp = false
|
||||||
|
var rawData = bytes
|
||||||
|
if (format == ZxingCpp.BarcodeFormat.DataMatrix.name &&
|
||||||
|
bytes.toString(Charsets.ISO_8859_1).startsWith("DEA5")
|
||||||
|
) {
|
||||||
|
if (bytes.size == 47) {
|
||||||
|
isStamp = true
|
||||||
|
} else if (bytes.size > 47) {
|
||||||
|
// Transform back to original data.
|
||||||
|
rawData = bytes.toString(Charsets.UTF_8).toByteArray(
|
||||||
|
Charsets.ISO_8859_1
|
||||||
|
)
|
||||||
|
if (rawData.size == 47) {
|
||||||
|
isStamp = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isStamp) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
val hex = StringBuilder()
|
||||||
|
hex.append(String.format("%02X", rawData[9]))
|
||||||
|
hex.append(String.format("%02X", rawData[10]))
|
||||||
|
hex.append(String.format("%02X", rawData[11]))
|
||||||
|
hex.append(String.format("%02X", rawData[12]))
|
||||||
|
hex.append(String.format("%02X", rawData[13]))
|
||||||
|
hex.append(String.format("%X", (rawData[4].toInt() and 0x0f).toByte()))
|
||||||
|
hex.append(String.format("%02X", rawData[5]))
|
||||||
|
hex.append(String.format("%02X", rawData[6]))
|
||||||
|
hex.append(String.format("%02X", rawData[7]))
|
||||||
|
hex.append(String.format("%02X", rawData[8]))
|
||||||
|
val hexString = hex.toString()
|
||||||
|
val trackingNumber = hexString + String.format(
|
||||||
|
"%X",
|
||||||
|
crc4(hexString.toByteArray(Charsets.ISO_8859_1))
|
||||||
|
)
|
||||||
|
return "<a href=\"https://www.deutschepost.de/de/s/sendungsverfolgung.html?piececode=$trackingNumber\">Deutsche Post: $trackingNumber</a>"
|
||||||
|
}
|
||||||
|
|
||||||
|
// CRC-4 with polynomial x^4 + x + 1.
|
||||||
|
private fun crc4(input: ByteArray): Int {
|
||||||
|
var crc = 0
|
||||||
|
var i = 0
|
||||||
|
while (i < input.size) {
|
||||||
|
val c = input[i].toInt()
|
||||||
|
var j = 0x80
|
||||||
|
while (j != 0) {
|
||||||
|
var bit = crc and 0x8
|
||||||
|
crc = crc shl 1
|
||||||
|
if (c and j != 0) {
|
||||||
|
bit = bit xor 0x8
|
||||||
|
}
|
||||||
|
if (bit != 0) {
|
||||||
|
crc = crc xor 0x3
|
||||||
|
}
|
||||||
|
j = j ushr 1
|
||||||
|
}
|
||||||
|
++i
|
||||||
|
}
|
||||||
|
crc = crc and 0xF
|
||||||
|
return crc
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun String.fromHtml() = if (
|
||||||
|
Build.VERSION.SDK_INT >= Build.VERSION_CODES.N
|
||||||
|
) {
|
||||||
|
Html.fromHtml(this, Html.FROM_HTML_MODE_LEGACY)
|
||||||
|
} else {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
Html.fromHtml(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun ByteArray.md5(): ByteArray = MessageDigest.getInstance(
|
||||||
|
"MD5"
|
||||||
|
).run {
|
||||||
|
update(this@md5)
|
||||||
|
digest()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun ByteArray.sha1(): ByteArray = MessageDigest.getInstance(
|
||||||
|
"SHA-1"
|
||||||
|
).run {
|
||||||
|
update(this@sha1)
|
||||||
|
digest()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun ByteArray.sha256(): ByteArray = MessageDigest.getInstance(
|
||||||
|
"SHA-256"
|
||||||
|
).run {
|
||||||
|
update(this@sha256)
|
||||||
|
digest()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun String.fold(): CharSequence {
|
||||||
|
val l = length
|
||||||
|
return if (l > 10) {
|
||||||
|
val half = l / 2
|
||||||
|
val s = substring(0, half) + "\n" + substring(half)
|
||||||
|
SpannableString(s).apply {
|
||||||
|
setSpan(
|
||||||
|
TypefaceSpan("monospace"),
|
||||||
|
0, s.length,
|
||||||
|
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun String.localDirName(): String? = when {
|
||||||
|
endsWith("/") -> this
|
||||||
|
else -> File(this).parentFile?.absolutePath
|
||||||
|
}?.replaceFirst(Regex("^/storage/emulated/0/"), "")
|
||||||
|
?.replaceFirst(Regex("^/sdcard/"), "")
|
||||||
|
?.trim('/')
|
||||||
|
|
||||||
|
private fun Activity.openPickedFile(uri: Uri) {
|
||||||
|
startActivity(
|
||||||
|
Intent.createChooser(
|
||||||
|
Intent(Intent.ACTION_VIEW).apply {
|
||||||
|
setDataAndType(uri, contentResolver.getType(uri) ?: "*/*")
|
||||||
|
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||||
|
},
|
||||||
|
getString(R.string.open_url)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,437 @@
|
|||||||
|
package de.markusfisch.android.binaryeye.activity
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.Menu
|
||||||
|
import android.view.MenuInflater
|
||||||
|
import android.view.MenuItem
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.AdapterView
|
||||||
|
import android.widget.ArrayAdapter
|
||||||
|
import android.widget.CheckBox
|
||||||
|
import android.widget.EditText
|
||||||
|
import android.widget.Spinner
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.appcompat.widget.SwitchCompat
|
||||||
|
import androidx.core.net.toUri
|
||||||
|
import de.markusfisch.android.binaryeye.R
|
||||||
|
import de.markusfisch.android.binaryeye.adapter.prettifyFormatName
|
||||||
|
import de.markusfisch.android.binaryeye.app.prefs
|
||||||
|
import de.markusfisch.android.binaryeye.net.isEncodeDeeplink
|
||||||
|
import de.markusfisch.android.binaryeye.text.unescape
|
||||||
|
import de.markusfisch.android.binaryeye.view.hideSoftKeyboard
|
||||||
|
import de.markusfisch.android.binaryeye.view.setPaddingFromWindowInsets
|
||||||
|
import de.markusfisch.android.binaryeye.widget.toast
|
||||||
|
import de.markusfisch.android.zxingcpp.ZxingCpp.BarcodeFormat
|
||||||
|
import java.io.InputStream
|
||||||
|
import kotlin.math.min
|
||||||
|
|
||||||
|
|
||||||
|
class EncodeActivity : AbstractBaseActivity() {
|
||||||
|
private lateinit var formatView: Spinner
|
||||||
|
private lateinit var ecLabel: TextView
|
||||||
|
private lateinit var ecSpinner: Spinner
|
||||||
|
private lateinit var colorsLabel: TextView
|
||||||
|
private lateinit var colorsSpinner: Spinner
|
||||||
|
private lateinit var addQuietZoneSwitch: SwitchCompat
|
||||||
|
private lateinit var contentView: EditText
|
||||||
|
private lateinit var unescapeCheckBox: CheckBox
|
||||||
|
|
||||||
|
private val writeableFormats = arrayListOf(
|
||||||
|
BarcodeFormat.Aztec,
|
||||||
|
BarcodeFormat.Codabar,
|
||||||
|
BarcodeFormat.Code39,
|
||||||
|
BarcodeFormat.Code39Ext,
|
||||||
|
BarcodeFormat.Code32,
|
||||||
|
BarcodeFormat.PZN,
|
||||||
|
BarcodeFormat.Code93,
|
||||||
|
BarcodeFormat.Code128,
|
||||||
|
BarcodeFormat.DataBar,
|
||||||
|
BarcodeFormat.DataBarOmni,
|
||||||
|
BarcodeFormat.DataBarStk,
|
||||||
|
BarcodeFormat.DataBarLtd,
|
||||||
|
BarcodeFormat.DataBarExp,
|
||||||
|
BarcodeFormat.DataBarExpStk,
|
||||||
|
BarcodeFormat.DataMatrix,
|
||||||
|
BarcodeFormat.DXFilmEdge,
|
||||||
|
BarcodeFormat.EAN8,
|
||||||
|
BarcodeFormat.EAN13,
|
||||||
|
BarcodeFormat.ITF,
|
||||||
|
BarcodeFormat.MaxiCode,
|
||||||
|
BarcodeFormat.PDF417,
|
||||||
|
BarcodeFormat.QRCode,
|
||||||
|
BarcodeFormat.MicroQRCode,
|
||||||
|
BarcodeFormat.RMQRCode,
|
||||||
|
BarcodeFormat.UPCA,
|
||||||
|
BarcodeFormat.UPCE,
|
||||||
|
)
|
||||||
|
|
||||||
|
private var bytes: ByteArray? = null
|
||||||
|
|
||||||
|
override fun onActivityResult(
|
||||||
|
requestCode: Int,
|
||||||
|
resultCode: Int,
|
||||||
|
resultData: Intent?
|
||||||
|
) {
|
||||||
|
super.onActivityResult(requestCode, resultCode, resultData)
|
||||||
|
when (requestCode) {
|
||||||
|
PICK_FILE_RESULT_CODE -> {
|
||||||
|
if (resultCode == Activity.RESULT_OK &&
|
||||||
|
resultData != null &&
|
||||||
|
resultData.data != null
|
||||||
|
) {
|
||||||
|
hideSoftKeyboard(contentView)
|
||||||
|
val uri = resultData.data ?: return
|
||||||
|
bytes = contentResolver?.openInputStream(uri)?.use {
|
||||||
|
it.readBytesMax(4096)
|
||||||
|
} ?: return
|
||||||
|
setEncodeByteArray()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate(state: Bundle?) {
|
||||||
|
super.onCreate(state)
|
||||||
|
setScreenContentView(R.layout.activity_encode)
|
||||||
|
setTitle(R.string.compose_barcode)
|
||||||
|
|
||||||
|
formatView = findViewById(R.id.format)
|
||||||
|
val formatAdapter = ArrayAdapter(
|
||||||
|
this,
|
||||||
|
android.R.layout.simple_spinner_item,
|
||||||
|
writeableFormats.map { it.name.prettifyFormatName() }
|
||||||
|
)
|
||||||
|
formatAdapter.setDropDownViewResource(
|
||||||
|
android.R.layout.simple_spinner_dropdown_item
|
||||||
|
)
|
||||||
|
formatView.adapter = formatAdapter
|
||||||
|
formatView.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
|
||||||
|
override fun onItemSelected(
|
||||||
|
parentView: AdapterView<*>?,
|
||||||
|
selectedItemView: View?,
|
||||||
|
position: Int,
|
||||||
|
id: Long
|
||||||
|
) {
|
||||||
|
val format = writeableFormats[position]
|
||||||
|
val arrayId = when (format) {
|
||||||
|
BarcodeFormat.Aztec -> R.array.aztec_error_correction_levels
|
||||||
|
BarcodeFormat.QRCode -> R.array.qr_error_correction_levels
|
||||||
|
BarcodeFormat.MicroQRCode -> R.array.mqr_error_correction_levels
|
||||||
|
BarcodeFormat.RMQRCode -> R.array.rmqr_error_correction_levels
|
||||||
|
BarcodeFormat.PDF417 -> R.array.pdf417_error_correction_levels
|
||||||
|
else -> 0
|
||||||
|
}
|
||||||
|
if (arrayId > 0) {
|
||||||
|
ecSpinner.setEntries(arrayId)
|
||||||
|
val idx = format.unpackEcLevel(
|
||||||
|
prefs.indexOfLastSelectedEcLevel
|
||||||
|
)
|
||||||
|
if (idx < ecSpinner.adapter.count) {
|
||||||
|
ecSpinner.setSelection(idx)
|
||||||
|
}
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}.setVisibilityFor(ecLabel, ecSpinner)
|
||||||
|
format.canBeInverted().setVisibilityFor(
|
||||||
|
colorsLabel, colorsSpinner
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onNothingSelected(parentView: AdapterView<*>?) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
ecLabel = findViewById(R.id.error_correction_label)
|
||||||
|
ecSpinner = findViewById(R.id.error_correction_level)
|
||||||
|
ecSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
|
||||||
|
override fun onItemSelected(
|
||||||
|
parentView: AdapterView<*>?,
|
||||||
|
selectedItemView: View?,
|
||||||
|
position: Int,
|
||||||
|
id: Long
|
||||||
|
) {
|
||||||
|
val format = writeableFormats[formatView.selectedItemPosition]
|
||||||
|
prefs.indexOfLastSelectedEcLevel = format.packEcLevel(
|
||||||
|
prefs.indexOfLastSelectedEcLevel,
|
||||||
|
position
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onNothingSelected(parentView: AdapterView<*>?) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
colorsLabel = findViewById(R.id.colors_label)
|
||||||
|
colorsSpinner = findViewById(R.id.colors)
|
||||||
|
addQuietZoneSwitch = findViewById(R.id.add_quiet_zone)
|
||||||
|
addQuietZoneSwitch.isChecked = prefs.addQuietZone
|
||||||
|
|
||||||
|
contentView = findViewById(R.id.content)
|
||||||
|
unescapeCheckBox = findViewById(R.id.unescape)
|
||||||
|
unescapeCheckBox.isChecked = prefs.expandEscapeSequences
|
||||||
|
|
||||||
|
var complete = false
|
||||||
|
val args = intent?.extras ?: getExtrasFromDeeplink()
|
||||||
|
args?.getString(CONTENT_TEXT)?.let {
|
||||||
|
contentView.setText(it)
|
||||||
|
complete = it.isNotEmpty()
|
||||||
|
}
|
||||||
|
args?.getByteArray(CONTENT_RAW)?.let {
|
||||||
|
bytes = it
|
||||||
|
complete = it.isNotEmpty()
|
||||||
|
setEncodeByteArray()
|
||||||
|
}
|
||||||
|
|
||||||
|
val barcodeFormat = args?.getString(FORMAT)
|
||||||
|
if (barcodeFormat != null) {
|
||||||
|
formatView.setSelection(
|
||||||
|
writeableFormats.indexOf(barcodeFormat.toFormat())
|
||||||
|
)
|
||||||
|
} else if (state == null) {
|
||||||
|
formatView.post {
|
||||||
|
formatView.setSelection(prefs.indexOfLastSelectedFormat)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
findViewById<View>(R.id.encode).setOnClickListener {
|
||||||
|
it.context.encode()
|
||||||
|
}
|
||||||
|
|
||||||
|
findViewById<View>(R.id.inset_layout).setPaddingFromWindowInsets()
|
||||||
|
findViewById<View>(R.id.scroll_view).setPaddingFromWindowInsets()
|
||||||
|
|
||||||
|
if (complete &&
|
||||||
|
args?.getBoolean(EXECUTE, false) == true
|
||||||
|
) {
|
||||||
|
contentView.post {
|
||||||
|
val content = contentView.context.getContent() ?: return@post
|
||||||
|
startActivity(newBarcodeIntent(content))
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||||
|
inflater.inflate(R.menu.fragment_encode, menu)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||||
|
return when (item.itemId) {
|
||||||
|
R.id.pick_file -> {
|
||||||
|
startActivityForResult(
|
||||||
|
Intent.createChooser(
|
||||||
|
Intent(Intent.ACTION_GET_CONTENT).apply {
|
||||||
|
type = "*/*"
|
||||||
|
},
|
||||||
|
getString(R.string.pick_file)
|
||||||
|
),
|
||||||
|
PICK_FILE_RESULT_CODE
|
||||||
|
)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> super.onOptionsItemSelected(item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPause() {
|
||||||
|
super.onPause()
|
||||||
|
prefs.indexOfLastSelectedFormat = formatView.selectedItemPosition
|
||||||
|
prefs.expandEscapeSequences = unescapeCheckBox.isChecked
|
||||||
|
prefs.addQuietZone = addQuietZoneSwitch.isChecked
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Context.encode() {
|
||||||
|
hideSoftKeyboard(contentView)
|
||||||
|
val content = getContent() ?: return
|
||||||
|
contentView.post {
|
||||||
|
startActivity(newBarcodeIntent(content))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun newBarcodeIntent(content: Any): Intent {
|
||||||
|
val format = writeableFormats[formatView.selectedItemPosition]
|
||||||
|
return BarcodeActivity.newIntent(
|
||||||
|
this,
|
||||||
|
content,
|
||||||
|
format,
|
||||||
|
format.getErrorCorrectionLevel(ecSpinner.selectedItemPosition),
|
||||||
|
if (addQuietZoneSwitch.isChecked) {
|
||||||
|
1
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
},
|
||||||
|
if (format.canBeInverted()) {
|
||||||
|
colorsSpinner.selectedItemPosition
|
||||||
|
} else 0
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Context.getContent(): Any? {
|
||||||
|
if (bytes != null) {
|
||||||
|
return bytes
|
||||||
|
}
|
||||||
|
var text = contentView.text.toString()
|
||||||
|
if (text.isEmpty()) {
|
||||||
|
toast(R.string.error_no_content)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
if (!unescapeCheckBox.isChecked) {
|
||||||
|
return text
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
text = text.unescape()
|
||||||
|
} catch (e: IllegalArgumentException) {
|
||||||
|
toast(e.message ?: "Invalid escape sequence")
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
// Return a ByteArray if there were escape sequences for
|
||||||
|
// non-printable characters.
|
||||||
|
return if (text.any { it.code < 32 && it.code !in setOf(9, 10, 13) }) {
|
||||||
|
// Do not expand UTF-8 sequences in binary data.
|
||||||
|
text.toByteArray(Charsets.ISO_8859_1)
|
||||||
|
} else {
|
||||||
|
text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setEncodeByteArray() {
|
||||||
|
contentView.text = null
|
||||||
|
contentView.hint = getString(R.string.binary_data)
|
||||||
|
contentView.isEnabled = false
|
||||||
|
unescapeCheckBox.isEnabled = false
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getExtrasFromDeeplink(): Bundle? {
|
||||||
|
val data = intent?.dataString ?: return null
|
||||||
|
if (!isEncodeDeeplink(data)) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
val uri = data.toUri()
|
||||||
|
return Bundle().apply {
|
||||||
|
uri.getQueryParameter("content")?.let {
|
||||||
|
putString(CONTENT_TEXT, it)
|
||||||
|
}
|
||||||
|
uri.getQueryParameter("format")?.let {
|
||||||
|
putString(FORMAT, it)
|
||||||
|
}
|
||||||
|
putBoolean(
|
||||||
|
EXECUTE,
|
||||||
|
uri.getQueryParameter("execute").let {
|
||||||
|
it == "" || it?.toBoolean() == true
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val CONTENT_TEXT = "content_text"
|
||||||
|
private const val CONTENT_RAW = "content_raw"
|
||||||
|
private const val FORMAT = "format"
|
||||||
|
private const val EXECUTE = "execute"
|
||||||
|
private const val PICK_FILE_RESULT_CODE = 1
|
||||||
|
|
||||||
|
fun <T> newIntent(
|
||||||
|
context: Context,
|
||||||
|
content: T? = null,
|
||||||
|
format: String? = null,
|
||||||
|
execute: Boolean = false
|
||||||
|
): Intent {
|
||||||
|
val args = Bundle()
|
||||||
|
content?.let {
|
||||||
|
when (content) {
|
||||||
|
is String -> args.putString(CONTENT_TEXT, content)
|
||||||
|
is ByteArray -> args.putByteArray(CONTENT_RAW, content)
|
||||||
|
else -> throw IllegalArgumentException(
|
||||||
|
"content must be a String or a ByteArray"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
args.putBoolean(EXECUTE, execute)
|
||||||
|
}
|
||||||
|
format?.let { args.putString(FORMAT, it) }
|
||||||
|
return Intent(context, EncodeActivity::class.java).apply {
|
||||||
|
putExtras(args)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Boolean.setVisibilityFor(vararg views: View) {
|
||||||
|
val visibility = if (this) View.VISIBLE else View.GONE
|
||||||
|
for (view in views) {
|
||||||
|
view.visibility = visibility
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun BarcodeFormat.packEcLevel(packed: Int, level: Int): Int {
|
||||||
|
val s = ecLevelShift()
|
||||||
|
return (level shl s) or (packed and (15 shl s).inv())
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun BarcodeFormat.unpackEcLevel(packed: Int) =
|
||||||
|
(packed shr ecLevelShift()) and 15
|
||||||
|
|
||||||
|
private fun BarcodeFormat.ecLevelShift() = when (this) {
|
||||||
|
BarcodeFormat.Aztec -> 0
|
||||||
|
BarcodeFormat.QRCode -> 4
|
||||||
|
BarcodeFormat.MicroQRCode -> 3
|
||||||
|
BarcodeFormat.RMQRCode -> 2
|
||||||
|
BarcodeFormat.PDF417 -> 8
|
||||||
|
else -> throw IllegalArgumentException("$this does not have error levels")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun BarcodeFormat.canBeInverted() = when (this) {
|
||||||
|
BarcodeFormat.Aztec,
|
||||||
|
BarcodeFormat.DataMatrix,
|
||||||
|
BarcodeFormat.QRCode,
|
||||||
|
BarcodeFormat.MicroQRCode,
|
||||||
|
BarcodeFormat.RMQRCode -> true
|
||||||
|
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun String.toFormat(
|
||||||
|
default: BarcodeFormat = BarcodeFormat.QRCode
|
||||||
|
): BarcodeFormat = try {
|
||||||
|
BarcodeFormat.valueOf(this)
|
||||||
|
} catch (_: IllegalArgumentException) {
|
||||||
|
default
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun BarcodeFormat.getErrorCorrectionLevel(position: Int) = when (this) {
|
||||||
|
BarcodeFormat.Aztec -> position
|
||||||
|
BarcodeFormat.QRCode,
|
||||||
|
BarcodeFormat.MicroQRCode -> (position + 1) * 2
|
||||||
|
|
||||||
|
BarcodeFormat.RMQRCode -> (position + 1) * 4
|
||||||
|
BarcodeFormat.PDF417 -> position
|
||||||
|
else -> 0
|
||||||
|
}.coerceIn(0, 8)
|
||||||
|
|
||||||
|
private fun Spinner.setEntries(resId: Int) = ArrayAdapter.createFromResource(
|
||||||
|
this.context,
|
||||||
|
resId,
|
||||||
|
android.R.layout.simple_spinner_item
|
||||||
|
).also { aa ->
|
||||||
|
aa.setDropDownViewResource(
|
||||||
|
android.R.layout.simple_spinner_dropdown_item
|
||||||
|
)
|
||||||
|
adapter = aa
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun InputStream.readBytesMax(max: Int): ByteArray {
|
||||||
|
var offset = 0
|
||||||
|
var remaining = min(available(), max)
|
||||||
|
val result = ByteArray(remaining)
|
||||||
|
while (remaining > 0) {
|
||||||
|
val read = read(result, offset, remaining)
|
||||||
|
if (read < 0) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
remaining -= read
|
||||||
|
offset += read
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
+149
-100
@@ -1,4 +1,4 @@
|
|||||||
package de.markusfisch.android.binaryeye.fragment
|
package de.markusfisch.android.binaryeye.activity
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.app.AlertDialog
|
import android.app.AlertDialog
|
||||||
@@ -8,29 +8,46 @@ import android.database.Cursor
|
|||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import android.support.v4.app.ActivityCompat
|
import android.view.LayoutInflater
|
||||||
import android.support.v4.app.Fragment
|
import android.view.Menu
|
||||||
import android.support.v4.content.ContextCompat
|
import android.view.MenuInflater
|
||||||
import android.support.v7.app.AppCompatActivity
|
import android.view.MenuItem
|
||||||
import android.support.v7.view.ActionMode
|
import android.view.View
|
||||||
import android.support.v7.widget.SearchView
|
|
||||||
import android.support.v7.widget.SwitchCompat
|
|
||||||
import android.view.*
|
|
||||||
import android.widget.EditText
|
import android.widget.EditText
|
||||||
import android.widget.ListView
|
import android.widget.ListView
|
||||||
|
import androidx.appcompat.view.ActionMode
|
||||||
|
import androidx.appcompat.widget.SearchView
|
||||||
|
import androidx.appcompat.widget.SwitchCompat
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
import de.markusfisch.android.binaryeye.R
|
import de.markusfisch.android.binaryeye.R
|
||||||
import de.markusfisch.android.binaryeye.adapter.ScansAdapter
|
import de.markusfisch.android.binaryeye.adapter.ScansAdapter
|
||||||
import de.markusfisch.android.binaryeye.app.*
|
import de.markusfisch.android.binaryeye.app.alertDialog
|
||||||
|
import de.markusfisch.android.binaryeye.app.db
|
||||||
|
import de.markusfisch.android.binaryeye.app.hasWritePermission
|
||||||
|
import de.markusfisch.android.binaryeye.app.prefs
|
||||||
import de.markusfisch.android.binaryeye.content.copyToClipboard
|
import de.markusfisch.android.binaryeye.content.copyToClipboard
|
||||||
|
import de.markusfisch.android.binaryeye.content.shareAsFile
|
||||||
import de.markusfisch.android.binaryeye.content.shareText
|
import de.markusfisch.android.binaryeye.content.shareText
|
||||||
import de.markusfisch.android.binaryeye.database.*
|
import de.markusfisch.android.binaryeye.content.wipeLastShareFile
|
||||||
|
import de.markusfisch.android.binaryeye.database.Database
|
||||||
|
import de.markusfisch.android.binaryeye.database.exportCsv
|
||||||
|
import de.markusfisch.android.binaryeye.database.exportDatabase
|
||||||
|
import de.markusfisch.android.binaryeye.database.exportJson
|
||||||
import de.markusfisch.android.binaryeye.io.askForFileName
|
import de.markusfisch.android.binaryeye.io.askForFileName
|
||||||
import de.markusfisch.android.binaryeye.io.toSaveResult
|
import de.markusfisch.android.binaryeye.io.toSaveResult
|
||||||
import de.markusfisch.android.binaryeye.view.*
|
import de.markusfisch.android.binaryeye.view.lockStatusBarColor
|
||||||
|
import de.markusfisch.android.binaryeye.view.setPaddingFromWindowInsets
|
||||||
|
import de.markusfisch.android.binaryeye.view.systemBarListViewScrollListener
|
||||||
|
import de.markusfisch.android.binaryeye.view.unlockStatusBarColor
|
||||||
|
import de.markusfisch.android.binaryeye.view.useVisibility
|
||||||
import de.markusfisch.android.binaryeye.widget.toast
|
import de.markusfisch.android.binaryeye.widget.toast
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
class HistoryFragment : Fragment() {
|
class HistoryActivity : AbstractBaseActivity() {
|
||||||
private lateinit var useHistorySwitch: SwitchCompat
|
private lateinit var useHistorySwitch: SwitchCompat
|
||||||
private lateinit var listView: ListView
|
private lateinit var listView: ListView
|
||||||
private lateinit var fab: View
|
private lateinit var fab: View
|
||||||
@@ -47,9 +64,11 @@ class HistoryFragment : Fragment() {
|
|||||||
R.menu.fragment_history_edit,
|
R.menu.fragment_history_edit,
|
||||||
menu
|
menu
|
||||||
)
|
)
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
// SDK35+ doesn't draw a status bar background anymore.
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.VANILLA_ICE_CREAM) {
|
||||||
lockStatusBarColor()
|
lockStatusBarColor()
|
||||||
val ac = activity ?: return false
|
val ac = this@HistoryActivity
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
ac.window.statusBarColor = ContextCompat.getColor(
|
ac.window.statusBarColor = ContextCompat.getColor(
|
||||||
ac,
|
ac,
|
||||||
R.color.accent_dark
|
R.color.accent_dark
|
||||||
@@ -69,7 +88,7 @@ class HistoryFragment : Fragment() {
|
|||||||
mode: ActionMode,
|
mode: ActionMode,
|
||||||
item: MenuItem
|
item: MenuItem
|
||||||
): Boolean {
|
): Boolean {
|
||||||
val ac = activity ?: return false
|
val ac = this@HistoryActivity
|
||||||
return when (item.itemId) {
|
return when (item.itemId) {
|
||||||
R.id.copy_scan -> {
|
R.id.copy_scan -> {
|
||||||
scansAdapter?.getSelectedContent("\n")?.let {
|
scansAdapter?.getSelectedContent("\n")?.let {
|
||||||
@@ -79,7 +98,8 @@ class HistoryFragment : Fragment() {
|
|||||||
closeActionMode()
|
closeActionMode()
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
R.id.edit_scan -> {
|
|
||||||
|
R.id.edit_label -> {
|
||||||
scansAdapter?.forSelection { id, position ->
|
scansAdapter?.forSelection { id, position ->
|
||||||
ac.askForName(
|
ac.askForName(
|
||||||
id,
|
id,
|
||||||
@@ -90,6 +110,7 @@ class HistoryFragment : Fragment() {
|
|||||||
closeActionMode()
|
closeActionMode()
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
R.id.remove_scan -> {
|
R.id.remove_scan -> {
|
||||||
scansAdapter?.getSelectedIds()?.let {
|
scansAdapter?.getSelectedIds()?.let {
|
||||||
if (it.isNotEmpty()) {
|
if (it.isNotEmpty()) {
|
||||||
@@ -99,6 +120,7 @@ class HistoryFragment : Fragment() {
|
|||||||
closeActionMode()
|
closeActionMode()
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> false
|
else -> false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -114,39 +136,30 @@ class HistoryFragment : Fragment() {
|
|||||||
private var filter: String? = null
|
private var filter: String? = null
|
||||||
private var clearListMenuItem: MenuItem? = null
|
private var clearListMenuItem: MenuItem? = null
|
||||||
private var exportHistoryMenuItem: MenuItem? = null
|
private var exportHistoryMenuItem: MenuItem? = null
|
||||||
|
private var shareAsFileMenuItem: MenuItem? = null
|
||||||
|
|
||||||
override fun onCreate(state: Bundle?) {
|
override fun onCreate(state: Bundle?) {
|
||||||
super.onCreate(state)
|
super.onCreate(state)
|
||||||
setHasOptionsMenu(true)
|
setScreenContentView(R.layout.activity_history)
|
||||||
}
|
setTitle(R.string.history)
|
||||||
|
|
||||||
override fun onCreateView(
|
useHistorySwitch = findViewById(
|
||||||
inflater: LayoutInflater,
|
|
||||||
container: ViewGroup?,
|
|
||||||
state: Bundle?
|
|
||||||
): View? {
|
|
||||||
val ac = activity ?: return null
|
|
||||||
ac.setTitle(R.string.history)
|
|
||||||
|
|
||||||
val view = inflater.inflate(
|
|
||||||
R.layout.fragment_history,
|
|
||||||
container,
|
|
||||||
false
|
|
||||||
)
|
|
||||||
|
|
||||||
useHistorySwitch = view.findViewById(
|
|
||||||
R.id.use_history
|
R.id.use_history
|
||||||
) as SwitchCompat
|
)
|
||||||
initHistorySwitch(useHistorySwitch)
|
initHistorySwitch(useHistorySwitch)
|
||||||
|
|
||||||
listView = view.findViewById(R.id.scans)
|
listView = findViewById(R.id.scans)
|
||||||
listView.setOnItemClickListener { _, _, _, id ->
|
listView.setOnItemClickListener { _, v, position, id ->
|
||||||
showScan(id)
|
if (actionMode != null) {
|
||||||
|
scansAdapter?.select(v, id, position)
|
||||||
|
} else {
|
||||||
|
showScan(id)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
listView.setOnItemLongClickListener { _, v, position, id ->
|
listView.setOnItemLongClickListener { _, v, position, id ->
|
||||||
scansAdapter?.select(v, id, position)
|
scansAdapter?.select(v, id, position)
|
||||||
if (actionMode == null && ac is AppCompatActivity) {
|
if (actionMode == null) {
|
||||||
actionMode = ac.delegate.startSupportActionMode(
|
actionMode = delegate.startSupportActionMode(
|
||||||
actionModeCallback
|
actionModeCallback
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -154,25 +167,27 @@ class HistoryFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
listView.setOnScrollListener(systemBarListViewScrollListener)
|
listView.setOnScrollListener(systemBarListViewScrollListener)
|
||||||
|
|
||||||
fab = view.findViewById(R.id.share)
|
fab = findViewById(R.id.share)
|
||||||
fab.setOnClickListener { v ->
|
fab.setOnClickListener { v ->
|
||||||
v.context.pickListSeparatorAndShare()
|
v.context.pickListSeparatorAndShare(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
progressView = view.findViewById(R.id.progress_view)
|
progressView = findViewById(R.id.progress_view)
|
||||||
|
|
||||||
(view.findViewById(R.id.inset_layout) as View).setPaddingFromWindowInsets()
|
findViewById<View>(R.id.inset_layout).setPaddingFromWindowInsets()
|
||||||
listView.setPaddingFromWindowInsets()
|
listView.setPaddingFromWindowInsets()
|
||||||
|
|
||||||
update()
|
|
||||||
|
|
||||||
return view
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
scansAdapter?.changeCursor(null)
|
scansAdapter?.changeCursor(null)
|
||||||
parentJob.cancel()
|
parentJob.cancel()
|
||||||
|
wipeLastShareFile()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
update()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPause() {
|
override fun onPause() {
|
||||||
@@ -182,49 +197,61 @@ class HistoryFragment : Fragment() {
|
|||||||
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||||
inflater.inflate(R.menu.fragment_history, menu)
|
inflater.inflate(R.menu.fragment_history, menu)
|
||||||
initSearchView(menu.findItem(R.id.search))
|
val searchMenuItem = menu.findItem(R.id.search)
|
||||||
menu.setGroupVisible(R.id.scans_available, scansAdapter?.count != 0)
|
initSearchView(searchMenuItem)
|
||||||
clearListMenuItem = menu.findItem(R.id.clear)
|
val visible = scansAdapter?.count != 0
|
||||||
exportHistoryMenuItem = menu.findItem(R.id.export_history)
|
searchMenuItem?.isVisible = visible
|
||||||
|
exportHistoryMenuItem = menu.findItem(R.id.export_history)?.apply {
|
||||||
|
isVisible = visible
|
||||||
|
}
|
||||||
|
shareAsFileMenuItem = menu.findItem(R.id.share_as_file)?.apply {
|
||||||
|
isVisible = visible
|
||||||
|
}
|
||||||
|
clearListMenuItem = menu.findItem(R.id.clear)?.apply {
|
||||||
|
isVisible = visible
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun initSearchView(item: MenuItem?) {
|
private fun initSearchView(item: MenuItem?) {
|
||||||
item ?: return
|
item ?: return
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) {
|
val ac = this
|
||||||
item.isVisible = false
|
val searchView = item.actionView as SearchView
|
||||||
} else {
|
val searchManager = ac.getSystemService(
|
||||||
val ac = activity ?: return
|
Context.SEARCH_SERVICE
|
||||||
val searchView = item.actionView as SearchView
|
) as SearchManager
|
||||||
val searchManager = ac.getSystemService(
|
searchView.setSearchableInfo(
|
||||||
Context.SEARCH_SERVICE
|
searchManager.getSearchableInfo(ac.componentName)
|
||||||
) as SearchManager
|
)
|
||||||
searchView.setSearchableInfo(
|
searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
|
||||||
searchManager.getSearchableInfo(ac.componentName)
|
override fun onQueryTextSubmit(query: String): Boolean {
|
||||||
)
|
update(query)
|
||||||
searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
|
return false
|
||||||
override fun onQueryTextSubmit(query: String): Boolean {
|
}
|
||||||
update(query)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onQueryTextChange(query: String): Boolean {
|
override fun onQueryTextChange(query: String): Boolean {
|
||||||
update(query)
|
update(query)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||||
return when (item.itemId) {
|
return when (item.itemId) {
|
||||||
R.id.clear -> {
|
R.id.clear -> {
|
||||||
context.askToRemoveScans()
|
this.askToRemoveScans()
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
R.id.export_history -> {
|
R.id.export_history -> {
|
||||||
askToExportToFile()
|
askToExportToFile()
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
R.id.share_as_file -> {
|
||||||
|
this.pickListSeparatorAndShare(true)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
else -> super.onOptionsItemSelected(item)
|
else -> super.onOptionsItemSelected(item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -248,13 +275,13 @@ class HistoryFragment : Fragment() {
|
|||||||
scope.launch {
|
scope.launch {
|
||||||
val cursor = db.getScans(filter)
|
val cursor = db.getScans(filter)
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
val ac = activity ?: return@withContext
|
val ac = this@HistoryActivity
|
||||||
val hasScans = cursor != null && cursor.count > 0
|
val hasScans = cursor.count > 0
|
||||||
if (filter == null) {
|
if (filter == null) {
|
||||||
if (!hasScans) {
|
if (!hasScans) {
|
||||||
listView.emptyView = useHistorySwitch
|
listView.emptyView = useHistorySwitch
|
||||||
}
|
}
|
||||||
ActivityCompat.invalidateOptionsMenu(ac)
|
ac.invalidateOptionsMenu()
|
||||||
}
|
}
|
||||||
enableMenuItems(hasScans)
|
enableMenuItems(hasScans)
|
||||||
fab.visibility = if (hasScans) {
|
fab.visibility = if (hasScans) {
|
||||||
@@ -262,10 +289,19 @@ class HistoryFragment : Fragment() {
|
|||||||
} else {
|
} else {
|
||||||
View.GONE
|
View.GONE
|
||||||
}
|
}
|
||||||
cursor?.let { cursor ->
|
cursor.let { cursor ->
|
||||||
// Close previous cursor.
|
// Close previous cursor.
|
||||||
scansAdapter?.also { it.changeCursor(null) }
|
scansAdapter?.also { it.changeCursor(null) }
|
||||||
scansAdapter = ScansAdapter(ac, cursor)
|
scansAdapter = ScansAdapter(ac, cursor) { id, pinned ->
|
||||||
|
if (actionMode == null) {
|
||||||
|
scope.launch {
|
||||||
|
db.setPinned(id, pinned)
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
update()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
listView.adapter = scansAdapter
|
listView.adapter = scansAdapter
|
||||||
listViewState?.also {
|
listViewState?.also {
|
||||||
listView.onRestoreInstanceState(it)
|
listView.onRestoreInstanceState(it)
|
||||||
@@ -278,6 +314,7 @@ class HistoryFragment : Fragment() {
|
|||||||
private fun enableMenuItems(enabled: Boolean) {
|
private fun enableMenuItems(enabled: Boolean) {
|
||||||
clearListMenuItem?.isEnabled = enabled
|
clearListMenuItem?.isEnabled = enabled
|
||||||
exportHistoryMenuItem?.isEnabled = enabled
|
exportHistoryMenuItem?.isEnabled = enabled
|
||||||
|
shareAsFileMenuItem?.isEnabled = enabled
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun closeActionMode() {
|
private fun closeActionMode() {
|
||||||
@@ -291,8 +328,8 @@ class HistoryFragment : Fragment() {
|
|||||||
private fun showScan(id: Long) = db.getScan(id)?.also { scan ->
|
private fun showScan(id: Long) = db.getScan(id)?.also { scan ->
|
||||||
closeActionMode()
|
closeActionMode()
|
||||||
try {
|
try {
|
||||||
fragmentManager?.addFragment(DecodeFragment.newInstance(scan))
|
startActivity(DecodeActivity.newIntent(this, scan))
|
||||||
} catch (e: IllegalArgumentException) {
|
} catch (_: IllegalArgumentException) {
|
||||||
// Ignore, can never happen.
|
// Ignore, can never happen.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -365,7 +402,7 @@ class HistoryFragment : Fragment() {
|
|||||||
|
|
||||||
private fun askToExportToFile() {
|
private fun askToExportToFile() {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
val ac = activity ?: return@launch
|
val ac = this@HistoryActivity
|
||||||
progressView.useVisibility {
|
progressView.useVisibility {
|
||||||
// Write permission is only required before Android Q.
|
// Write permission is only required before Android Q.
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q &&
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q &&
|
||||||
@@ -393,12 +430,12 @@ class HistoryFragment : Fragment() {
|
|||||||
} ?: return@useVisibility
|
} ?: return@useVisibility
|
||||||
val message = when (delimiter) {
|
val message = when (delimiter) {
|
||||||
"db" -> ac.exportDatabase(name)
|
"db" -> ac.exportDatabase(name)
|
||||||
else -> db.getScansDetailed(filter)?.use {
|
else -> db.getScansDetailed(filter).use {
|
||||||
when (delimiter) {
|
when (delimiter) {
|
||||||
"json" -> ac.exportJson(name, it)
|
"json" -> ac.exportJson(name, it)
|
||||||
else -> ac.exportCsv(name, it, delimiter)
|
else -> ac.exportCsv(name, it, delimiter)
|
||||||
}
|
}
|
||||||
} ?: false
|
}
|
||||||
}.toSaveResult()
|
}.toSaveResult()
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
ac.toast(message)
|
ac.toast(message)
|
||||||
@@ -407,32 +444,44 @@ class HistoryFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun Context.pickListSeparatorAndShare() {
|
private fun Context.pickListSeparatorAndShare(asFile: Boolean) {
|
||||||
val separators = resources.getStringArray(
|
val separators = resources.getStringArray(
|
||||||
R.array.list_separators_values
|
R.array.list_separators_values
|
||||||
)
|
)
|
||||||
AlertDialog.Builder(this)
|
AlertDialog.Builder(this)
|
||||||
.setTitle(R.string.pick_list_separator)
|
.setTitle(R.string.pick_list_separator)
|
||||||
.setItems(R.array.list_separators_names) { _, which ->
|
.setItems(R.array.list_separators_names) { _, which ->
|
||||||
shareScans(separators[which])
|
shareScans(separators[which], asFile)
|
||||||
}
|
}
|
||||||
.show()
|
.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun shareScans(format: String) = scope.launch {
|
private fun shareScans(format: String, asFile: Boolean) = scope.launch {
|
||||||
progressView.useVisibility {
|
progressView.useVisibility {
|
||||||
var text: String? = null
|
val selectedIds = scansAdapter?.getSelectedIds()
|
||||||
db.getScansDetailed(filter)?.use { cursor ->
|
if (selectedIds?.isNotEmpty() == true) {
|
||||||
|
db.getScansDetailed(selectedIds.toLongArray())
|
||||||
|
} else {
|
||||||
|
db.getScansDetailed(filter)
|
||||||
|
}.use { cursor ->
|
||||||
val details = format.split(":")
|
val details = format.split(":")
|
||||||
text = when (details[0]) {
|
when (details[0]) {
|
||||||
"text" -> cursor.exportText(details[1])
|
"text" -> Pair(cursor.exportText(details[1]), "txt")
|
||||||
"csv" -> cursor.exportCsv(details[1])
|
"csv" -> Pair(cursor.exportCsv(details[1]), "csv")
|
||||||
else -> cursor.exportJson()
|
else -> Pair(cursor.exportJson(), "json")
|
||||||
}
|
}
|
||||||
}
|
}.let { (text, ext) ->
|
||||||
text?.let {
|
text?.let {
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
context.shareText(it)
|
if (asFile) {
|
||||||
|
this@HistoryActivity.shareAsFile(
|
||||||
|
it,
|
||||||
|
String.format("scans.%s", ext)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
this@HistoryActivity.shareText(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -441,7 +490,7 @@ class HistoryFragment : Fragment() {
|
|||||||
|
|
||||||
private fun Cursor.exportText(separator: String): String {
|
private fun Cursor.exportText(separator: String): String {
|
||||||
val sb = StringBuilder()
|
val sb = StringBuilder()
|
||||||
val contentIndex = getColumnIndex(Database.SCANS_CONTENT)
|
val contentIndex = getColumnIndex(Database.SCANS_TEXT)
|
||||||
if (contentIndex > -1 && moveToFirst()) {
|
if (contentIndex > -1 && moveToFirst()) {
|
||||||
do {
|
do {
|
||||||
val content = getString(contentIndex)
|
val content = getString(contentIndex)
|
||||||
@@ -0,0 +1,171 @@
|
|||||||
|
package de.markusfisch.android.binaryeye.activity
|
||||||
|
|
||||||
|
import android.app.AlertDialog
|
||||||
|
import android.app.Dialog
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.ArrayAdapter
|
||||||
|
import android.widget.EditText
|
||||||
|
import android.widget.ListView
|
||||||
|
import android.widget.Spinner
|
||||||
|
import android.widget.TextView
|
||||||
|
import de.markusfisch.android.binaryeye.R
|
||||||
|
import de.markusfisch.android.binaryeye.adapter.prettifyFormatName
|
||||||
|
import de.markusfisch.android.binaryeye.adapter.setupFormatSpinner
|
||||||
|
import de.markusfisch.android.binaryeye.app.prefs
|
||||||
|
import de.markusfisch.android.binaryeye.preference.IgnoreCode
|
||||||
|
import de.markusfisch.android.binaryeye.view.setPaddingFromWindowInsets
|
||||||
|
import de.markusfisch.android.binaryeye.view.systemBarListViewScrollListener
|
||||||
|
import de.markusfisch.android.binaryeye.widget.toast
|
||||||
|
|
||||||
|
class IgnoreCodesActivity : AbstractBaseActivity() {
|
||||||
|
private val patterns = ArrayList<IgnoreCode>()
|
||||||
|
|
||||||
|
private lateinit var adapter: PatternsAdapter
|
||||||
|
|
||||||
|
override fun onCreate(state: Bundle?) {
|
||||||
|
super.onCreate(state)
|
||||||
|
setScreenContentView(R.layout.activity_ignore_codes)
|
||||||
|
setTitle(R.string.ignore_codes)
|
||||||
|
|
||||||
|
patterns.addAll(prefs.ignoreCodes)
|
||||||
|
|
||||||
|
val listView = findViewById<ListView>(R.id.actions)
|
||||||
|
listView.emptyView = findViewById(R.id.no_actions)
|
||||||
|
findViewById<TextView>(R.id.no_actions).setText(
|
||||||
|
R.string.ignore_codes_empty
|
||||||
|
)
|
||||||
|
adapter = PatternsAdapter(patterns)
|
||||||
|
listView.adapter = adapter
|
||||||
|
listView.setOnScrollListener(systemBarListViewScrollListener)
|
||||||
|
listView.setOnItemClickListener { _, _, position, _ ->
|
||||||
|
editPattern(position)
|
||||||
|
}
|
||||||
|
listView.setOnItemLongClickListener { _, _, position, _ ->
|
||||||
|
confirmRemovePattern(position)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
findViewById<View>(R.id.add).apply {
|
||||||
|
contentDescription = getString(R.string.ignore_code_add)
|
||||||
|
setOnClickListener {
|
||||||
|
editPattern(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
findViewById<View>(R.id.inset_layout).setPaddingFromWindowInsets()
|
||||||
|
listView.setPaddingFromWindowInsets()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun editPattern(position: Int?) {
|
||||||
|
val pattern = position?.let { patterns[it] }
|
||||||
|
val view = layoutInflater.inflate(R.layout.dialog_ignore_code, null)
|
||||||
|
val regexView = view.findViewById<EditText>(R.id.regex)
|
||||||
|
val formatView = view.findViewById<Spinner>(R.id.format)
|
||||||
|
val values = setupFormatSpinner(formatView)
|
||||||
|
if (pattern != null) {
|
||||||
|
regexView.setText(pattern.pattern)
|
||||||
|
regexView.setSelection(pattern.pattern.length)
|
||||||
|
formatView.setSelection(
|
||||||
|
values.indexOf(pattern.format ?: "").coerceAtLeast(0)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
formatView.setSelection(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
val dialog = AlertDialog.Builder(this)
|
||||||
|
.setTitle(
|
||||||
|
if (pattern == null) {
|
||||||
|
R.string.ignore_code_add
|
||||||
|
} else {
|
||||||
|
R.string.ignore_code_edit
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.setView(view)
|
||||||
|
.setPositiveButton(android.R.string.ok, null)
|
||||||
|
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
||||||
|
.create()
|
||||||
|
|
||||||
|
dialog.setOnShowListener {
|
||||||
|
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener {
|
||||||
|
addOrUpdatePattern(position, regexView, formatView, values, dialog)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dialog.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun addOrUpdatePattern(
|
||||||
|
position: Int?,
|
||||||
|
regexView: EditText,
|
||||||
|
formatView: Spinner,
|
||||||
|
values: List<String>,
|
||||||
|
dialog: Dialog,
|
||||||
|
) {
|
||||||
|
val pattern = regexView.text.toString().trim()
|
||||||
|
if (pattern.isEmpty()) {
|
||||||
|
regexView.error = getString(R.string.ignore_code_regex_required)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
Regex(pattern)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
val message = e.message ?: getString(
|
||||||
|
R.string.ignore_code_regex_invalid
|
||||||
|
)
|
||||||
|
regexView.error = message
|
||||||
|
toast(message)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val item = IgnoreCode(
|
||||||
|
pattern = pattern,
|
||||||
|
format = values[formatView.selectedItemPosition].ifEmpty { null }
|
||||||
|
)
|
||||||
|
if (position == null) {
|
||||||
|
patterns.add(item)
|
||||||
|
} else {
|
||||||
|
patterns[position] = item
|
||||||
|
}
|
||||||
|
prefs.setIgnoreCodes(patterns)
|
||||||
|
adapter.notifyDataSetChanged()
|
||||||
|
dialog.dismiss()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun confirmRemovePattern(position: Int) {
|
||||||
|
AlertDialog.Builder(this)
|
||||||
|
.setMessage(R.string.ignore_code_remove_confirm)
|
||||||
|
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||||
|
patterns.removeAt(position)
|
||||||
|
prefs.setIgnoreCodes(patterns)
|
||||||
|
adapter.notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
private inner class PatternsAdapter(
|
||||||
|
items: List<IgnoreCode>
|
||||||
|
) : ArrayAdapter<IgnoreCode>(
|
||||||
|
this,
|
||||||
|
android.R.layout.simple_list_item_2,
|
||||||
|
android.R.id.text1,
|
||||||
|
items
|
||||||
|
) {
|
||||||
|
override fun getView(
|
||||||
|
position: Int,
|
||||||
|
convertView: View?,
|
||||||
|
parent: ViewGroup
|
||||||
|
): View {
|
||||||
|
val view = super.getView(position, convertView, parent)
|
||||||
|
val item = patterns[position]
|
||||||
|
val title = view.findViewById<TextView>(android.R.id.text1)
|
||||||
|
val subtitle = view.findViewById<TextView>(android.R.id.text2)
|
||||||
|
title.text = item.pattern
|
||||||
|
subtitle.text = item.format?.prettifyFormatName() ?: getString(
|
||||||
|
R.string.all_formats
|
||||||
|
)
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,132 +0,0 @@
|
|||||||
package de.markusfisch.android.binaryeye.activity
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.content.pm.PackageManager
|
|
||||||
import android.os.Build
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.support.v4.app.Fragment
|
|
||||||
import android.support.v7.app.AppCompatActivity
|
|
||||||
import android.support.v7.widget.Toolbar
|
|
||||||
import de.markusfisch.android.binaryeye.R
|
|
||||||
import de.markusfisch.android.binaryeye.app.*
|
|
||||||
import de.markusfisch.android.binaryeye.database.Scan
|
|
||||||
import de.markusfisch.android.binaryeye.fragment.DecodeFragment
|
|
||||||
import de.markusfisch.android.binaryeye.fragment.EncodeFragment
|
|
||||||
import de.markusfisch.android.binaryeye.fragment.HistoryFragment
|
|
||||||
import de.markusfisch.android.binaryeye.fragment.PreferencesFragment
|
|
||||||
import de.markusfisch.android.binaryeye.view.colorSystemAndToolBars
|
|
||||||
import de.markusfisch.android.binaryeye.view.initSystemBars
|
|
||||||
import de.markusfisch.android.binaryeye.view.recordToolbarHeight
|
|
||||||
|
|
||||||
class MainActivity : AppCompatActivity() {
|
|
||||||
override fun onRequestPermissionsResult(
|
|
||||||
requestCode: Int,
|
|
||||||
permissions: Array<String>,
|
|
||||||
grantResults: IntArray
|
|
||||||
) {
|
|
||||||
when (requestCode) {
|
|
||||||
PERMISSION_LOCATION, PERMISSION_WRITE -> if (grantResults.isNotEmpty() &&
|
|
||||||
grantResults[0] == PackageManager.PERMISSION_GRANTED
|
|
||||||
) {
|
|
||||||
permissionGrantedCallback?.invoke()
|
|
||||||
permissionGrantedCallback = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onSupportNavigateUp(): Boolean {
|
|
||||||
val fm = supportFragmentManager
|
|
||||||
if (fm != null && fm.backStackEntryCount > 0) {
|
|
||||||
fm.popBackStack()
|
|
||||||
} else {
|
|
||||||
finish()
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun attachBaseContext(base: Context?) {
|
|
||||||
base?.applyLocale(prefs.customLocale)
|
|
||||||
super.attachBaseContext(base)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreate(state: Bundle?) {
|
|
||||||
super.onCreate(state)
|
|
||||||
setContentView(R.layout.activity_main)
|
|
||||||
|
|
||||||
initSystemBars(this)
|
|
||||||
val toolbar = findViewById(R.id.toolbar) as Toolbar
|
|
||||||
recordToolbarHeight(toolbar)
|
|
||||||
setSupportActionBar(toolbar)
|
|
||||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
|
||||||
|
|
||||||
supportFragmentManager.addOnBackStackChangedListener {
|
|
||||||
colorSystemAndToolBars(this@MainActivity)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (state == null) {
|
|
||||||
supportFragmentManager?.setFragment(getFragmentForIntent(intent))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private const val PREFERENCES = "preferences"
|
|
||||||
private const val HISTORY = "history"
|
|
||||||
private const val ENCODE = "encode"
|
|
||||||
const val DECODED = "decoded"
|
|
||||||
|
|
||||||
private fun getFragmentForIntent(intent: Intent?): Fragment {
|
|
||||||
intent ?: return PreferencesFragment()
|
|
||||||
return when {
|
|
||||||
intent.hasExtra(PREFERENCES) -> PreferencesFragment()
|
|
||||||
intent.hasExtra(HISTORY) -> HistoryFragment()
|
|
||||||
intent.hasExtra(ENCODE) -> EncodeFragment.newInstance(
|
|
||||||
intent.getStringExtra(ENCODE)
|
|
||||||
)
|
|
||||||
intent.hasExtra(DECODED) -> DecodeFragment.newInstance(
|
|
||||||
intent.getParcelableExtra(DECODED)!!
|
|
||||||
)
|
|
||||||
else -> PreferencesFragment()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getPreferencesIntent(context: Context): Intent {
|
|
||||||
val intent = Intent(context, MainActivity::class.java)
|
|
||||||
intent.putExtra(PREFERENCES, true)
|
|
||||||
return intent
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getHistoryIntent(context: Context): Intent {
|
|
||||||
val intent = Intent(context, MainActivity::class.java)
|
|
||||||
intent.putExtra(HISTORY, true)
|
|
||||||
return intent
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getEncodeIntent(
|
|
||||||
context: Context,
|
|
||||||
text: String? = "",
|
|
||||||
isExternal: Boolean = false
|
|
||||||
): Intent {
|
|
||||||
val intent = Intent(context, MainActivity::class.java)
|
|
||||||
intent.putExtra(ENCODE, text)
|
|
||||||
if (isExternal) {
|
|
||||||
val flagActivityClearTask =
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
|
|
||||||
Intent.FLAG_ACTIVITY_CLEAR_TASK
|
|
||||||
} else 0
|
|
||||||
intent.addFlags(
|
|
||||||
Intent.FLAG_ACTIVITY_NO_HISTORY or
|
|
||||||
flagActivityClearTask or
|
|
||||||
Intent.FLAG_ACTIVITY_NEW_TASK
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return intent
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getDecodeIntent(context: Context, scan: Scan): Intent {
|
|
||||||
val intent = Intent(context, MainActivity::class.java)
|
|
||||||
intent.putExtra(DECODED, scan)
|
|
||||||
return intent
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+17
-28
@@ -1,41 +1,32 @@
|
|||||||
package de.markusfisch.android.binaryeye.fragment
|
package de.markusfisch.android.binaryeye.activity
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.wifi.WifiManager
|
import android.net.wifi.WifiManager
|
||||||
import android.net.wifi.WifiNetworkSuggestion
|
import android.net.wifi.WifiNetworkSuggestion
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.support.annotation.RequiresApi
|
import android.view.View
|
||||||
import android.support.v4.app.Fragment
|
|
||||||
import android.view.*
|
|
||||||
import android.widget.ArrayAdapter
|
import android.widget.ArrayAdapter
|
||||||
import android.widget.ListView
|
import android.widget.ListView
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
|
import androidx.core.util.size
|
||||||
import de.markusfisch.android.binaryeye.R
|
import de.markusfisch.android.binaryeye.R
|
||||||
import de.markusfisch.android.binaryeye.view.setPaddingFromWindowInsets
|
import de.markusfisch.android.binaryeye.view.setPaddingFromWindowInsets
|
||||||
import de.markusfisch.android.binaryeye.view.systemBarListViewScrollListener
|
import de.markusfisch.android.binaryeye.view.systemBarListViewScrollListener
|
||||||
import de.markusfisch.android.binaryeye.widget.toast
|
import de.markusfisch.android.binaryeye.widget.toast
|
||||||
|
|
||||||
class NetworkSuggestionsFragment : Fragment() {
|
class NetworkSuggestionsActivity : AbstractBaseActivity() {
|
||||||
@RequiresApi(Build.VERSION_CODES.R)
|
@RequiresApi(Build.VERSION_CODES.R)
|
||||||
override fun onCreateView(
|
override fun onCreate(state: Bundle?) {
|
||||||
inflater: LayoutInflater,
|
super.onCreate(state)
|
||||||
container: ViewGroup?,
|
setScreenContentView(R.layout.activity_network_suggestions)
|
||||||
state: Bundle?
|
setTitle(R.string.network_suggestions)
|
||||||
): View? {
|
|
||||||
val ac = activity ?: return null
|
|
||||||
ac.setTitle(R.string.network_suggestions)
|
|
||||||
|
|
||||||
val view = inflater.inflate(
|
val wm = applicationContext.getSystemService(
|
||||||
R.layout.fragment_network_suggestions,
|
|
||||||
container,
|
|
||||||
false
|
|
||||||
)
|
|
||||||
|
|
||||||
val wm = ac.applicationContext.getSystemService(
|
|
||||||
Context.WIFI_SERVICE
|
Context.WIFI_SERVICE
|
||||||
) as WifiManager
|
) as WifiManager
|
||||||
val suggestionArrayAdapter = ArrayAdapter(
|
val suggestionArrayAdapter = ArrayAdapter(
|
||||||
ac,
|
this,
|
||||||
android.R.layout.simple_list_item_checked,
|
android.R.layout.simple_list_item_checked,
|
||||||
wm.networkSuggestions.map {
|
wm.networkSuggestions.map {
|
||||||
Suggestion(
|
Suggestion(
|
||||||
@@ -45,18 +36,18 @@ class NetworkSuggestionsFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
val listView = view.findViewById<ListView>(R.id.suggestions)
|
val listView = findViewById<ListView>(R.id.suggestions)
|
||||||
listView.emptyView = view.findViewById(R.id.no_suggestions)
|
listView.emptyView = findViewById(R.id.no_suggestions)
|
||||||
listView.adapter = suggestionArrayAdapter
|
listView.adapter = suggestionArrayAdapter
|
||||||
listView.setOnScrollListener(systemBarListViewScrollListener)
|
listView.setOnScrollListener(systemBarListViewScrollListener)
|
||||||
(view.findViewById(R.id.inset_layout) as View).setPaddingFromWindowInsets()
|
findViewById<View>(R.id.inset_layout).setPaddingFromWindowInsets()
|
||||||
listView.setPaddingFromWindowInsets()
|
listView.setPaddingFromWindowInsets()
|
||||||
|
|
||||||
view.findViewById<View>(R.id.remove).setOnClickListener {
|
findViewById<View>(R.id.remove).setOnClickListener {
|
||||||
val removeList = ArrayList<WifiNetworkSuggestion>()
|
val removeList = ArrayList<WifiNetworkSuggestion>()
|
||||||
val removeFromAdapter = ArrayList<Suggestion>()
|
val removeFromAdapter = ArrayList<Suggestion>()
|
||||||
val checked = listView.checkedItemPositions
|
val checked = listView.checkedItemPositions
|
||||||
val size = checked.size()
|
val size = checked.size
|
||||||
for (i in 0 until size) {
|
for (i in 0 until size) {
|
||||||
if (checked.valueAt(i)) {
|
if (checked.valueAt(i)) {
|
||||||
val pos = checked.keyAt(i)
|
val pos = checked.keyAt(i)
|
||||||
@@ -74,13 +65,11 @@ class NetworkSuggestionsFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
wm.removeNetworkSuggestions(removeList)
|
wm.removeNetworkSuggestions(removeList)
|
||||||
} else {
|
} else {
|
||||||
ac.toast(
|
toast(
|
||||||
R.string.clear_network_suggestions_nothing_to_remove
|
R.string.clear_network_suggestions_nothing_to_remove
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return view
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -7,12 +7,12 @@ import android.graphics.Matrix
|
|||||||
import android.graphics.Rect
|
import android.graphics.Rect
|
||||||
import android.graphics.RectF
|
import android.graphics.RectF
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Parcelable
|
|
||||||
import android.support.v7.app.AppCompatActivity
|
|
||||||
import android.support.v7.widget.Toolbar
|
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
|
import android.view.View
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import de.markusfisch.android.binaryeye.R
|
import de.markusfisch.android.binaryeye.R
|
||||||
import de.markusfisch.android.binaryeye.app.applyLocale
|
import de.markusfisch.android.binaryeye.app.applyLocale
|
||||||
import de.markusfisch.android.binaryeye.app.prefs
|
import de.markusfisch.android.binaryeye.app.prefs
|
||||||
@@ -21,15 +21,22 @@ import de.markusfisch.android.binaryeye.graphics.fixTransparency
|
|||||||
import de.markusfisch.android.binaryeye.graphics.loadImageUri
|
import de.markusfisch.android.binaryeye.graphics.loadImageUri
|
||||||
import de.markusfisch.android.binaryeye.graphics.mapPosition
|
import de.markusfisch.android.binaryeye.graphics.mapPosition
|
||||||
import de.markusfisch.android.binaryeye.media.releaseToneGenerators
|
import de.markusfisch.android.binaryeye.media.releaseToneGenerators
|
||||||
import de.markusfisch.android.binaryeye.view.*
|
import de.markusfisch.android.binaryeye.view.colorSystemAndToolBars
|
||||||
|
import de.markusfisch.android.binaryeye.view.initBars
|
||||||
|
import de.markusfisch.android.binaryeye.view.scanFeedback
|
||||||
|
import de.markusfisch.android.binaryeye.view.setPaddingFromWindowInsets
|
||||||
import de.markusfisch.android.binaryeye.widget.CropImageView
|
import de.markusfisch.android.binaryeye.widget.CropImageView
|
||||||
import de.markusfisch.android.binaryeye.widget.DetectorView
|
import de.markusfisch.android.binaryeye.widget.DetectorView
|
||||||
import de.markusfisch.android.binaryeye.widget.toast
|
import de.markusfisch.android.binaryeye.widget.toast
|
||||||
import de.markusfisch.android.zxingcpp.ZxingCpp
|
import de.markusfisch.android.zxingcpp.ZxingCpp
|
||||||
import de.markusfisch.android.zxingcpp.ZxingCpp.Binarizer
|
import de.markusfisch.android.zxingcpp.ZxingCpp.Binarizer
|
||||||
import de.markusfisch.android.zxingcpp.ZxingCpp.DecodeHints
|
import de.markusfisch.android.zxingcpp.ZxingCpp.ReaderOptions
|
||||||
import de.markusfisch.android.zxingcpp.ZxingCpp.Result
|
import de.markusfisch.android.zxingcpp.ZxingCpp.Result
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
@@ -38,19 +45,18 @@ class PickActivity : AppCompatActivity() {
|
|||||||
private val matrix = Matrix()
|
private val matrix = Matrix()
|
||||||
private val parentJob = Job()
|
private val parentJob = Job()
|
||||||
private val scope = CoroutineScope(Dispatchers.IO + parentJob)
|
private val scope = CoroutineScope(Dispatchers.IO + parentJob)
|
||||||
private val decodeHints = DecodeHints(
|
private val options = ReaderOptions(
|
||||||
tryHarder = true,
|
tryHarder = true,
|
||||||
tryRotate = true,
|
tryRotate = true,
|
||||||
tryInvert = true,
|
tryInvert = true,
|
||||||
tryDownscale = true,
|
tryDownscale = true,
|
||||||
maxNumberOfSymbols = 1,
|
maxNumberOfSymbols = 1,
|
||||||
formats = prefs.barcodeFormats.joinToString()
|
formats = prefs.barcodeFormats.toFormatSet()
|
||||||
)
|
)
|
||||||
|
|
||||||
private lateinit var cropImageView: CropImageView
|
private var cropImageView: CropImageView? = null
|
||||||
private lateinit var detectorView: DetectorView
|
private var detectorView: DetectorView? = null
|
||||||
private lateinit var freeRotationItem: MenuItem
|
private var freeRotationItem: MenuItem? = null
|
||||||
|
|
||||||
private var result: Result? = null
|
private var result: Result? = null
|
||||||
|
|
||||||
override fun attachBaseContext(base: Context?) {
|
override fun attachBaseContext(base: Context?) {
|
||||||
@@ -66,11 +72,7 @@ class PickActivity : AppCompatActivity() {
|
|||||||
// locale.
|
// locale.
|
||||||
setTitle(R.string.pick_code_to_scan)
|
setTitle(R.string.pick_code_to_scan)
|
||||||
|
|
||||||
initSystemBars(this)
|
initBars()
|
||||||
val toolbar = findViewById(R.id.toolbar) as Toolbar
|
|
||||||
recordToolbarHeight(toolbar)
|
|
||||||
setSupportActionBar(toolbar)
|
|
||||||
|
|
||||||
supportFragmentManager.addOnBackStackChangedListener {
|
supportFragmentManager.addOnBackStackChangedListener {
|
||||||
colorSystemAndToolBars(this@PickActivity)
|
colorSystemAndToolBars(this@PickActivity)
|
||||||
}
|
}
|
||||||
@@ -79,25 +81,36 @@ class PickActivity : AppCompatActivity() {
|
|||||||
if (bitmap == null) {
|
if (bitmap == null) {
|
||||||
applicationContext.toast(R.string.error_no_content)
|
applicationContext.toast(R.string.error_no_content)
|
||||||
finish()
|
finish()
|
||||||
|
// This is why cropImageView and detectorView aren't lateinit.
|
||||||
|
// If they were, a kotlin.UninitializedPropertyAccessException
|
||||||
|
// would happen here.
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
cropImageView = findViewById(R.id.image) as CropImageView
|
cropImageView = (findViewById(R.id.image) as CropImageView).apply {
|
||||||
cropImageView.restrictTranslation = false
|
restrictTranslation = false
|
||||||
cropImageView.freeRotation = prefs.freeRotation
|
freeRotation = prefs.freeRotation
|
||||||
cropImageView.setImageBitmap(bitmap)
|
setImageBitmap(bitmap)
|
||||||
cropImageView.onScan = {
|
onScan = {
|
||||||
scanWithinBounds(bitmap)
|
scanWithinBounds(bitmap)
|
||||||
|
}
|
||||||
|
runAfterLayout = {
|
||||||
|
minWidth = min(
|
||||||
|
minWidth / 2f,
|
||||||
|
max(bitmap.width, bitmap.height).toFloat()
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
detectorView = findViewById(R.id.detector_view) as DetectorView
|
detectorView = (findViewById(R.id.detector_view) as DetectorView).apply {
|
||||||
detectorView.onRoiChanged = {
|
onRoiChanged = {
|
||||||
scanWithinBounds(bitmap)
|
scanWithinBounds(bitmap)
|
||||||
|
}
|
||||||
|
setPaddingFromWindowInsets()
|
||||||
|
cropHandleName = "picker_crop_handle"
|
||||||
}
|
}
|
||||||
detectorView.setPaddingFromWindowInsets()
|
|
||||||
detectorView.restoreCropHandlePos()
|
|
||||||
|
|
||||||
findViewById(R.id.scan).setOnClickListener {
|
findViewById<View>(R.id.scan).setOnClickListener {
|
||||||
showResult()
|
showResult()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -117,35 +130,42 @@ class PickActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun scanWithinBounds(bitmap: Bitmap) {
|
private fun scanWithinBounds(bitmap: Bitmap) {
|
||||||
val viewRoi = if (detectorView.roi.width() < 1) {
|
val dv = detectorView ?: return
|
||||||
cropImageView.getBoundsRect()
|
val civ = cropImageView ?: return
|
||||||
} else {
|
val viewRoi = Rect(
|
||||||
detectorView.roi
|
if (dv.roi.width() < 1) {
|
||||||
}
|
civ.getBoundsRect()
|
||||||
val mappedRect = cropImageView.mappedRect
|
} else {
|
||||||
val cropped = bitmap.crop(
|
dv.roi
|
||||||
getNormalizedRoi(mappedRect, viewRoi),
|
}
|
||||||
cropImageView.imageRotation,
|
|
||||||
cropImageView.pivotX,
|
|
||||||
cropImageView.pivotY
|
|
||||||
) ?: return
|
|
||||||
val croppedInView = Rect(
|
|
||||||
max(viewRoi.left, mappedRect.left.roundToInt()),
|
|
||||||
max(viewRoi.top, mappedRect.top.roundToInt()),
|
|
||||||
min(viewRoi.right, mappedRect.right.roundToInt()),
|
|
||||||
min(viewRoi.bottom, mappedRect.bottom.roundToInt())
|
|
||||||
)
|
)
|
||||||
matrix.apply {
|
val mappedRect = RectF(civ.mappedRect)
|
||||||
setScale(
|
val imageRotation = civ.imageRotation
|
||||||
croppedInView.width().toFloat() / cropped.width,
|
val pivotX = civ.pivotX
|
||||||
croppedInView.height().toFloat() / cropped.height
|
val pivotY = civ.pivotY
|
||||||
)
|
|
||||||
postTranslate(
|
|
||||||
croppedInView.left.toFloat(),
|
|
||||||
croppedInView.top.toFloat()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
scope.launch {
|
scope.launch {
|
||||||
|
val cropped = bitmap.crop(
|
||||||
|
getNormalizedRoi(mappedRect, viewRoi),
|
||||||
|
imageRotation,
|
||||||
|
pivotX,
|
||||||
|
pivotY
|
||||||
|
) ?: return@launch
|
||||||
|
val croppedInView = Rect(
|
||||||
|
max(viewRoi.left, mappedRect.left.roundToInt()),
|
||||||
|
max(viewRoi.top, mappedRect.top.roundToInt()),
|
||||||
|
min(viewRoi.right, mappedRect.right.roundToInt()),
|
||||||
|
min(viewRoi.bottom, mappedRect.bottom.roundToInt())
|
||||||
|
)
|
||||||
|
matrix.apply {
|
||||||
|
setScale(
|
||||||
|
croppedInView.width().toFloat() / cropped.width,
|
||||||
|
croppedInView.height().toFloat() / cropped.height
|
||||||
|
)
|
||||||
|
postTranslate(
|
||||||
|
croppedInView.left.toFloat(),
|
||||||
|
croppedInView.top.toFloat()
|
||||||
|
)
|
||||||
|
}
|
||||||
cropped.decode()?.first()?.let {
|
cropped.decode()?.first()?.let {
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
if (isFinishing) {
|
if (isFinishing) {
|
||||||
@@ -153,10 +173,10 @@ class PickActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
result = it
|
result = it
|
||||||
scanFeedback()
|
scanFeedback()
|
||||||
detectorView.update(
|
dv.update(
|
||||||
matrix.mapPosition(
|
matrix.mapPosition(
|
||||||
it.position,
|
it.position,
|
||||||
detectorView.coordinates
|
dv.coordinates
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -171,7 +191,7 @@ class PickActivity : AppCompatActivity() {
|
|||||||
0, 0,
|
0, 0,
|
||||||
width, height,
|
width, height,
|
||||||
0,
|
0,
|
||||||
decodeHints.apply {
|
options.apply {
|
||||||
binarizer = Binarizer.LOCAL_AVERAGE
|
binarizer = Binarizer.LOCAL_AVERAGE
|
||||||
}
|
}
|
||||||
) ?: ZxingCpp.readBitmap(
|
) ?: ZxingCpp.readBitmap(
|
||||||
@@ -179,14 +199,14 @@ class PickActivity : AppCompatActivity() {
|
|||||||
0, 0,
|
0, 0,
|
||||||
width, height,
|
width, height,
|
||||||
0,
|
0,
|
||||||
decodeHints.apply {
|
options.apply {
|
||||||
binarizer = Binarizer.GLOBAL_HISTOGRAM
|
binarizer = Binarizer.GLOBAL_HISTOGRAM
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
detectorView.saveCropHandlePos()
|
detectorView?.storeCropHandlePos()
|
||||||
parentJob.cancel()
|
parentJob.cancel()
|
||||||
releaseToneGenerators()
|
releaseToneGenerators()
|
||||||
}
|
}
|
||||||
@@ -205,12 +225,14 @@ class PickActivity : AppCompatActivity() {
|
|||||||
rotateClockwise()
|
rotateClockwise()
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
R.id.toggle_free -> {
|
R.id.toggle_free -> {
|
||||||
prefs.freeRotation = prefs.freeRotation xor true
|
prefs.freeRotation = prefs.freeRotation xor true
|
||||||
cropImageView.freeRotation = prefs.freeRotation
|
cropImageView?.freeRotation = prefs.freeRotation
|
||||||
freeRotationItem.updateFreeRotationIcon()
|
freeRotationItem?.updateFreeRotationIcon()
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> super.onOptionsItemSelected(item)
|
else -> super.onOptionsItemSelected(item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -226,13 +248,12 @@ class PickActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun rotateClockwise() {
|
private fun rotateClockwise() {
|
||||||
cropImageView.imageRotation = cropImageView.imageRotation + 90 % 360
|
val civ = cropImageView ?: return
|
||||||
|
civ.imageRotation = (civ.imageRotation + 90) % 360
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadSentImage(intent: Intent): Bitmap? {
|
private fun loadSentImage(intent: Intent): Bitmap? {
|
||||||
val uri = intent.getParcelableExtra<Parcelable>(
|
val uri = intent.getUriExtra() ?: return null
|
||||||
Intent.EXTRA_STREAM
|
|
||||||
) as? Uri ?: return null
|
|
||||||
return contentResolver.loadImageUri(uri)
|
return contentResolver.loadImageUri(uri)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -262,3 +283,12 @@ private fun getNormalizedRoi(imageRect: RectF, roi: Rect): RectF {
|
|||||||
(roi.bottom - imageRect.top) / h
|
(roi.bottom - imageRect.top) / h
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun Intent.getUriExtra(): Uri? = if (
|
||||||
|
Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU
|
||||||
|
) {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
getParcelableExtra(Intent.EXTRA_STREAM)
|
||||||
|
} else {
|
||||||
|
getParcelableExtra(Intent.EXTRA_STREAM, Uri::class.java)
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
package de.markusfisch.android.binaryeye.activity
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.Bundle
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import de.markusfisch.android.binaryeye.R
|
||||||
|
import de.markusfisch.android.binaryeye.app.applyLocale
|
||||||
|
import de.markusfisch.android.binaryeye.app.prefs
|
||||||
|
import de.markusfisch.android.binaryeye.fragment.PreferencesFragment
|
||||||
|
import de.markusfisch.android.binaryeye.view.initBars
|
||||||
|
|
||||||
|
class PreferencesActivity : AppCompatActivity() {
|
||||||
|
override fun attachBaseContext(base: Context?) {
|
||||||
|
base?.applyLocale(prefs.customLocale)
|
||||||
|
super.attachBaseContext(base)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate(state: Bundle?) {
|
||||||
|
super.onCreate(state)
|
||||||
|
setContentView(R.layout.activity_main)
|
||||||
|
initBars()
|
||||||
|
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||||
|
if (state == null) {
|
||||||
|
supportFragmentManager.beginTransaction()
|
||||||
|
.replace(R.id.content_frame, PreferencesFragment())
|
||||||
|
.commit()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSupportNavigateUp(): Boolean {
|
||||||
|
finish()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,163 @@
|
|||||||
|
package de.markusfisch.android.binaryeye.activity
|
||||||
|
|
||||||
|
import android.app.AlertDialog
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.ArrayAdapter
|
||||||
|
import android.widget.EditText
|
||||||
|
import android.widget.ListView
|
||||||
|
import android.widget.TextView
|
||||||
|
import de.markusfisch.android.binaryeye.R
|
||||||
|
import de.markusfisch.android.binaryeye.app.prefs
|
||||||
|
import de.markusfisch.android.binaryeye.view.setPaddingFromWindowInsets
|
||||||
|
import de.markusfisch.android.binaryeye.view.systemBarListViewScrollListener
|
||||||
|
import de.markusfisch.android.binaryeye.widget.toast
|
||||||
|
|
||||||
|
class ProfilesActivity : AbstractBaseActivity() {
|
||||||
|
private val profiles = ArrayList<ProfileItem>()
|
||||||
|
|
||||||
|
private lateinit var adapter: ProfilesAdapter
|
||||||
|
|
||||||
|
override fun onCreate(state: Bundle?) {
|
||||||
|
super.onCreate(state)
|
||||||
|
setScreenContentView(R.layout.activity_profiles)
|
||||||
|
setTitle(R.string.profile)
|
||||||
|
|
||||||
|
refreshProfiles()
|
||||||
|
|
||||||
|
val listView = findViewById<ListView>(R.id.profiles)
|
||||||
|
listView.emptyView = findViewById(R.id.no_profiles)
|
||||||
|
adapter = ProfilesAdapter(this, profiles)
|
||||||
|
listView.adapter = adapter
|
||||||
|
listView.setOnScrollListener(systemBarListViewScrollListener)
|
||||||
|
listView.setOnItemClickListener { _, _, position, _ ->
|
||||||
|
selectProfile(position)
|
||||||
|
}
|
||||||
|
listView.setOnItemLongClickListener { _, _, position, _ ->
|
||||||
|
confirmRemoveProfile(position)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
findViewById<View>(R.id.add).setOnClickListener {
|
||||||
|
addProfile()
|
||||||
|
}
|
||||||
|
|
||||||
|
findViewById<View>(R.id.inset_layout).setPaddingFromWindowInsets()
|
||||||
|
listView.setPaddingFromWindowInsets()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun refreshProfiles() {
|
||||||
|
profiles.clear()
|
||||||
|
profiles.add(
|
||||||
|
ProfileItem(
|
||||||
|
null,
|
||||||
|
getString(R.string.profile_default)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
prefs.profiles.forEach { name ->
|
||||||
|
profiles.add(ProfileItem(name, name))
|
||||||
|
}
|
||||||
|
if (this::adapter.isInitialized) {
|
||||||
|
adapter.notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun selectProfile(position: Int) {
|
||||||
|
val profile = profiles[position].name
|
||||||
|
if (prefs.profile == profile) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
prefs.load(this, profile)
|
||||||
|
adapter.notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dialogs do not have a parent view.
|
||||||
|
@Suppress("InflateParams")
|
||||||
|
private fun addProfile() {
|
||||||
|
val view = layoutInflater.inflate(
|
||||||
|
R.layout.dialog_profile_name, null
|
||||||
|
)
|
||||||
|
val editText = view.findViewById<EditText>(R.id.name)
|
||||||
|
AlertDialog.Builder(this)
|
||||||
|
.setView(view)
|
||||||
|
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||||
|
val name = editText.text.toString().trim()
|
||||||
|
if (name.isNotEmpty() && prefs.addProfile(name)) {
|
||||||
|
prefs.load(this, name)
|
||||||
|
refreshProfiles()
|
||||||
|
} else {
|
||||||
|
toast(R.string.profile_invalid_name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun confirmRemoveProfile(position: Int) {
|
||||||
|
val profile = profiles[position]
|
||||||
|
val profileName = profile.name ?: return
|
||||||
|
AlertDialog.Builder(this)
|
||||||
|
.setMessage(R.string.profile_remove_confirm)
|
||||||
|
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||||
|
val isCurrent = prefs.profile == profileName
|
||||||
|
prefs.removeProfile(profileName)
|
||||||
|
if (isCurrent) {
|
||||||
|
prefs.load(this, null)
|
||||||
|
}
|
||||||
|
refreshProfiles()
|
||||||
|
}
|
||||||
|
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
private class ProfilesAdapter(
|
||||||
|
context: Context,
|
||||||
|
private val items: List<ProfileItem>
|
||||||
|
) : ArrayAdapter<ProfileItem>(
|
||||||
|
context,
|
||||||
|
android.R.layout.simple_list_item_2,
|
||||||
|
android.R.id.text1,
|
||||||
|
items
|
||||||
|
) {
|
||||||
|
override fun getView(
|
||||||
|
position: Int,
|
||||||
|
convertView: View?,
|
||||||
|
parent: ViewGroup
|
||||||
|
): View {
|
||||||
|
val view = super.getView(position, convertView, parent)
|
||||||
|
val item = items[position]
|
||||||
|
val isCurrent = item.name == prefs.profile
|
||||||
|
getViewHolder(view).apply {
|
||||||
|
title.text = item.label
|
||||||
|
if (isCurrent) {
|
||||||
|
subtitle.text = context.getString(R.string.profile_current)
|
||||||
|
subtitle.visibility = View.VISIBLE
|
||||||
|
} else {
|
||||||
|
subtitle.text = ""
|
||||||
|
subtitle.visibility = View.GONE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getViewHolder(
|
||||||
|
view: View
|
||||||
|
): ViewHolder = view.tag as ViewHolder? ?: ViewHolder(
|
||||||
|
view.findViewById(android.R.id.text1),
|
||||||
|
view.findViewById(android.R.id.text2),
|
||||||
|
).also {
|
||||||
|
view.tag = it
|
||||||
|
}
|
||||||
|
|
||||||
|
private data class ViewHolder(
|
||||||
|
val title: TextView,
|
||||||
|
val subtitle: TextView,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private data class ProfileItem(
|
||||||
|
val name: String?,
|
||||||
|
val label: String
|
||||||
|
)
|
||||||
@@ -2,7 +2,7 @@ package de.markusfisch.android.binaryeye.activity
|
|||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.support.v7.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
|
||||||
class SplashActivity : AppCompatActivity() {
|
class SplashActivity : AppCompatActivity() {
|
||||||
override fun onCreate(state: Bundle?) {
|
override fun onCreate(state: Bundle?) {
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package de.markusfisch.android.binaryeye.activity
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.View
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import de.markusfisch.android.binaryeye.R
|
||||||
|
import de.markusfisch.android.binaryeye.app.prefs
|
||||||
|
|
||||||
|
class WelcomeActivity : AppCompatActivity() {
|
||||||
|
override fun onCreate(state: Bundle?) {
|
||||||
|
super.onCreate(state)
|
||||||
|
setContentView(R.layout.activity_welcome)
|
||||||
|
findViewById<View>(R.id.expert).setOnClickListener {
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
findViewById<View>(R.id.simple).setOnClickListener {
|
||||||
|
prefs.apply {
|
||||||
|
showCropHandle = false
|
||||||
|
zoomBySwiping = false
|
||||||
|
beep = true
|
||||||
|
copyImmediately = true
|
||||||
|
openImmediately = true
|
||||||
|
showMetaData = false
|
||||||
|
showHexDump = false
|
||||||
|
defaultSearchUrl = "https://www.google.com/search?q="
|
||||||
|
openWithUrl = defaultSearchUrl
|
||||||
|
expandEscapeSequences = false
|
||||||
|
brightenScreen = true
|
||||||
|
}
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,25 +2,33 @@ package de.markusfisch.android.binaryeye.adapter
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.database.Cursor
|
import android.database.Cursor
|
||||||
import android.support.v4.content.ContextCompat
|
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import android.widget.ArrayAdapter
|
||||||
import android.widget.CursorAdapter
|
import android.widget.CursorAdapter
|
||||||
|
import android.widget.ImageButton
|
||||||
|
import android.widget.Spinner
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
import de.markusfisch.android.binaryeye.R
|
import de.markusfisch.android.binaryeye.R
|
||||||
import de.markusfisch.android.binaryeye.database.Database
|
import de.markusfisch.android.binaryeye.database.Database
|
||||||
import java.text.DateFormat
|
import java.text.DateFormat
|
||||||
import java.text.ParseException
|
import java.text.ParseException
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.*
|
import java.util.Locale
|
||||||
|
|
||||||
class ScansAdapter(context: Context, cursor: Cursor) :
|
class ScansAdapter(
|
||||||
|
context: Context,
|
||||||
|
cursor: Cursor,
|
||||||
|
private val onPinClick: (Long, Boolean) -> Unit
|
||||||
|
) :
|
||||||
CursorAdapter(context, cursor, false) {
|
CursorAdapter(context, cursor, false) {
|
||||||
private val idIndex = cursor.getColumnIndex(Database.SCANS_ID)
|
private val idIndex = cursor.getColumnIndex(Database.SCANS_ID)
|
||||||
|
private val pinnedIndex = cursor.getColumnIndex(Database.SCANS_PINNED)
|
||||||
private val timeIndex = cursor.getColumnIndex(Database.SCANS_DATETIME)
|
private val timeIndex = cursor.getColumnIndex(Database.SCANS_DATETIME)
|
||||||
private val nameIndex = cursor.getColumnIndex(Database.SCANS_NAME)
|
private val nameIndex = cursor.getColumnIndex(Database.SCANS_NAME)
|
||||||
private val contentIndex = cursor.getColumnIndex(Database.SCANS_CONTENT)
|
private val textIndex = cursor.getColumnIndex(Database.SCANS_TEXT)
|
||||||
private val formatIndex = cursor.getColumnIndex(Database.SCANS_FORMAT)
|
private val formatIndex = cursor.getColumnIndex(Database.SCANS_FORMAT)
|
||||||
private val selections = mutableMapOf<Long, Int>()
|
private val selections = mutableMapOf<Long, Int>()
|
||||||
private val selectedColor = ContextCompat.getColor(
|
private val selectedColor = ContextCompat.getColor(
|
||||||
@@ -74,7 +82,7 @@ class ScansAdapter(context: Context, cursor: Cursor) :
|
|||||||
|
|
||||||
fun getContent(
|
fun getContent(
|
||||||
position: Int
|
position: Int
|
||||||
) = (getItem(position) as Cursor?)?.getString(contentIndex)
|
) = (getItem(position) as Cursor?)?.getString(textIndex)
|
||||||
|
|
||||||
override fun newView(
|
override fun newView(
|
||||||
context: Context,
|
context: Context,
|
||||||
@@ -89,27 +97,53 @@ class ScansAdapter(context: Context, cursor: Cursor) :
|
|||||||
context: Context,
|
context: Context,
|
||||||
cursor: Cursor
|
cursor: Cursor
|
||||||
) {
|
) {
|
||||||
|
val id = cursor.getLong(idIndex)
|
||||||
|
val pinned = cursor.getInt(pinnedIndex) != 0
|
||||||
getViewHolder(view).apply {
|
getViewHolder(view).apply {
|
||||||
timeView.text = formatDateTime(cursor.getString(timeIndex))
|
metaView.text = context.getString(
|
||||||
|
R.string.scan_meta_data,
|
||||||
|
formatDateTime(cursor.getString(timeIndex)),
|
||||||
|
cursor.getString(formatIndex).prettifyFormatName()
|
||||||
|
)
|
||||||
val name = cursor.getString(nameIndex)
|
val name = cursor.getString(nameIndex)
|
||||||
val content = cursor.getString(contentIndex)
|
val text = cursor.getString(textIndex)
|
||||||
var icon = 0
|
var icon = 0
|
||||||
contentView.text = when {
|
contentView.text = when {
|
||||||
name?.isNotEmpty() == true -> {
|
name?.isNotEmpty() == true -> {
|
||||||
icon = R.drawable.ic_label
|
icon = R.drawable.ic_label
|
||||||
name
|
name
|
||||||
}
|
}
|
||||||
content?.isEmpty() == true -> context.getString(
|
|
||||||
|
text.isNullOrEmpty() -> context.getString(
|
||||||
R.string.binary_data
|
R.string.binary_data
|
||||||
)
|
)
|
||||||
else -> content
|
|
||||||
|
else -> text
|
||||||
}
|
}
|
||||||
contentView.setCompoundDrawablesWithIntrinsicBounds(
|
contentView.setCompoundDrawablesWithIntrinsicBounds(
|
||||||
icon, 0, 0, 0
|
icon, 0, 0, 0
|
||||||
)
|
)
|
||||||
formatView.text = prettifyFormatName(cursor.getString(formatIndex))
|
pinView.apply {
|
||||||
|
setImageResource(
|
||||||
|
if (pinned) {
|
||||||
|
R.drawable.ic_action_pin
|
||||||
|
} else {
|
||||||
|
R.drawable.ic_action_pin_outline
|
||||||
|
}
|
||||||
|
)
|
||||||
|
contentDescription = context.getString(
|
||||||
|
if (pinned) {
|
||||||
|
R.string.unpin_item
|
||||||
|
} else {
|
||||||
|
R.string.pin_item
|
||||||
|
}
|
||||||
|
)
|
||||||
|
setOnClickListener {
|
||||||
|
onPinClick(id, !pinned)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
val selected = selections[cursor.getLong(idIndex)] != null
|
val selected = selections[id] != null
|
||||||
view.post {
|
view.post {
|
||||||
// Needs to be put on the queue to work.
|
// Needs to be put on the queue to work.
|
||||||
view.select(selected)
|
view.select(selected)
|
||||||
@@ -123,21 +157,48 @@ class ScansAdapter(context: Context, cursor: Cursor) :
|
|||||||
private fun getViewHolder(
|
private fun getViewHolder(
|
||||||
view: View
|
view: View
|
||||||
): ViewHolder = view.tag as ViewHolder? ?: ViewHolder(
|
): ViewHolder = view.tag as ViewHolder? ?: ViewHolder(
|
||||||
view.findViewById(R.id.time),
|
view.findViewById(R.id.meta),
|
||||||
view.findViewById(R.id.content),
|
view.findViewById(R.id.content),
|
||||||
view.findViewById(R.id.format)
|
view.findViewById(R.id.pin)
|
||||||
).also {
|
).also {
|
||||||
view.tag = it
|
view.tag = it
|
||||||
}
|
}
|
||||||
|
|
||||||
private data class ViewHolder(
|
private data class ViewHolder(
|
||||||
val timeView: TextView,
|
val metaView: TextView,
|
||||||
val contentView: TextView,
|
val contentView: TextView,
|
||||||
val formatView: TextView
|
val pinView: ImageButton
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun prettifyFormatName(format: String) = format.replace("_", " ")
|
private val formatWordBoundary =
|
||||||
|
"(?<=[a-z0-9])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])|(?<=[A-Za-z])(?=\\d)|(?<=\\d)(?=[A-Za-z])".toRegex()
|
||||||
|
|
||||||
|
fun Context.setupFormatSpinner(spinner: Spinner): List<String> {
|
||||||
|
val names = resources.getStringArray(
|
||||||
|
R.array.barcode_formats_names
|
||||||
|
).toMutableList().apply {
|
||||||
|
add(0, getString(R.string.all_formats))
|
||||||
|
}
|
||||||
|
val values = resources.getStringArray(
|
||||||
|
R.array.barcode_formats_values
|
||||||
|
).toMutableList().apply {
|
||||||
|
add(0, "")
|
||||||
|
}
|
||||||
|
spinner.adapter = ArrayAdapter(
|
||||||
|
this,
|
||||||
|
android.R.layout.simple_spinner_item,
|
||||||
|
names
|
||||||
|
).apply {
|
||||||
|
setDropDownViewResource(
|
||||||
|
android.R.layout.simple_spinner_dropdown_item
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return values
|
||||||
|
}
|
||||||
|
|
||||||
|
fun String.prettifyFormatName() = replace("_", " ")
|
||||||
|
.replace(formatWordBoundary, " ")
|
||||||
|
|
||||||
private fun formatDateTime(rfc: String): String {
|
private fun formatDateTime(rfc: String): String {
|
||||||
try {
|
try {
|
||||||
@@ -147,7 +208,7 @@ private fun formatDateTime(rfc: String): String {
|
|||||||
DateFormat.SHORT
|
DateFormat.SHORT
|
||||||
).format(it)
|
).format(it)
|
||||||
}
|
}
|
||||||
} catch (e: ParseException) {
|
} catch (_: ParseException) {
|
||||||
// Ignore and fall through.
|
// Ignore and fall through.
|
||||||
}
|
}
|
||||||
return rfc
|
return rfc
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
package de.markusfisch.android.binaryeye.app
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.support.v4.app.Fragment
|
|
||||||
import android.support.v4.app.FragmentManager
|
|
||||||
import de.markusfisch.android.binaryeye.R
|
|
||||||
|
|
||||||
fun FragmentManager.setFragment(fragment: Fragment) {
|
|
||||||
getTransaction(fragment).commit()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun FragmentManager.addFragment(fragment: Fragment) {
|
|
||||||
getTransaction(fragment).addToBackStack(null).commit()
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("CommitTransaction")
|
|
||||||
private fun FragmentManager.getTransaction(
|
|
||||||
fragment: Fragment
|
|
||||||
) = beginTransaction().replace(R.id.content_frame, fragment)
|
|
||||||
@@ -2,7 +2,7 @@ package de.markusfisch.android.binaryeye.app
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import java.util.*
|
import java.util.Locale
|
||||||
|
|
||||||
fun Context.applyLocale(localeName: String) {
|
fun Context.applyLocale(localeName: String) {
|
||||||
if (localeName.isEmpty()) {
|
if (localeName.isEmpty()) {
|
||||||
|
|||||||
@@ -4,15 +4,15 @@ import android.Manifest
|
|||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.support.v4.app.ActivityCompat
|
import androidx.core.app.ActivityCompat
|
||||||
import android.support.v4.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
|
|
||||||
var permissionGrantedCallback: (() -> Any)? = null
|
var permissionGrantedCallback: (() -> Any)? = null
|
||||||
|
|
||||||
const val PERMISSION_CAMERA = 1
|
const val PERMISSION_CAMERA = 1
|
||||||
fun Activity.hasCameraPermission() = hasPermission(
|
fun Activity.hasCameraPermission(request: Boolean = true) = hasPermission(
|
||||||
Manifest.permission.CAMERA,
|
Manifest.permission.CAMERA,
|
||||||
PERMISSION_CAMERA
|
if (request) PERMISSION_CAMERA else 0
|
||||||
)
|
)
|
||||||
|
|
||||||
const val PERMISSION_WRITE = 2
|
const val PERMISSION_WRITE = 2
|
||||||
@@ -53,11 +53,13 @@ private fun Activity.hasPermission(
|
|||||||
permission
|
permission
|
||||||
) != PackageManager.PERMISSION_GRANTED
|
) != PackageManager.PERMISSION_GRANTED
|
||||||
) {
|
) {
|
||||||
ActivityCompat.requestPermissions(
|
if (requestCode > 0) {
|
||||||
this,
|
ActivityCompat.requestPermissions(
|
||||||
arrayOf(permission),
|
this,
|
||||||
requestCode
|
arrayOf(permission),
|
||||||
)
|
requestCode
|
||||||
|
)
|
||||||
|
}
|
||||||
false
|
false
|
||||||
} else {
|
} else {
|
||||||
true
|
true
|
||||||
|
|||||||
@@ -0,0 +1,111 @@
|
|||||||
|
package de.markusfisch.android.binaryeye.automation
|
||||||
|
|
||||||
|
import de.markusfisch.android.binaryeye.database.Scan
|
||||||
|
import de.markusfisch.android.binaryeye.net.urlEncode
|
||||||
|
import org.json.JSONArray
|
||||||
|
import org.json.JSONObject
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
data class AutomatedAction(
|
||||||
|
val pattern: String,
|
||||||
|
val type: Type,
|
||||||
|
val format: String? = null,
|
||||||
|
val urlTemplate: String? = null,
|
||||||
|
val intentUriTemplate: String? = null
|
||||||
|
) {
|
||||||
|
enum class Type {
|
||||||
|
Intent, CustomIntent
|
||||||
|
}
|
||||||
|
|
||||||
|
fun matches(content: String, format: String): Boolean = try {
|
||||||
|
(this.format == null || this.format == format) &&
|
||||||
|
Regex(pattern).matches(content)
|
||||||
|
} catch (_: Exception) {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toJson(): JSONObject = JSONObject().apply {
|
||||||
|
put(KEY_PATTERN, pattern)
|
||||||
|
put(KEY_TYPE, type.name.lowercase(Locale.ROOT))
|
||||||
|
put(KEY_FORMAT, format ?: "")
|
||||||
|
put(KEY_URL_TEMPLATE, urlTemplate ?: "")
|
||||||
|
put(KEY_INTENT_URI_TEMPLATE, intentUriTemplate ?: "")
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val KEY_PATTERN = "pattern"
|
||||||
|
private const val KEY_TYPE = "type"
|
||||||
|
private const val KEY_FORMAT = "format"
|
||||||
|
private const val KEY_URL_TEMPLATE = "url_template"
|
||||||
|
private const val KEY_INTENT_URI_TEMPLATE = "intent_uri_template"
|
||||||
|
|
||||||
|
fun fromJson(obj: JSONObject): AutomatedAction? {
|
||||||
|
val pattern = obj.optString(KEY_PATTERN, "").trim()
|
||||||
|
if (pattern.isEmpty()) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
val type = when (obj.optString(KEY_TYPE, "")) {
|
||||||
|
"custom_intent" -> Type.CustomIntent
|
||||||
|
"intent" -> Type.Intent
|
||||||
|
"html" -> return null
|
||||||
|
else -> Type.Intent
|
||||||
|
}
|
||||||
|
val format = obj.optString(KEY_FORMAT, "").trim()
|
||||||
|
val urlTemplate = obj.optString(KEY_URL_TEMPLATE, "").trim()
|
||||||
|
val intentUriTemplate = obj.optString(
|
||||||
|
KEY_INTENT_URI_TEMPLATE,
|
||||||
|
""
|
||||||
|
).trim()
|
||||||
|
return AutomatedAction(
|
||||||
|
pattern = pattern,
|
||||||
|
type = type,
|
||||||
|
format = format.ifEmpty { null },
|
||||||
|
urlTemplate = urlTemplate.ifEmpty { null },
|
||||||
|
intentUriTemplate = intentUriTemplate.ifEmpty { null }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun fromJsonArray(json: String): MutableList<AutomatedAction> {
|
||||||
|
val actions = ArrayList<AutomatedAction>()
|
||||||
|
val array = try {
|
||||||
|
JSONArray(json)
|
||||||
|
} catch (_: Exception) {
|
||||||
|
JSONArray()
|
||||||
|
}
|
||||||
|
for (i in 0 until array.length()) {
|
||||||
|
val obj = array.optJSONObject(i) ?: continue
|
||||||
|
fromJson(obj)?.let { actions.add(it) }
|
||||||
|
}
|
||||||
|
return actions
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toJsonArray(actions: List<AutomatedAction>): String {
|
||||||
|
val array = JSONArray()
|
||||||
|
actions.forEach { array.put(it.toJson()) }
|
||||||
|
return array.toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun String.buildUrl(scan: Scan): String {
|
||||||
|
return buildTemplate(scan, encodeResult = true)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun String.buildTemplate(scan: Scan, encodeResult: Boolean): String {
|
||||||
|
val text = scan.text
|
||||||
|
val raw = scan.raw ?: text.toByteArray()
|
||||||
|
val result = if (encodeResult) {
|
||||||
|
text.urlEncode()
|
||||||
|
} else {
|
||||||
|
text
|
||||||
|
}
|
||||||
|
val format = if (encodeResult) {
|
||||||
|
scan.format.name.urlEncode()
|
||||||
|
} else {
|
||||||
|
scan.format.name
|
||||||
|
}
|
||||||
|
return this
|
||||||
|
.replace("{RESULT}", result)
|
||||||
|
.replace("{RESULT_BYTES}", raw.toHexString())
|
||||||
|
.replace("{FORMAT}", format)
|
||||||
|
}
|
||||||
+59
@@ -0,0 +1,59 @@
|
|||||||
|
package de.markusfisch.android.binaryeye.automation
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import de.markusfisch.android.binaryeye.R
|
||||||
|
import de.markusfisch.android.binaryeye.app.prefs
|
||||||
|
import de.markusfisch.android.binaryeye.content.execShareIntent
|
||||||
|
import de.markusfisch.android.binaryeye.content.openUrl
|
||||||
|
import de.markusfisch.android.binaryeye.database.Scan
|
||||||
|
import de.markusfisch.android.binaryeye.widget.toast
|
||||||
|
|
||||||
|
fun Context.runAutomatedActions(scan: Scan): Boolean {
|
||||||
|
val content = scan.text
|
||||||
|
if (content.isEmpty()) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
val actions = prefs.automatedActions
|
||||||
|
if (actions.isEmpty()) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for (action in actions) {
|
||||||
|
if (action.matches(content, scan.format.name)) {
|
||||||
|
return when (action.type) {
|
||||||
|
AutomatedAction.Type.Intent -> {
|
||||||
|
val url = action.urlTemplate?.buildUrl(scan)
|
||||||
|
if (url.isNullOrEmpty()) {
|
||||||
|
toast(R.string.automated_action_invalid_url)
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
openUrl(url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AutomatedAction.Type.CustomIntent ->
|
||||||
|
execCustomIntent(action, scan)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Context.execCustomIntent(
|
||||||
|
action: AutomatedAction,
|
||||||
|
scan: Scan
|
||||||
|
): Boolean {
|
||||||
|
val template = action.intentUriTemplate
|
||||||
|
if (template.isNullOrEmpty()) {
|
||||||
|
toast(R.string.automated_action_intent_uri_required)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
val intentUri = template.buildTemplate(scan, encodeResult = false)
|
||||||
|
val intent = try {
|
||||||
|
Intent.parseUri(intentUri, Intent.URI_INTENT_SCHEME)
|
||||||
|
} catch (_: Exception) {
|
||||||
|
toast(R.string.automated_action_intent_uri_invalid)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return execShareIntent(intent)
|
||||||
|
}
|
||||||
@@ -3,7 +3,7 @@ package de.markusfisch.android.binaryeye.bluetooth
|
|||||||
import android.bluetooth.BluetoothAdapter
|
import android.bluetooth.BluetoothAdapter
|
||||||
import android.bluetooth.BluetoothDevice
|
import android.bluetooth.BluetoothDevice
|
||||||
import android.bluetooth.BluetoothSocket
|
import android.bluetooth.BluetoothSocket
|
||||||
import android.support.v7.preference.ListPreference
|
import androidx.preference.ListPreference
|
||||||
import de.markusfisch.android.binaryeye.database.Scan
|
import de.markusfisch.android.binaryeye.database.Scan
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -11,7 +11,7 @@ import kotlinx.coroutines.launch
|
|||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.io.OutputStreamWriter
|
import java.io.OutputStreamWriter
|
||||||
import java.util.*
|
import java.util.UUID
|
||||||
|
|
||||||
fun Scan.sendBluetoothAsync(
|
fun Scan.sendBluetoothAsync(
|
||||||
host: String,
|
host: String,
|
||||||
@@ -21,10 +21,10 @@ fun Scan.sendBluetoothAsync(
|
|||||||
val connected = if (isConnected) {
|
val connected = if (isConnected) {
|
||||||
true
|
true
|
||||||
} else {
|
} else {
|
||||||
connect(host)
|
connect(host, true)
|
||||||
}
|
}
|
||||||
val sent = if (connected) {
|
val sent = if (connected) {
|
||||||
send(content)
|
send(text, host)
|
||||||
} else {
|
} else {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
@@ -35,21 +35,20 @@ fun Scan.sendBluetoothAsync(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun setBluetoothHosts(listPref: ListPreference) {
|
fun setBluetoothHosts(listPref: ListPreference) {
|
||||||
val devices = try {
|
try {
|
||||||
BluetoothAdapter.getDefaultAdapter().bondedDevices
|
val devices = blue.bondedDevices ?: return
|
||||||
} catch (e: SecurityException) {
|
listPref.entries = devices.map {
|
||||||
|
it.name
|
||||||
|
}.toTypedArray()
|
||||||
|
listPref.entryValues = devices.map {
|
||||||
|
it.address
|
||||||
|
}.toTypedArray()
|
||||||
|
listPref.callChangeListener(listPref.value)
|
||||||
|
} catch (_: SecurityException) {
|
||||||
// Do nothing, either the user has denied Bluetooth access
|
// Do nothing, either the user has denied Bluetooth access
|
||||||
// or the permission was removed by the system. We're catching
|
// or the permission was removed by the system. We're catching
|
||||||
// the exception to keep the app from crashing.
|
// the exception to keep the app from crashing.
|
||||||
null
|
}
|
||||||
} ?: return
|
|
||||||
listPref.entries = devices.map {
|
|
||||||
it.name
|
|
||||||
}.toTypedArray()
|
|
||||||
listPref.entryValues = devices.map {
|
|
||||||
it.address
|
|
||||||
}.toTypedArray()
|
|
||||||
listPref.callChangeListener(listPref.value)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private var socket: BluetoothSocket? = null
|
private var socket: BluetoothSocket? = null
|
||||||
@@ -62,7 +61,7 @@ private val uuid = UUID.fromString(
|
|||||||
|
|
||||||
private var isConnected = false
|
private var isConnected = false
|
||||||
|
|
||||||
private fun connect(deviceName: String): Boolean = try {
|
private fun connect(deviceName: String, onceMore: Boolean): Boolean = try {
|
||||||
val device = findByName(deviceName) ?: throw RuntimeException(
|
val device = findByName(deviceName) ?: throw RuntimeException(
|
||||||
"Bluetooth device not found"
|
"Bluetooth device not found"
|
||||||
)
|
)
|
||||||
@@ -71,32 +70,41 @@ private fun connect(deviceName: String): Boolean = try {
|
|||||||
writer = socket?.outputStream?.writer()
|
writer = socket?.outputStream?.writer()
|
||||||
isConnected = true
|
isConnected = true
|
||||||
true
|
true
|
||||||
} catch (e: Exception) {
|
} catch (_: Exception) {
|
||||||
close()
|
if (onceMore) {
|
||||||
|
connect(deviceName, false)
|
||||||
|
} else {
|
||||||
|
close()
|
||||||
|
}
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun send(message: String): Boolean = try {
|
private fun send(message: String, host: String): Boolean = try {
|
||||||
writer?.apply {
|
writer?.apply {
|
||||||
write(message)
|
write(message)
|
||||||
write("\n")
|
write("\n")
|
||||||
flush()
|
flush()
|
||||||
}
|
}
|
||||||
true
|
true
|
||||||
} catch (e: Exception) {
|
} catch (_: Exception) {
|
||||||
close()
|
close()
|
||||||
false
|
|
||||||
|
if (connect(host, false)) {
|
||||||
|
send(message, host)
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun close() {
|
private fun close() {
|
||||||
try {
|
try {
|
||||||
writer?.close()
|
writer?.close()
|
||||||
} catch (e: IOException) {
|
} catch (_: IOException) {
|
||||||
// Catch exception if writer wasn't initialized.
|
// Catch exception if writer wasn't initialized.
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
socket?.close()
|
socket?.close()
|
||||||
} catch (e: IOException) {
|
} catch (_: IOException) {
|
||||||
// Nothing we can do about this but keep
|
// Nothing we can do about this but keep
|
||||||
// the app from crashing.
|
// the app from crashing.
|
||||||
}
|
}
|
||||||
@@ -106,11 +114,15 @@ private fun close() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun findByName(findableName: String): BluetoothDevice? {
|
private fun findByName(findableName: String): BluetoothDevice? {
|
||||||
val deviceList = blue.bondedDevices
|
try {
|
||||||
for (device in deviceList) {
|
val deviceList = blue.bondedDevices
|
||||||
if (device.address == findableName) {
|
for (device in deviceList) {
|
||||||
return device
|
if (device.address == findableName) {
|
||||||
|
return device
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
} catch (_: SecurityException) {
|
||||||
|
// Fall through.
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,210 @@
|
|||||||
|
package de.markusfisch.android.binaryeye.content
|
||||||
|
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import de.markusfisch.android.binaryeye.database.Scan
|
||||||
|
import de.markusfisch.android.binaryeye.graphics.COLOR_BLACK
|
||||||
|
import de.markusfisch.android.binaryeye.graphics.COLOR_WHITE
|
||||||
|
import de.markusfisch.android.zxingcpp.ZxingCpp
|
||||||
|
import de.markusfisch.android.zxingcpp.ZxingCpp.BarcodeFormat
|
||||||
|
import de.markusfisch.android.zxingcpp.ZxingCpp.BitMatrix
|
||||||
|
import de.markusfisch.android.zxingcpp.ZxingCpp.toBitmap
|
||||||
|
import de.markusfisch.android.zxingcpp.ZxingCpp.toSvg
|
||||||
|
import de.markusfisch.android.zxingcpp.ZxingCpp.toText
|
||||||
|
import java.util.Arrays
|
||||||
|
|
||||||
|
enum class BarcodeColors {
|
||||||
|
BLACK_ON_WHITE,
|
||||||
|
WHITE_ON_BLACK,
|
||||||
|
BLACK_ON_TRANSPARENT,
|
||||||
|
WHITE_ON_TRANSPARENT;
|
||||||
|
|
||||||
|
fun foregroundColor(): Int = when (this) {
|
||||||
|
BLACK_ON_WHITE,
|
||||||
|
BLACK_ON_TRANSPARENT -> COLOR_BLACK
|
||||||
|
|
||||||
|
WHITE_ON_BLACK,
|
||||||
|
WHITE_ON_TRANSPARENT -> COLOR_WHITE
|
||||||
|
}
|
||||||
|
|
||||||
|
fun backgroundColor(): Int = when (this) {
|
||||||
|
BLACK_ON_WHITE -> COLOR_WHITE
|
||||||
|
WHITE_ON_BLACK -> COLOR_BLACK
|
||||||
|
BLACK_ON_TRANSPARENT,
|
||||||
|
WHITE_ON_TRANSPARENT -> 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Scan.toBarcode(): Barcode<*> {
|
||||||
|
val content = text.ifEmpty {
|
||||||
|
raw
|
||||||
|
}
|
||||||
|
return if (symbol != null) {
|
||||||
|
BitMatrixBarcode(
|
||||||
|
symbol,
|
||||||
|
content,
|
||||||
|
format
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
ContentBarcode(
|
||||||
|
content,
|
||||||
|
format,
|
||||||
|
errorCorrectionLevel.toErrorCorrectionInt(),
|
||||||
|
margin = 1
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun String?.toErrorCorrectionInt() = when (this) {
|
||||||
|
"L" -> 0
|
||||||
|
"M" -> 4
|
||||||
|
"Q" -> 6
|
||||||
|
"H" -> 8
|
||||||
|
else -> -1
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class Barcode<T>(
|
||||||
|
val content: T,
|
||||||
|
val format: BarcodeFormat,
|
||||||
|
val colors: BarcodeColors
|
||||||
|
) {
|
||||||
|
private var _bitmap: Bitmap? = null
|
||||||
|
fun bitmap(): Bitmap {
|
||||||
|
val b = _bitmap ?: bitmap(512)
|
||||||
|
_bitmap = b
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
fun bitmap(size: Int): Bitmap {
|
||||||
|
return toBitmap(size)
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract fun toBitmap(size: Int): Bitmap
|
||||||
|
|
||||||
|
private var _svg: String? = null
|
||||||
|
fun svg(): String {
|
||||||
|
val s = _svg ?: toSvg()
|
||||||
|
_svg = s
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract fun toSvg(): String
|
||||||
|
|
||||||
|
private var _text: String? = null
|
||||||
|
fun text(): String {
|
||||||
|
val t = _text ?: toText()
|
||||||
|
_text = t
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract fun toText(): String
|
||||||
|
|
||||||
|
fun textOrHex(): String = when (content) {
|
||||||
|
is String -> content
|
||||||
|
is ByteArray -> content.toHexString()
|
||||||
|
else -> throw IllegalArgumentException("Illegal arguments")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ContentBarcode<T>(
|
||||||
|
content: T,
|
||||||
|
format: BarcodeFormat,
|
||||||
|
val ecLevel: Int = -1,
|
||||||
|
val margin: Int = 0,
|
||||||
|
colors: BarcodeColors = BarcodeColors.BLACK_ON_WHITE
|
||||||
|
) : Barcode<T>(content, format, colors) {
|
||||||
|
override fun toBitmap(size: Int) = ZxingCpp.encodeAsBitmap(
|
||||||
|
content, format, size, format.height(size), margin, ecLevel,
|
||||||
|
setColor = colors.foregroundColor(),
|
||||||
|
unsetColor = colors.backgroundColor()
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun toSvg() = ZxingCpp.encodeAsSvg(
|
||||||
|
content, format, margin, ecLevel
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun toText() = ZxingCpp.encodeAsText(
|
||||||
|
content, format, margin, ecLevel,
|
||||||
|
inverted = colors == BarcodeColors.BLACK_ON_WHITE
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
class BitMatrixBarcode<T>(
|
||||||
|
val bitMatrix: BitMatrix,
|
||||||
|
content: T,
|
||||||
|
format: BarcodeFormat,
|
||||||
|
colors: BarcodeColors = BarcodeColors.BLACK_ON_WHITE
|
||||||
|
) : Barcode<T>(content, format, colors) {
|
||||||
|
override fun toBitmap(size: Int) = bitMatrix.inflate(size).toBitmap(
|
||||||
|
setColor = colors.foregroundColor(),
|
||||||
|
unsetColor = colors.backgroundColor()
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun toSvg() = bitMatrix.inflate().toSvg()
|
||||||
|
|
||||||
|
override fun toText() = bitMatrix.inflate().toText(
|
||||||
|
inverted = colors == BarcodeColors.BLACK_ON_WHITE
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun BitMatrix.inflate(
|
||||||
|
size: Int = 0,
|
||||||
|
quietZone: Boolean = true
|
||||||
|
): BitMatrix {
|
||||||
|
val extraModules = if (quietZone) 2 else 0
|
||||||
|
val codeWidth = width + extraModules
|
||||||
|
val codeHeight = height + extraModules
|
||||||
|
var scale = size / codeWidth
|
||||||
|
if (scale < 1) {
|
||||||
|
if (size > 0 && !quietZone) {
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
scale = 1
|
||||||
|
}
|
||||||
|
val outWidth = codeWidth * scale
|
||||||
|
val outHeight = codeHeight * scale
|
||||||
|
val scaled = BitMatrix(
|
||||||
|
outWidth,
|
||||||
|
outHeight,
|
||||||
|
ByteArray(outWidth * outHeight)
|
||||||
|
)
|
||||||
|
Arrays.fill(scaled.data, 1)
|
||||||
|
val padding = extraModules / 2
|
||||||
|
val left = padding * scale
|
||||||
|
val top = padding * scale
|
||||||
|
val right = outWidth - left
|
||||||
|
val bottom = outHeight - top
|
||||||
|
for (y in top until bottom) {
|
||||||
|
var dst = y * outWidth + left
|
||||||
|
val offset = (y - top) / scale * width
|
||||||
|
for (x in left until right) {
|
||||||
|
scaled.data[dst++] = data[
|
||||||
|
offset + (x - left) / scale
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return scaled
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun BarcodeFormat.height(size: Int) = when (this) {
|
||||||
|
// 1D barcodes don't need as much vertical space.
|
||||||
|
BarcodeFormat.Codabar,
|
||||||
|
BarcodeFormat.Code39,
|
||||||
|
BarcodeFormat.Code39Ext,
|
||||||
|
BarcodeFormat.Code32,
|
||||||
|
BarcodeFormat.PZN,
|
||||||
|
BarcodeFormat.Code93,
|
||||||
|
BarcodeFormat.Code128,
|
||||||
|
BarcodeFormat.DataBarOmni,
|
||||||
|
BarcodeFormat.DataBarStk,
|
||||||
|
BarcodeFormat.DataBarLtd,
|
||||||
|
BarcodeFormat.DataBarExp,
|
||||||
|
BarcodeFormat.DataBarExpStk,
|
||||||
|
BarcodeFormat.DXFilmEdge,
|
||||||
|
BarcodeFormat.EAN8,
|
||||||
|
BarcodeFormat.EAN13,
|
||||||
|
BarcodeFormat.ITF,
|
||||||
|
BarcodeFormat.UPCA,
|
||||||
|
BarcodeFormat.UPCE -> size / 3
|
||||||
|
|
||||||
|
else -> size
|
||||||
|
}
|
||||||
@@ -8,16 +8,11 @@ import android.os.PersistableBundle
|
|||||||
|
|
||||||
fun Context.copyToClipboard(text: String, isSensitive: Boolean = false) {
|
fun Context.copyToClipboard(text: String, isSensitive: Boolean = false) {
|
||||||
val service = getSystemService(Context.CLIPBOARD_SERVICE)
|
val service = getSystemService(Context.CLIPBOARD_SERVICE)
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) {
|
(service as android.content.ClipboardManager).setPrimaryClip(
|
||||||
@Suppress("DEPRECATION")
|
ClipData.newPlainText("plain text", text).apply {
|
||||||
(service as android.text.ClipboardManager).text = text
|
setSensitive(isSensitive)
|
||||||
} else {
|
}
|
||||||
(service as android.content.ClipboardManager).setPrimaryClip(
|
)
|
||||||
ClipData.newPlainText("plain text", text).apply {
|
|
||||||
setSensitive(isSensitive)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun ClipData.setSensitive(isSensitive: Boolean) {
|
private fun ClipData.setSensitive(isSensitive: Boolean) {
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
package de.markusfisch.android.binaryeye.content
|
|
||||||
|
|
||||||
fun ByteArray.toHexString() = joinToString("") { "%02X".format(it) }
|
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
package de.markusfisch.android.binaryeye.content
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import de.markusfisch.android.binaryeye.R
|
||||||
|
|
||||||
|
private val idToLabel = hashMapOf(
|
||||||
|
"DCA" to R.string.idl_vehicle_class,
|
||||||
|
"DCB" to R.string.idl_restriction_codes,
|
||||||
|
"DCD" to R.string.idl_endorsement_codes,
|
||||||
|
"DBA" to R.string.idl_date_of_expiry,
|
||||||
|
"DCS" to R.string.idl_customer_family_name,
|
||||||
|
"DAC" to R.string.idl_customer_first_name,
|
||||||
|
"DCT" to R.string.idl_customer_first_name_alt,
|
||||||
|
"DAD" to R.string.idl_customer_middle_name,
|
||||||
|
"DBD" to R.string.idl_date_of_issue,
|
||||||
|
"DBB" to R.string.idl_date_of_birth,
|
||||||
|
"DBC" to R.string.idl_sex,
|
||||||
|
"DAY" to R.string.idl_eye_color,
|
||||||
|
"DAU" to R.string.idl_body_height,
|
||||||
|
"DAG" to R.string.idl_address_street,
|
||||||
|
"DAI" to R.string.idl_address_city,
|
||||||
|
"DAJ" to R.string.idl_address_jurisdiction_code,
|
||||||
|
"DAK" to R.string.idl_address_postal_code,
|
||||||
|
"DAQ" to R.string.idl_customer_id_number,
|
||||||
|
"DCF" to R.string.idl_document_discriminator,
|
||||||
|
"DCG" to R.string.idl_country,
|
||||||
|
"DDE" to R.string.idl_family_name_truncation,
|
||||||
|
"DDF" to R.string.idl_first_name_truncation,
|
||||||
|
"DDG" to R.string.idl_middle_name_truncation,
|
||||||
|
"DAH" to R.string.idl_address_street_2,
|
||||||
|
"DAZ" to R.string.idl_hair_color,
|
||||||
|
"DCI" to R.string.idl_place_of_birth,
|
||||||
|
"DCJ" to R.string.idl_audit_information,
|
||||||
|
"DCK" to R.string.idl_inventory_control_number,
|
||||||
|
"DBN" to R.string.idl_alias_family_name,
|
||||||
|
"DBG" to R.string.idl_alias_given_name,
|
||||||
|
"DBS" to R.string.idl_alias_suffix_name,
|
||||||
|
"DCU" to R.string.idl_name_suffix,
|
||||||
|
"DCE" to R.string.idl_weight_range,
|
||||||
|
"DCL" to R.string.idl_ethnicity,
|
||||||
|
"DCM" to R.string.idl_standard_vehicle_classification,
|
||||||
|
"DCN" to R.string.idl_standard_endorsement_code,
|
||||||
|
"DCO" to R.string.idl_standard_restriction_code,
|
||||||
|
"DCP" to R.string.idl_vehicle_classification_description,
|
||||||
|
"DCQ" to R.string.idl_endorsement_code_description,
|
||||||
|
"DCR" to R.string.idl_restriction_code_description,
|
||||||
|
"DDA" to R.string.idl_compliance_type,
|
||||||
|
"DDB" to R.string.idl_card_revision_date,
|
||||||
|
"DDC" to R.string.idl_hazmat_endorsement_expiration_date,
|
||||||
|
"DDD" to R.string.idl_limited_duration_document_indicator,
|
||||||
|
"DAW" to R.string.idl_weight_pounds,
|
||||||
|
"DAX" to R.string.idl_weight_kilograms,
|
||||||
|
"DDH" to R.string.idl_under_18_until_date,
|
||||||
|
"DDI" to R.string.idl_under_19_until_date,
|
||||||
|
"DDJ" to R.string.idl_under_21_until_date,
|
||||||
|
"DDK" to R.string.idl_organ_donor_indicator,
|
||||||
|
"DDL" to R.string.idl_veteran_indicator
|
||||||
|
)
|
||||||
|
|
||||||
|
fun Context.idlToRes(id: String): String {
|
||||||
|
val resId = idToLabel[id]
|
||||||
|
return if (resId != null) {
|
||||||
|
getString(resId)
|
||||||
|
} else {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
package de.markusfisch.android.binaryeye.content
|
||||||
|
|
||||||
|
data class IdlInfo(
|
||||||
|
var iin: String? = null,
|
||||||
|
val elements: MutableMap<String, String> = linkedMapOf()
|
||||||
|
)
|
||||||
|
|
||||||
|
object IdlParser {
|
||||||
|
private fun resolveSex(value: String): String = when (value.firstOrNull()) {
|
||||||
|
'1' -> "M"
|
||||||
|
'2' -> "F"
|
||||||
|
'9' -> "X"
|
||||||
|
else -> value
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun skipWhitespace(s: String, p: Int): Int {
|
||||||
|
var i = p
|
||||||
|
while (i < s.length && s[i].isWhitespace()) {
|
||||||
|
++i;
|
||||||
|
}
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun findSubType(s: String, idlInfo: IdlInfo): String {
|
||||||
|
val len = s.length
|
||||||
|
var p = skipWhitespace(s, 0)
|
||||||
|
|
||||||
|
if (p < len && s[p] == '@') {
|
||||||
|
while (p < len) {
|
||||||
|
if (!s[p].isDigit()) {
|
||||||
|
++p
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
var d = p
|
||||||
|
while (d < len && s[d].isDigit()) {
|
||||||
|
++d
|
||||||
|
}
|
||||||
|
if (d - p < 8 || len - d < 3) {
|
||||||
|
p = d
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
val code = s.substring(d, d + 2)
|
||||||
|
if (code != "DL" && code != "ID") {
|
||||||
|
p = d + 2
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
idlInfo.elements["DL"] = code
|
||||||
|
d += 2
|
||||||
|
|
||||||
|
while (d < len) {
|
||||||
|
while (d < len && s[d] !in "DI") {
|
||||||
|
++d
|
||||||
|
}
|
||||||
|
if (len - d < 2) break
|
||||||
|
|
||||||
|
val sec = s.substring(d, d + 2)
|
||||||
|
if (sec == "DL" || sec == "ID") {
|
||||||
|
return s.substring(d + 2)
|
||||||
|
}
|
||||||
|
++d
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun findIIN(s: String): String? {
|
||||||
|
val ansi = s.indexOf("ANSI").takeIf { it >= 0 } ?: return null
|
||||||
|
var p = ansi + 4
|
||||||
|
val len = s.length
|
||||||
|
val whitespaceSkipped = skipWhitespace(s, p)
|
||||||
|
if (whitespaceSkipped == p) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
p = whitespaceSkipped
|
||||||
|
val iinStart = p
|
||||||
|
var digits = 0
|
||||||
|
while (digits < 6 && p < len && s[p].isDigit()) {
|
||||||
|
++p
|
||||||
|
++digits
|
||||||
|
}
|
||||||
|
return s.substring(iinStart, p)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun parse(s: String?): IdlInfo? {
|
||||||
|
if (s.isNullOrEmpty()) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
val idlInfo = IdlInfo().apply {
|
||||||
|
iin = findIIN(s)
|
||||||
|
}
|
||||||
|
val data = findSubType(s, idlInfo)
|
||||||
|
val len = data.length
|
||||||
|
var start = 0
|
||||||
|
|
||||||
|
for (p in 0..len) {
|
||||||
|
if (p < len && data[p] > '\u001f') {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (p - start > 3) {
|
||||||
|
val vp = start + 3
|
||||||
|
val key = data.substring(start, vp)
|
||||||
|
|
||||||
|
if (key.matches(Regex("[DZ][A-Z]{2}"))) {
|
||||||
|
var value = data.substring(vp, p)
|
||||||
|
if (key == "DBC") {
|
||||||
|
value = resolveSex(value)
|
||||||
|
}
|
||||||
|
idlInfo.elements[key] = value.trim()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
start = p + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
return if (idlInfo.elements.isEmpty()) null else idlInfo
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,11 +5,14 @@ import android.content.Context
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.support.v4.content.FileProvider
|
import androidx.core.content.FileProvider
|
||||||
|
import androidx.core.net.toUri
|
||||||
import de.markusfisch.android.binaryeye.BuildConfig
|
import de.markusfisch.android.binaryeye.BuildConfig
|
||||||
import de.markusfisch.android.binaryeye.R
|
import de.markusfisch.android.binaryeye.R
|
||||||
import de.markusfisch.android.binaryeye.widget.toast
|
import de.markusfisch.android.binaryeye.widget.toast
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import java.io.FileOutputStream
|
||||||
|
import java.io.IOException
|
||||||
|
|
||||||
fun Context.execShareIntent(intent: Intent): Boolean = if (
|
fun Context.execShareIntent(intent: Intent): Boolean = if (
|
||||||
!startIntent(intent)
|
!startIntent(intent)
|
||||||
@@ -31,12 +34,20 @@ fun Context.startIntent(intent: Intent): Boolean = try {
|
|||||||
// necessary.
|
// necessary.
|
||||||
startActivity(intent)
|
startActivity(intent)
|
||||||
true
|
true
|
||||||
} catch (e: ActivityNotFoundException) {
|
} catch (_: ActivityNotFoundException) {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Context.openUrl(url: String, silent: Boolean = false): Boolean {
|
fun Context.openUrl(url: String, silent: Boolean = false): Boolean {
|
||||||
val intent = Intent(Intent.ACTION_VIEW, url.parseAndNormalizeUri())
|
return openUri(url.parseAndNormalizeUri(), silent)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Context.openUri(uri: Uri, silent: Boolean = false): Boolean {
|
||||||
|
val intent = Intent(Intent.ACTION_VIEW, uri).apply {
|
||||||
|
if (uri.scheme == "content") {
|
||||||
|
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||||
|
}
|
||||||
|
}
|
||||||
return if (silent) {
|
return if (silent) {
|
||||||
startIntent(intent)
|
startIntent(intent)
|
||||||
} else {
|
} else {
|
||||||
@@ -44,12 +55,8 @@ fun Context.openUrl(url: String, silent: Boolean = false): Boolean {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun String.parseAndNormalizeUri(): Uri = Uri.parse(this).let {
|
fun String.parseAndNormalizeUri(): Uri = toUri().let {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
|
it.normalizeScheme()
|
||||||
it.normalizeScheme()
|
|
||||||
} else {
|
|
||||||
it
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Context.shareText(text: String, mimeType: String = "text/plain") {
|
fun Context.shareText(text: String, mimeType: String = "text/plain") {
|
||||||
@@ -59,6 +66,44 @@ fun Context.shareText(text: String, mimeType: String = "text/plain") {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var lastShareFile: File? = null
|
||||||
|
fun wipeLastShareFile() {
|
||||||
|
lastShareFile?.let {
|
||||||
|
it.delete()
|
||||||
|
lastShareFile = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <T> Context.shareAsFile(content: T, fileName: String) {
|
||||||
|
val (bytes, mimeType) = when (content) {
|
||||||
|
is ByteArray -> Pair(
|
||||||
|
content,
|
||||||
|
"text/plain"
|
||||||
|
)
|
||||||
|
|
||||||
|
is String -> Pair(
|
||||||
|
content.toByteArray(),
|
||||||
|
"application/octet-stream"
|
||||||
|
)
|
||||||
|
|
||||||
|
else -> throw IllegalArgumentException(
|
||||||
|
"Unsupported content text for shareAsFile()"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
wipeLastShareFile()
|
||||||
|
val file = File(externalCacheDir, fileName)
|
||||||
|
try {
|
||||||
|
FileOutputStream(file).use {
|
||||||
|
it.write(bytes)
|
||||||
|
}
|
||||||
|
} catch (_: IOException) {
|
||||||
|
toast(R.string.error_saving_file)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
lastShareFile = file
|
||||||
|
shareFile(file, mimeType)
|
||||||
|
}
|
||||||
|
|
||||||
fun Context.shareFile(file: File, mimeType: String) {
|
fun Context.shareFile(file: File, mimeType: String) {
|
||||||
getUriForFile(file)?.let {
|
getUriForFile(file)?.let {
|
||||||
shareUri(it, mimeType)
|
shareUri(it, mimeType)
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package de.markusfisch.android.binaryeye.database
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.database.Cursor
|
import android.database.Cursor
|
||||||
import de.markusfisch.android.binaryeye.content.toHexString
|
|
||||||
import de.markusfisch.android.binaryeye.io.writeExternalFile
|
import de.markusfisch.android.binaryeye.io.writeExternalFile
|
||||||
import java.io.ByteArrayOutputStream
|
import java.io.ByteArrayOutputStream
|
||||||
import java.io.OutputStream
|
import java.io.OutputStream
|
||||||
@@ -30,9 +29,11 @@ private fun exportCsv(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
val columns = arrayOf(
|
val columns = arrayOf(
|
||||||
|
Database.SCANS_PINNED,
|
||||||
Database.SCANS_DATETIME,
|
Database.SCANS_DATETIME,
|
||||||
Database.SCANS_FORMAT,
|
Database.SCANS_FORMAT,
|
||||||
Database.SCANS_CONTENT,
|
Database.SCANS_NAME,
|
||||||
|
Database.SCANS_TEXT,
|
||||||
Database.SCANS_ERROR_CORRECTION_LEVEL,
|
Database.SCANS_ERROR_CORRECTION_LEVEL,
|
||||||
Database.SCANS_VERSION,
|
Database.SCANS_VERSION,
|
||||||
Database.SCANS_SEQUENCE_SIZE,
|
Database.SCANS_SEQUENCE_SIZE,
|
||||||
@@ -46,7 +47,7 @@ private fun exportCsv(
|
|||||||
val indices = columns.map {
|
val indices = columns.map {
|
||||||
cursor.getColumnIndex(it)
|
cursor.getColumnIndex(it)
|
||||||
}
|
}
|
||||||
val contentIndex = cursor.getColumnIndex(Database.SCANS_CONTENT)
|
val contentIndex = cursor.getColumnIndex(Database.SCANS_TEXT)
|
||||||
val rawIndex = cursor.getColumnIndex(Database.SCANS_RAW)
|
val rawIndex = cursor.getColumnIndex(Database.SCANS_RAW)
|
||||||
outputStream.write(
|
outputStream.write(
|
||||||
columns.joinToString(
|
columns.joinToString(
|
||||||
|
|||||||
@@ -1,15 +1,7 @@
|
|||||||
package de.markusfisch.android.binaryeye.database
|
package de.markusfisch.android.binaryeye.database
|
||||||
|
|
||||||
import android.database.Cursor
|
import android.database.Cursor
|
||||||
|
import de.markusfisch.android.zxingcpp.ZxingCpp
|
||||||
// Overwrite Kotlin's ".use" function for Cursor because Cursor cannot
|
|
||||||
// be cast to Closeable below API level 16. This should be removed when
|
|
||||||
// the minSDK is increased to at least JELLY_BEAN (16).
|
|
||||||
inline fun <R> Cursor.use(block: (Cursor) -> R): R = try {
|
|
||||||
block.invoke(this)
|
|
||||||
} finally {
|
|
||||||
close()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun Cursor.getInt(name: String): Int {
|
fun Cursor.getInt(name: String): Int {
|
||||||
val idx = getColumnIndex(name)
|
val idx = getColumnIndex(name)
|
||||||
@@ -30,3 +22,18 @@ fun Cursor.getBlob(name: String): ByteArray? {
|
|||||||
val idx = getColumnIndex(name)
|
val idx = getColumnIndex(name)
|
||||||
return if (idx < 0) null else getBlob(idx)
|
return if (idx < 0) null else getBlob(idx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun Cursor.getBitMatrix(
|
||||||
|
widthName: String,
|
||||||
|
heightName: String,
|
||||||
|
dataName: String
|
||||||
|
): ZxingCpp.BitMatrix? {
|
||||||
|
val width = getInt(widthName)
|
||||||
|
val height = getInt(heightName)
|
||||||
|
val data = getBlob(dataName)
|
||||||
|
return if (width > 0 && height > 0 && data != null) {
|
||||||
|
ZxingCpp.BitMatrix(width, height, data)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,6 +6,9 @@ import android.database.Cursor
|
|||||||
import android.database.sqlite.SQLiteDatabase
|
import android.database.sqlite.SQLiteDatabase
|
||||||
import android.database.sqlite.SQLiteOpenHelper
|
import android.database.sqlite.SQLiteOpenHelper
|
||||||
import de.markusfisch.android.binaryeye.app.prefs
|
import de.markusfisch.android.binaryeye.app.prefs
|
||||||
|
import de.markusfisch.android.binaryeye.preference.Preferences
|
||||||
|
import de.markusfisch.android.binaryeye.zxingcpp.migrateBarcodeFormatName
|
||||||
|
import de.markusfisch.android.zxingcpp.ZxingCpp.BarcodeFormat
|
||||||
|
|
||||||
class Database {
|
class Database {
|
||||||
private lateinit var db: SQLiteDatabase
|
private lateinit var db: SQLiteDatabase
|
||||||
@@ -14,25 +17,27 @@ class Database {
|
|||||||
db = OpenHelper(context).writableDatabase
|
db = OpenHelper(context).writableDatabase
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getScans(query: String? = null): Cursor? = db.rawQuery(
|
fun getScans(query: String? = null): Cursor = db.rawQuery(
|
||||||
"""SELECT
|
"""SELECT
|
||||||
$SCANS_ID,
|
$SCANS_ID,
|
||||||
|
$SCANS_PINNED,
|
||||||
$SCANS_DATETIME,
|
$SCANS_DATETIME,
|
||||||
$SCANS_NAME,
|
$SCANS_NAME,
|
||||||
$SCANS_CONTENT,
|
$SCANS_TEXT,
|
||||||
$SCANS_FORMAT
|
$SCANS_FORMAT
|
||||||
FROM $SCANS
|
FROM $SCANS
|
||||||
${getWhereClause(query)}
|
${getWhereClause(query)}
|
||||||
ORDER BY $SCANS_DATETIME DESC
|
ORDER BY $SCANS_PINNED DESC, $SCANS_DATETIME DESC
|
||||||
""".trimMargin(), getWhereArguments(query)
|
""".trimMargin(), getWhereArguments(query)
|
||||||
)
|
)
|
||||||
|
|
||||||
fun getScansDetailed(query: String? = null): Cursor? = db.rawQuery(
|
fun getScansDetailed(query: String? = null): Cursor = db.rawQuery(
|
||||||
"""SELECT
|
"""SELECT
|
||||||
$SCANS_ID,
|
$SCANS_ID,
|
||||||
|
$SCANS_PINNED,
|
||||||
$SCANS_DATETIME,
|
$SCANS_DATETIME,
|
||||||
$SCANS_NAME,
|
$SCANS_NAME,
|
||||||
$SCANS_CONTENT,
|
$SCANS_TEXT,
|
||||||
$SCANS_RAW,
|
$SCANS_RAW,
|
||||||
$SCANS_FORMAT,
|
$SCANS_FORMAT,
|
||||||
$SCANS_ERROR_CORRECTION_LEVEL,
|
$SCANS_ERROR_CORRECTION_LEVEL,
|
||||||
@@ -46,7 +51,7 @@ class Database {
|
|||||||
$SCANS_GTIN_ISSUE_NUMBER
|
$SCANS_GTIN_ISSUE_NUMBER
|
||||||
FROM $SCANS
|
FROM $SCANS
|
||||||
${getWhereClause(query)}
|
${getWhereClause(query)}
|
||||||
ORDER BY $SCANS_DATETIME DESC
|
ORDER BY $SCANS_PINNED DESC, $SCANS_DATETIME DESC
|
||||||
""".trimMargin(), getWhereArguments(query)
|
""".trimMargin(), getWhereArguments(query)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -54,7 +59,7 @@ class Database {
|
|||||||
query: String?,
|
query: String?,
|
||||||
prefix: String = "WHERE"
|
prefix: String = "WHERE"
|
||||||
) = if (query?.isNotEmpty() == true) {
|
) = if (query?.isNotEmpty() == true) {
|
||||||
"""$prefix $SCANS_CONTENT LIKE ?
|
"""$prefix $SCANS_TEXT LIKE ?
|
||||||
OR $SCANS_NAME LIKE ?"""
|
OR $SCANS_NAME LIKE ?"""
|
||||||
} else {
|
} else {
|
||||||
""
|
""
|
||||||
@@ -69,12 +74,13 @@ class Database {
|
|||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getScan(id: Long): Scan? = db.rawQuery(
|
fun getScansDetailed(ids: LongArray): Cursor = db.rawQuery(
|
||||||
"""SELECT
|
"""SELECT
|
||||||
$SCANS_ID,
|
$SCANS_ID,
|
||||||
|
$SCANS_PINNED,
|
||||||
$SCANS_DATETIME,
|
$SCANS_DATETIME,
|
||||||
$SCANS_NAME,
|
$SCANS_NAME,
|
||||||
$SCANS_CONTENT,
|
$SCANS_TEXT,
|
||||||
$SCANS_RAW,
|
$SCANS_RAW,
|
||||||
$SCANS_FORMAT,
|
$SCANS_FORMAT,
|
||||||
$SCANS_ERROR_CORRECTION_LEVEL,
|
$SCANS_ERROR_CORRECTION_LEVEL,
|
||||||
@@ -87,16 +93,52 @@ class Database {
|
|||||||
$SCANS_GTIN_PRICE,
|
$SCANS_GTIN_PRICE,
|
||||||
$SCANS_GTIN_ISSUE_NUMBER
|
$SCANS_GTIN_ISSUE_NUMBER
|
||||||
FROM $SCANS
|
FROM $SCANS
|
||||||
|
WHERE $SCANS_ID IN (${ids.joinToString(",")})
|
||||||
|
ORDER BY $SCANS_PINNED DESC, $SCANS_DATETIME DESC
|
||||||
|
""".trimMargin(), null
|
||||||
|
)
|
||||||
|
|
||||||
|
fun getScan(id: Long): Scan? = db.rawQuery(
|
||||||
|
"""SELECT
|
||||||
|
$SCANS_ID,
|
||||||
|
$SCANS_PINNED,
|
||||||
|
$SCANS_DATETIME,
|
||||||
|
$SCANS_NAME,
|
||||||
|
$SCANS_TEXT,
|
||||||
|
$SCANS_RAW,
|
||||||
|
$SCANS_FORMAT,
|
||||||
|
$SCANS_ERROR_CORRECTION_LEVEL,
|
||||||
|
$SCANS_VERSION,
|
||||||
|
$SCANS_DATA_MASK,
|
||||||
|
$SCANS_SYMBOL_WIDTH,
|
||||||
|
$SCANS_SYMBOL_HEIGHT,
|
||||||
|
$SCANS_SYMBOL_DATA,
|
||||||
|
$SCANS_SEQUENCE_SIZE,
|
||||||
|
$SCANS_SEQUENCE_INDEX,
|
||||||
|
$SCANS_SEQUENCE_ID,
|
||||||
|
$SCANS_GTIN_COUNTRY,
|
||||||
|
$SCANS_GTIN_ADD_ON,
|
||||||
|
$SCANS_GTIN_PRICE,
|
||||||
|
$SCANS_GTIN_ISSUE_NUMBER
|
||||||
|
FROM $SCANS
|
||||||
WHERE $SCANS_ID = ?
|
WHERE $SCANS_ID = ?
|
||||||
""".trimMargin(), arrayOf("$id")
|
""".trimMargin(), arrayOf("$id")
|
||||||
)?.use {
|
).use {
|
||||||
if (it.moveToFirst()) {
|
if (it.moveToFirst()) {
|
||||||
Scan(
|
Scan(
|
||||||
it.getString(SCANS_CONTENT),
|
it.getString(SCANS_TEXT),
|
||||||
it.getBlob(SCANS_RAW),
|
it.getBlob(SCANS_RAW),
|
||||||
it.getString(SCANS_FORMAT),
|
BarcodeFormat.valueOf(
|
||||||
|
it.getString(SCANS_FORMAT).migrateBarcodeFormatName()
|
||||||
|
),
|
||||||
it.getString(SCANS_ERROR_CORRECTION_LEVEL),
|
it.getString(SCANS_ERROR_CORRECTION_LEVEL),
|
||||||
it.getString(SCANS_VERSION),
|
it.getString(SCANS_VERSION),
|
||||||
|
it.getInt(SCANS_DATA_MASK),
|
||||||
|
it.getBitMatrix(
|
||||||
|
SCANS_SYMBOL_WIDTH,
|
||||||
|
SCANS_SYMBOL_HEIGHT,
|
||||||
|
SCANS_SYMBOL_DATA
|
||||||
|
),
|
||||||
it.getInt(SCANS_SEQUENCE_SIZE),
|
it.getInt(SCANS_SEQUENCE_SIZE),
|
||||||
it.getInt(SCANS_SEQUENCE_INDEX),
|
it.getInt(SCANS_SEQUENCE_INDEX),
|
||||||
it.getString(SCANS_SEQUENCE_ID),
|
it.getString(SCANS_SEQUENCE_ID),
|
||||||
@@ -105,77 +147,115 @@ class Database {
|
|||||||
it.getString(SCANS_GTIN_PRICE),
|
it.getString(SCANS_GTIN_PRICE),
|
||||||
it.getString(SCANS_GTIN_ISSUE_NUMBER),
|
it.getString(SCANS_GTIN_ISSUE_NUMBER),
|
||||||
it.getString(SCANS_DATETIME),
|
it.getString(SCANS_DATETIME),
|
||||||
it.getLong(SCANS_ID)
|
it.getLong(SCANS_ID),
|
||||||
|
it.getString(SCANS_NAME),
|
||||||
|
it.getInt(SCANS_PINNED) != 0
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun insertScan(scan: Scan): Long = db.insert(
|
fun insertScan(scan: Scan): Long {
|
||||||
SCANS,
|
when (prefs.ignoreDuplicates()) {
|
||||||
null,
|
Preferences.Companion.IgnoreDuplicates.Consecutive -> {
|
||||||
ContentValues().apply {
|
|
||||||
put(SCANS_DATETIME, scan.dateTime)
|
|
||||||
val isRaw = scan.raw != null
|
|
||||||
if (isRaw) {
|
|
||||||
put(SCANS_CONTENT, "")
|
|
||||||
put(SCANS_RAW, scan.raw)
|
|
||||||
} else {
|
|
||||||
put(SCANS_CONTENT, scan.content)
|
|
||||||
}
|
|
||||||
put(SCANS_FORMAT, scan.format)
|
|
||||||
scan.errorCorrectionLevel?.let {
|
|
||||||
put(SCANS_ERROR_CORRECTION_LEVEL, it)
|
|
||||||
}
|
|
||||||
put(SCANS_VERSION, scan.version)
|
|
||||||
put(SCANS_SEQUENCE_SIZE, scan.sequenceSize)
|
|
||||||
put(SCANS_SEQUENCE_INDEX, scan.sequenceIndex)
|
|
||||||
put(SCANS_SEQUENCE_ID, scan.sequenceId)
|
|
||||||
scan.country?.let { put(SCANS_GTIN_COUNTRY, it) }
|
|
||||||
scan.addOn?.let { put(SCANS_GTIN_ADD_ON, it) }
|
|
||||||
scan.price?.let { put(SCANS_GTIN_PRICE, it) }
|
|
||||||
scan.issueNumber?.let { put(SCANS_GTIN_ISSUE_NUMBER, it) }
|
|
||||||
if (prefs.ignoreConsecutiveDuplicates) {
|
|
||||||
val id = getIdOfLastScan(
|
val id = getIdOfLastScan(
|
||||||
get(SCANS_CONTENT) as String,
|
scan.text,
|
||||||
if (isRaw) get(SCANS_RAW) as ByteArray else null,
|
scan.raw,
|
||||||
scan.format
|
scan.format
|
||||||
)
|
)
|
||||||
if (id > 0L) {
|
if (id > 0L) {
|
||||||
return id
|
return id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Preferences.Companion.IgnoreDuplicates.Any -> {
|
||||||
|
val id = getIdOfScanByContent(scan.text, scan.format)
|
||||||
|
if (id > 0L) {
|
||||||
|
return id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> Unit
|
||||||
}
|
}
|
||||||
)
|
return db.insert(
|
||||||
|
SCANS,
|
||||||
|
null,
|
||||||
|
ContentValues().apply {
|
||||||
|
put(SCANS_DATETIME, scan.dateTime)
|
||||||
|
put(SCANS_TEXT, scan.text)
|
||||||
|
if (scan.raw != null) {
|
||||||
|
put(SCANS_RAW, scan.raw)
|
||||||
|
}
|
||||||
|
put(SCANS_FORMAT, scan.format.name)
|
||||||
|
scan.errorCorrectionLevel?.let {
|
||||||
|
put(SCANS_ERROR_CORRECTION_LEVEL, it)
|
||||||
|
}
|
||||||
|
put(SCANS_VERSION, scan.version)
|
||||||
|
put(SCANS_DATA_MASK, scan.dataMask)
|
||||||
|
if (scan.symbol != null) {
|
||||||
|
put(SCANS_SYMBOL_WIDTH, scan.symbol.width)
|
||||||
|
put(SCANS_SYMBOL_HEIGHT, scan.symbol.height)
|
||||||
|
put(SCANS_SYMBOL_DATA, scan.symbol.data)
|
||||||
|
}
|
||||||
|
put(SCANS_SEQUENCE_SIZE, scan.sequenceSize)
|
||||||
|
put(SCANS_SEQUENCE_INDEX, scan.sequenceIndex)
|
||||||
|
put(SCANS_SEQUENCE_ID, scan.sequenceId)
|
||||||
|
scan.country?.let { put(SCANS_GTIN_COUNTRY, it) }
|
||||||
|
scan.addOn?.let { put(SCANS_GTIN_ADD_ON, it) }
|
||||||
|
scan.price?.let { put(SCANS_GTIN_PRICE, it) }
|
||||||
|
scan.issueNumber?.let { put(SCANS_GTIN_ISSUE_NUMBER, it) }
|
||||||
|
put(SCANS_PINNED, if (scan.pinned) 1 else 0)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
private fun getIdOfLastScan(
|
private fun getIdOfLastScan(
|
||||||
content: String,
|
content: String,
|
||||||
raw: ByteArray?,
|
raw: ByteArray?,
|
||||||
format: String
|
format: BarcodeFormat
|
||||||
): Long = db.rawQuery(
|
): Long = db.rawQuery(
|
||||||
"""SELECT
|
"""SELECT
|
||||||
$SCANS_ID,
|
$SCANS_ID,
|
||||||
$SCANS_CONTENT,
|
$SCANS_TEXT,
|
||||||
$SCANS_RAW,
|
$SCANS_RAW,
|
||||||
$SCANS_FORMAT
|
$SCANS_FORMAT
|
||||||
FROM $SCANS
|
FROM $SCANS
|
||||||
ORDER BY $SCANS_ID DESC
|
ORDER BY $SCANS_ID DESC
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
""".trimMargin(), null
|
""".trimMargin(), null
|
||||||
)?.use {
|
).use {
|
||||||
if (it.count > 0 &&
|
if (it.count > 0 &&
|
||||||
it.moveToFirst() &&
|
it.moveToFirst() &&
|
||||||
it.getString(SCANS_CONTENT) == content &&
|
it.getString(SCANS_TEXT) == content &&
|
||||||
(raw == null || it.getBlob(SCANS_RAW)
|
(raw == null || it.getBlob(SCANS_RAW)
|
||||||
?.contentEquals(raw) == true) &&
|
?.contentEquals(raw) == true) &&
|
||||||
it.getString(SCANS_FORMAT) == format
|
it.getString(SCANS_FORMAT) == format.name
|
||||||
) {
|
) {
|
||||||
it.getLong(SCANS_ID)
|
it.getLong(SCANS_ID)
|
||||||
} else {
|
} else {
|
||||||
0L
|
0L
|
||||||
}
|
}
|
||||||
} ?: 0L
|
}
|
||||||
|
|
||||||
|
private fun getIdOfScanByContent(
|
||||||
|
content: String,
|
||||||
|
format: BarcodeFormat
|
||||||
|
): Long = db.rawQuery(
|
||||||
|
"""SELECT
|
||||||
|
$SCANS_ID
|
||||||
|
FROM $SCANS
|
||||||
|
WHERE $SCANS_TEXT = ?
|
||||||
|
AND $SCANS_FORMAT = ?
|
||||||
|
LIMIT 1
|
||||||
|
""".trimMargin(), arrayOf(content, format.name)
|
||||||
|
).use {
|
||||||
|
if (it.moveToFirst()) {
|
||||||
|
it.getLong(SCANS_ID)
|
||||||
|
} else {
|
||||||
|
0L
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun removeScan(id: Long) {
|
fun removeScan(id: Long) {
|
||||||
db.delete(SCANS, "$SCANS_ID = ?", arrayOf("$id"))
|
db.delete(SCANS, "$SCANS_ID = ?", arrayOf("$id"))
|
||||||
@@ -191,8 +271,14 @@ class Database {
|
|||||||
db.update(SCANS, cv, "$SCANS_ID = ?", arrayOf("$id"))
|
db.update(SCANS, cv, "$SCANS_ID = ?", arrayOf("$id"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun setPinned(id: Long, pinned: Boolean) {
|
||||||
|
val cv = ContentValues()
|
||||||
|
cv.put(SCANS_PINNED, if (pinned) 1 else 0)
|
||||||
|
db.update(SCANS, cv, "$SCANS_ID = ?", arrayOf("$id"))
|
||||||
|
}
|
||||||
|
|
||||||
private class OpenHelper(context: Context) :
|
private class OpenHelper(context: Context) :
|
||||||
SQLiteOpenHelper(context, FILE_NAME, null, 6) {
|
SQLiteOpenHelper(context, FILE_NAME, null, 10) {
|
||||||
override fun onCreate(db: SQLiteDatabase) {
|
override fun onCreate(db: SQLiteDatabase) {
|
||||||
db.createScans()
|
db.createScans()
|
||||||
}
|
}
|
||||||
@@ -217,6 +303,18 @@ class Database {
|
|||||||
if (oldVersion < 6) {
|
if (oldVersion < 6) {
|
||||||
db.migrateToVersionString()
|
db.migrateToVersionString()
|
||||||
}
|
}
|
||||||
|
if (oldVersion < 7) {
|
||||||
|
db.addDataMaskColumn()
|
||||||
|
}
|
||||||
|
if (oldVersion < 8) {
|
||||||
|
db.addSymbolColumns()
|
||||||
|
}
|
||||||
|
if (oldVersion < 9) {
|
||||||
|
db.migrateToNativeFormatNames()
|
||||||
|
}
|
||||||
|
if (oldVersion < 10) {
|
||||||
|
db.addPinnedColumn()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -226,12 +324,16 @@ class Database {
|
|||||||
const val SCANS_ID = "_id"
|
const val SCANS_ID = "_id"
|
||||||
const val SCANS_DATETIME = "_datetime"
|
const val SCANS_DATETIME = "_datetime"
|
||||||
const val SCANS_NAME = "name"
|
const val SCANS_NAME = "name"
|
||||||
const val SCANS_CONTENT = "content"
|
const val SCANS_TEXT = "content"
|
||||||
const val SCANS_RAW = "raw"
|
const val SCANS_RAW = "raw"
|
||||||
const val SCANS_FORMAT = "format"
|
const val SCANS_FORMAT = "format"
|
||||||
const val SCANS_ERROR_CORRECTION_LEVEL = "error_correction_level"
|
const val SCANS_ERROR_CORRECTION_LEVEL = "error_correction_level"
|
||||||
const val SCANS_VERSION_NUMBER = "version_number"
|
const val SCANS_VERSION_NUMBER = "version_number"
|
||||||
const val SCANS_VERSION = "version"
|
const val SCANS_VERSION = "version"
|
||||||
|
const val SCANS_DATA_MASK = "data_mask"
|
||||||
|
const val SCANS_SYMBOL_WIDTH = "symbol_width"
|
||||||
|
const val SCANS_SYMBOL_HEIGHT = "symbol_height"
|
||||||
|
const val SCANS_SYMBOL_DATA = "symbol_data"
|
||||||
const val SCANS_ISSUE_NUMBER = "issue_number"
|
const val SCANS_ISSUE_NUMBER = "issue_number"
|
||||||
const val SCANS_ORIENTATION = "orientation"
|
const val SCANS_ORIENTATION = "orientation"
|
||||||
const val SCANS_OTHER_META_DATA = "other_meta_data"
|
const val SCANS_OTHER_META_DATA = "other_meta_data"
|
||||||
@@ -246,6 +348,7 @@ class Database {
|
|||||||
const val SCANS_GTIN_ADD_ON = "gtin_add_on"
|
const val SCANS_GTIN_ADD_ON = "gtin_add_on"
|
||||||
const val SCANS_GTIN_PRICE = "gtin_price"
|
const val SCANS_GTIN_PRICE = "gtin_price"
|
||||||
const val SCANS_GTIN_ISSUE_NUMBER = "gtin_issue_number"
|
const val SCANS_GTIN_ISSUE_NUMBER = "gtin_issue_number"
|
||||||
|
const val SCANS_PINNED = "pinned"
|
||||||
|
|
||||||
private fun SQLiteDatabase.createScans() {
|
private fun SQLiteDatabase.createScans() {
|
||||||
execSQL("DROP TABLE IF EXISTS $SCANS".trimMargin())
|
execSQL("DROP TABLE IF EXISTS $SCANS".trimMargin())
|
||||||
@@ -254,18 +357,23 @@ class Database {
|
|||||||
$SCANS_ID INTEGER PRIMARY KEY AUTOINCREMENT,
|
$SCANS_ID INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
$SCANS_DATETIME DATETIME NOT NULL,
|
$SCANS_DATETIME DATETIME NOT NULL,
|
||||||
$SCANS_NAME TEXT,
|
$SCANS_NAME TEXT,
|
||||||
$SCANS_CONTENT TEXT NOT NULL,
|
$SCANS_TEXT TEXT NOT NULL,
|
||||||
$SCANS_RAW BLOB,
|
$SCANS_RAW BLOB,
|
||||||
$SCANS_FORMAT TEXT NOT NULL,
|
$SCANS_FORMAT TEXT NOT NULL,
|
||||||
$SCANS_ERROR_CORRECTION_LEVEL TEXT,
|
$SCANS_ERROR_CORRECTION_LEVEL TEXT,
|
||||||
$SCANS_VERSION TEXT,
|
$SCANS_VERSION TEXT,
|
||||||
|
$SCANS_DATA_MASK INTEGER,
|
||||||
|
$SCANS_SYMBOL_WIDTH INTEGER,
|
||||||
|
$SCANS_SYMBOL_HEIGHT INTEGER,
|
||||||
|
$SCANS_SYMBOL_DATA BLOB,
|
||||||
$SCANS_SEQUENCE_SIZE INTEGER,
|
$SCANS_SEQUENCE_SIZE INTEGER,
|
||||||
$SCANS_SEQUENCE_INDEX INTEGER,
|
$SCANS_SEQUENCE_INDEX INTEGER,
|
||||||
$SCANS_SEQUENCE_ID TEXT,
|
$SCANS_SEQUENCE_ID TEXT,
|
||||||
$SCANS_GTIN_COUNTRY TEXT,
|
$SCANS_GTIN_COUNTRY TEXT,
|
||||||
$SCANS_GTIN_ADD_ON TEXT,
|
$SCANS_GTIN_ADD_ON TEXT,
|
||||||
$SCANS_GTIN_PRICE TEXT,
|
$SCANS_GTIN_PRICE TEXT,
|
||||||
$SCANS_GTIN_ISSUE_NUMBER TEXT
|
$SCANS_GTIN_ISSUE_NUMBER TEXT,
|
||||||
|
$SCANS_PINNED INTEGER NOT NULL DEFAULT 0
|
||||||
)""".trimMargin()
|
)""".trimMargin()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -286,7 +394,7 @@ class Database {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun SQLiteDatabase.addNameColumn() {
|
private fun SQLiteDatabase.addNameColumn() {
|
||||||
execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_NAME TEXT".trimMargin())
|
execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_NAME TEXT")
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun SQLiteDatabase.migrateToZxingCpp() {
|
private fun SQLiteDatabase.migrateToZxingCpp() {
|
||||||
@@ -306,9 +414,63 @@ class Database {
|
|||||||
execSQL("UPDATE $SCANS SET $SCANS_GTIN_ISSUE_NUMBER = $SCANS_ISSUE_NUMBER")
|
execSQL("UPDATE $SCANS SET $SCANS_GTIN_ISSUE_NUMBER = $SCANS_ISSUE_NUMBER")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun SQLiteDatabase.migrateToNativeFormatNames() {
|
||||||
|
val mappings = listOf(
|
||||||
|
"NONE" to "None",
|
||||||
|
"AZTEC" to "Aztec",
|
||||||
|
"CODABAR" to "Codabar",
|
||||||
|
"CODE_39" to "Code39",
|
||||||
|
"CODE_39_EXT" to "Code39Ext",
|
||||||
|
"CODE_32" to "Code32",
|
||||||
|
"PZN" to "PZN",
|
||||||
|
"CODE_93" to "Code93",
|
||||||
|
"CODE_128" to "Code128",
|
||||||
|
"DATA_BAR_OMNI" to "DataBarOmni",
|
||||||
|
"DATA_BAR_STK" to "DataBarStk",
|
||||||
|
"DATA_BAR_LTD" to "DataBarLtd",
|
||||||
|
"DATA_BAR_EXPANDED" to "DataBarExp",
|
||||||
|
"DATA_BAR_EXP_STK" to "DataBarExpStk",
|
||||||
|
"DATA_MATRIX" to "DataMatrix",
|
||||||
|
"DX_FILM_EDGE" to "DXFilmEdge",
|
||||||
|
"EAN_8" to "EAN8",
|
||||||
|
"EAN_13" to "EAN13",
|
||||||
|
"ITF" to "ITF",
|
||||||
|
"MAXICODE" to "MaxiCode",
|
||||||
|
"PDF_417" to "PDF417",
|
||||||
|
"QR_CODE" to "QRCode",
|
||||||
|
"MICRO_QR_CODE" to "MicroQRCode",
|
||||||
|
"RMQR_CODE" to "RMQRCode",
|
||||||
|
"UPC_A" to "UPCA",
|
||||||
|
"UPC_E" to "UPCE"
|
||||||
|
)
|
||||||
|
for ((oldName, newName) in mappings) {
|
||||||
|
execSQL(
|
||||||
|
"UPDATE $SCANS SET $SCANS_FORMAT = ? WHERE $SCANS_FORMAT = ?",
|
||||||
|
arrayOf(newName, oldName)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun SQLiteDatabase.migrateToVersionString() {
|
private fun SQLiteDatabase.migrateToVersionString() {
|
||||||
execSQL("ALTER TABLE $SCANS ADD $SCANS_VERSION TEXT")
|
execSQL("ALTER TABLE $SCANS ADD $SCANS_VERSION TEXT")
|
||||||
execSQL("UPDATE $SCANS SET $SCANS_VERSION = $SCANS_VERSION_NUMBER")
|
execSQL("UPDATE $SCANS SET $SCANS_VERSION = $SCANS_VERSION_NUMBER")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun SQLiteDatabase.addDataMaskColumn() {
|
||||||
|
execSQL("ALTER TABLE $SCANS ADD $SCANS_DATA_MASK INTEGER")
|
||||||
|
execSQL("UPDATE $SCANS SET $SCANS_DATA_MASK = -1")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun SQLiteDatabase.addSymbolColumns() {
|
||||||
|
execSQL("ALTER TABLE $SCANS ADD $SCANS_SYMBOL_WIDTH INTEGER")
|
||||||
|
execSQL("ALTER TABLE $SCANS ADD $SCANS_SYMBOL_HEIGHT INTEGER")
|
||||||
|
execSQL("ALTER TABLE $SCANS ADD $SCANS_SYMBOL_DATA BLOB")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun SQLiteDatabase.addPinnedColumn() {
|
||||||
|
execSQL(
|
||||||
|
"ALTER TABLE $SCANS ADD COLUMN $SCANS_PINNED INTEGER NOT NULL DEFAULT 0"
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package de.markusfisch.android.binaryeye.database
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.database.Cursor
|
import android.database.Cursor
|
||||||
import de.markusfisch.android.binaryeye.content.toHexString
|
|
||||||
import de.markusfisch.android.binaryeye.io.writeExternalFile
|
import de.markusfisch.android.binaryeye.io.writeExternalFile
|
||||||
import org.json.JSONArray
|
import org.json.JSONArray
|
||||||
import org.json.JSONException
|
import org.json.JSONException
|
||||||
@@ -22,9 +21,11 @@ fun Cursor.exportJson(): String? {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
val columns = arrayOf(
|
val columns = arrayOf(
|
||||||
|
Database.SCANS_PINNED,
|
||||||
Database.SCANS_DATETIME,
|
Database.SCANS_DATETIME,
|
||||||
Database.SCANS_FORMAT,
|
Database.SCANS_FORMAT,
|
||||||
Database.SCANS_CONTENT,
|
Database.SCANS_NAME,
|
||||||
|
Database.SCANS_TEXT,
|
||||||
Database.SCANS_ERROR_CORRECTION_LEVEL,
|
Database.SCANS_ERROR_CORRECTION_LEVEL,
|
||||||
Database.SCANS_VERSION,
|
Database.SCANS_VERSION,
|
||||||
Database.SCANS_SEQUENCE_SIZE,
|
Database.SCANS_SEQUENCE_SIZE,
|
||||||
@@ -38,7 +39,7 @@ fun Cursor.exportJson(): String? {
|
|||||||
val indices = columns.map {
|
val indices = columns.map {
|
||||||
Pair(getColumnIndex(it), it)
|
Pair(getColumnIndex(it), it)
|
||||||
}
|
}
|
||||||
val contentIndex = getColumnIndex(Database.SCANS_CONTENT)
|
val contentIndex = getColumnIndex(Database.SCANS_TEXT)
|
||||||
val rawIndex = getColumnIndex(Database.SCANS_RAW)
|
val rawIndex = getColumnIndex(Database.SCANS_RAW)
|
||||||
val root = JSONArray()
|
val root = JSONArray()
|
||||||
do {
|
do {
|
||||||
@@ -69,7 +70,7 @@ private fun Cursor.toJsonObject(
|
|||||||
obj.put(it.second, value ?: "")
|
obj.put(it.second, value ?: "")
|
||||||
}
|
}
|
||||||
obj
|
obj
|
||||||
} catch (e: JSONException) {
|
} catch (_: JSONException) {
|
||||||
obj
|
obj
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +1,22 @@
|
|||||||
package de.markusfisch.android.binaryeye.database
|
package de.markusfisch.android.binaryeye.database
|
||||||
|
|
||||||
import android.graphics.Bitmap
|
import android.os.Bundle
|
||||||
import android.os.Build
|
|
||||||
import android.os.Parcel
|
|
||||||
import android.os.Parcelable
|
|
||||||
import android.text.format.DateFormat
|
import android.text.format.DateFormat
|
||||||
import de.markusfisch.android.zxingcpp.ZxingCpp
|
import de.markusfisch.android.binaryeye.zxingcpp.migrateBarcodeFormatName
|
||||||
|
import de.markusfisch.android.zxingcpp.ZxingCpp.BarcodeFormat
|
||||||
|
import de.markusfisch.android.zxingcpp.ZxingCpp.BitMatrix
|
||||||
import de.markusfisch.android.zxingcpp.ZxingCpp.ContentType
|
import de.markusfisch.android.zxingcpp.ZxingCpp.ContentType
|
||||||
import de.markusfisch.android.zxingcpp.ZxingCpp.Format
|
|
||||||
import de.markusfisch.android.zxingcpp.ZxingCpp.Result
|
import de.markusfisch.android.zxingcpp.ZxingCpp.Result
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
data class Scan(
|
data class Scan(
|
||||||
val content: String,
|
val text: String,
|
||||||
val raw: ByteArray?,
|
val raw: ByteArray?,
|
||||||
val format: String,
|
val format: BarcodeFormat,
|
||||||
val errorCorrectionLevel: String? = null,
|
val errorCorrectionLevel: String? = null,
|
||||||
val version: String? = null,
|
val version: String? = null,
|
||||||
|
val dataMask: Int = -1,
|
||||||
|
val symbol: BitMatrix? = null,
|
||||||
val sequenceSize: Int = -1,
|
val sequenceSize: Int = -1,
|
||||||
val sequenceIndex: Int = -1,
|
val sequenceIndex: Int = -1,
|
||||||
val sequenceId: String = "",
|
val sequenceId: String = "",
|
||||||
@@ -24,8 +25,10 @@ data class Scan(
|
|||||||
val price: String? = null,
|
val price: String? = null,
|
||||||
val issueNumber: String? = null,
|
val issueNumber: String? = null,
|
||||||
val dateTime: String = getDateTime(),
|
val dateTime: String = getDateTime(),
|
||||||
var id: Long = 0L
|
var id: Long = 0L,
|
||||||
) : Parcelable {
|
val label: String? = null,
|
||||||
|
val pinned: Boolean = false
|
||||||
|
) {
|
||||||
// Needs to be overwritten manually, as ByteArray is an array and
|
// Needs to be overwritten manually, as ByteArray is an array and
|
||||||
// this isn't handled well by Kotlin.
|
// this isn't handled well by Kotlin.
|
||||||
override fun equals(other: Any?): Boolean {
|
override fun equals(other: Any?): Boolean {
|
||||||
@@ -40,19 +43,23 @@ data class Scan(
|
|||||||
|
|
||||||
return id == other.id &&
|
return id == other.id &&
|
||||||
dateTime == other.dateTime &&
|
dateTime == other.dateTime &&
|
||||||
content == other.content &&
|
text == other.text &&
|
||||||
((raw == null && other.raw == null) ||
|
((raw == null && other.raw == null) ||
|
||||||
(raw != null && other.raw != null && raw.contentEquals(other.raw))) &&
|
(raw != null && other.raw != null &&
|
||||||
|
raw.contentEquals(other.raw))) &&
|
||||||
format == other.format &&
|
format == other.format &&
|
||||||
errorCorrectionLevel == other.errorCorrectionLevel &&
|
errorCorrectionLevel == other.errorCorrectionLevel &&
|
||||||
version == other.version &&
|
version == other.version &&
|
||||||
|
dataMask == other.dataMask &&
|
||||||
sequenceSize == other.sequenceSize &&
|
sequenceSize == other.sequenceSize &&
|
||||||
sequenceIndex == other.sequenceIndex &&
|
sequenceIndex == other.sequenceIndex &&
|
||||||
sequenceId == other.sequenceId &&
|
sequenceId == other.sequenceId &&
|
||||||
country == other.country &&
|
country == other.country &&
|
||||||
addOn == other.addOn &&
|
addOn == other.addOn &&
|
||||||
price == other.price &&
|
price == other.price &&
|
||||||
issueNumber == other.issueNumber
|
issueNumber == other.issueNumber &&
|
||||||
|
label == other.label &&
|
||||||
|
pinned == other.pinned
|
||||||
}
|
}
|
||||||
|
|
||||||
// Needs to be overwritten manually, as ByteArray is an array and
|
// Needs to be overwritten manually, as ByteArray is an array and
|
||||||
@@ -60,11 +67,12 @@ data class Scan(
|
|||||||
override fun hashCode(): Int {
|
override fun hashCode(): Int {
|
||||||
var result = id.hashCode()
|
var result = id.hashCode()
|
||||||
result = 31 * result + dateTime.hashCode()
|
result = 31 * result + dateTime.hashCode()
|
||||||
result = 31 * result + content.hashCode()
|
result = 31 * result + text.hashCode()
|
||||||
result = 31 * result + (raw?.contentHashCode() ?: 0)
|
result = 31 * result + (raw?.contentHashCode() ?: 0)
|
||||||
result = 31 * result + format.hashCode()
|
result = 31 * result + format.hashCode()
|
||||||
result = 31 * result + (errorCorrectionLevel?.hashCode() ?: 0)
|
result = 31 * result + (errorCorrectionLevel?.hashCode() ?: 0)
|
||||||
result = 31 * result + version.hashCode()
|
result = 31 * result + version.hashCode()
|
||||||
|
result = 31 * result + dataMask
|
||||||
result = 31 * result + sequenceSize
|
result = 31 * result + sequenceSize
|
||||||
result = 31 * result + sequenceIndex
|
result = 31 * result + sequenceIndex
|
||||||
result = 31 * result + sequenceId.hashCode()
|
result = 31 * result + sequenceId.hashCode()
|
||||||
@@ -72,77 +80,28 @@ data class Scan(
|
|||||||
result = 31 * result + (addOn?.hashCode() ?: 0)
|
result = 31 * result + (addOn?.hashCode() ?: 0)
|
||||||
result = 31 * result + (price?.hashCode() ?: 0)
|
result = 31 * result + (price?.hashCode() ?: 0)
|
||||||
result = 31 * result + (issueNumber?.hashCode() ?: 0)
|
result = 31 * result + (issueNumber?.hashCode() ?: 0)
|
||||||
|
result = 31 * result + (label?.hashCode() ?: 0)
|
||||||
|
result = 31 * result + pinned.hashCode()
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
private constructor(parcel: Parcel) : this(
|
|
||||||
content = parcel.readString() ?: "",
|
|
||||||
raw = parcel.readSizedByteArray(),
|
|
||||||
format = parcel.readString() ?: "",
|
|
||||||
errorCorrectionLevel = parcel.readString(),
|
|
||||||
version = parcel.readString(),
|
|
||||||
sequenceSize = parcel.readInt(),
|
|
||||||
sequenceIndex = parcel.readInt(),
|
|
||||||
sequenceId = parcel.readString() ?: "",
|
|
||||||
country = parcel.readString(),
|
|
||||||
addOn = parcel.readString(),
|
|
||||||
price = parcel.readString(),
|
|
||||||
issueNumber = parcel.readString(),
|
|
||||||
dateTime = parcel.readString() ?: "",
|
|
||||||
id = parcel.readLong()
|
|
||||||
)
|
|
||||||
|
|
||||||
override fun writeToParcel(parcel: Parcel, flags: Int) {
|
|
||||||
parcel.apply {
|
|
||||||
writeString(content)
|
|
||||||
writeSizedByteArray(raw)
|
|
||||||
writeString(format)
|
|
||||||
writeString(errorCorrectionLevel)
|
|
||||||
writeString(version)
|
|
||||||
writeInt(sequenceSize)
|
|
||||||
writeInt(sequenceIndex)
|
|
||||||
writeString(sequenceId)
|
|
||||||
writeString(country)
|
|
||||||
writeString(addOn)
|
|
||||||
writeString(price)
|
|
||||||
writeString(issueNumber)
|
|
||||||
writeString(dateTime)
|
|
||||||
writeLong(id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun describeContents() = 0
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
@JvmField
|
|
||||||
val CREATOR = object : Parcelable.Creator<Scan> {
|
|
||||||
override fun createFromParcel(parcel: Parcel) = Scan(parcel)
|
|
||||||
override fun newArray(size: Int) = arrayOfNulls<Scan>(size)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Result.toScan(): Scan {
|
fun Result.toScan(): Scan {
|
||||||
val content: String
|
|
||||||
val raw: ByteArray?
|
|
||||||
when (contentType) {
|
|
||||||
ContentType.GS1,
|
|
||||||
ContentType.ISO15434,
|
|
||||||
ContentType.TEXT -> {
|
|
||||||
content = text
|
|
||||||
raw = null
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
content = ""
|
|
||||||
raw = rawBytes
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return Scan(
|
return Scan(
|
||||||
content,
|
when (contentType) {
|
||||||
raw,
|
ContentType.GS1,
|
||||||
|
ContentType.ISO15434,
|
||||||
|
ContentType.TEXT -> text
|
||||||
|
|
||||||
|
else -> "" // Show as binary.
|
||||||
|
},
|
||||||
|
rawBytes,
|
||||||
format,
|
format,
|
||||||
ecLevel,
|
ecLevel,
|
||||||
version,
|
version,
|
||||||
|
dataMask,
|
||||||
|
symbol,
|
||||||
sequenceSize,
|
sequenceSize,
|
||||||
sequenceIndex,
|
sequenceIndex,
|
||||||
sequenceId,
|
sequenceId,
|
||||||
@@ -153,69 +112,98 @@ fun Result.toScan(): Scan {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
data class Recreation(
|
fun Scan.toBundle() = Bundle().apply {
|
||||||
val format: Format,
|
putString(SCAN_TEXT, text)
|
||||||
val ecLevel: Int,
|
putByteArray(SCAN_RAW, raw)
|
||||||
val size: Int,
|
putString(SCAN_FORMAT, format.name)
|
||||||
val margin: Int
|
putString(SCAN_ERROR_CORRECTION_LEVEL, errorCorrectionLevel)
|
||||||
) {
|
putString(SCAN_VERSION, version)
|
||||||
fun encode(content: String): Bitmap? = try {
|
putInt(SCAN_DATA_MASK, dataMask)
|
||||||
ZxingCpp.encodeAsBitmap(
|
putInt(SCAN_SYMBOL_WIDTH, symbol?.width ?: 0)
|
||||||
content, format, size, size, margin, ecLevel
|
putInt(SCAN_SYMBOL_HEIGHT, symbol?.height ?: 0)
|
||||||
)
|
putByteArray(SCAN_SYMBOL_DATA, symbol?.data)
|
||||||
} catch (_: RuntimeException) {
|
putInt(SCAN_SEQUENCE_SIZE, sequenceSize)
|
||||||
null
|
putInt(SCAN_SEQUENCE_INDEX, sequenceIndex)
|
||||||
}
|
putString(SCAN_SEQUENCE_ID, sequenceId)
|
||||||
|
putString(SCAN_COUNTRY, country)
|
||||||
|
putString(SCAN_ADD_ON, addOn)
|
||||||
|
putString(SCAN_PRICE, price)
|
||||||
|
putString(SCAN_ISSUE_NUMBER, issueNumber)
|
||||||
|
putString(SCAN_DATE_TIME, dateTime)
|
||||||
|
putLong(SCAN_ID, id)
|
||||||
|
putString(SCAN_LABEL, label)
|
||||||
|
putBoolean(SCAN_PINNED, pinned)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Scan.toRecreation(
|
fun Bundle.toScan(): Scan? {
|
||||||
size: Int = 128,
|
val formatName = getString(SCAN_FORMAT)?.migrateBarcodeFormatName()
|
||||||
margin: Int = -1
|
if (formatName.isNullOrEmpty()) {
|
||||||
) = if (content.isEmpty()) {
|
return null
|
||||||
null
|
}
|
||||||
} else {
|
return try {
|
||||||
Recreation(
|
Scan(
|
||||||
Format.valueOf(format),
|
text = getString(SCAN_TEXT) ?: "",
|
||||||
when (errorCorrectionLevel) {
|
raw = getByteArray(SCAN_RAW),
|
||||||
"L" -> 0
|
format = BarcodeFormat.valueOf(formatName),
|
||||||
"M" -> 4
|
errorCorrectionLevel = getString(SCAN_ERROR_CORRECTION_LEVEL),
|
||||||
"Q" -> 6
|
version = getString(SCAN_VERSION),
|
||||||
"H" -> 8
|
dataMask = getInt(SCAN_DATA_MASK, -1),
|
||||||
else -> -1
|
symbol = getBitMatrix(),
|
||||||
},
|
sequenceSize = getInt(SCAN_SEQUENCE_SIZE, -1),
|
||||||
size,
|
sequenceIndex = getInt(SCAN_SEQUENCE_INDEX, -1),
|
||||||
margin
|
sequenceId = getString(SCAN_SEQUENCE_ID) ?: "",
|
||||||
)
|
country = getString(SCAN_COUNTRY),
|
||||||
|
addOn = getString(SCAN_ADD_ON),
|
||||||
|
price = getString(SCAN_PRICE),
|
||||||
|
issueNumber = getString(SCAN_ISSUE_NUMBER),
|
||||||
|
dateTime = getString(SCAN_DATE_TIME) ?: getDateTime(),
|
||||||
|
id = getLong(SCAN_ID, 0L),
|
||||||
|
label = getString(SCAN_LABEL),
|
||||||
|
pinned = getBoolean(SCAN_PINNED, false)
|
||||||
|
)
|
||||||
|
} catch (_: IllegalArgumentException) {
|
||||||
|
null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getDateTime(
|
private fun getDateTime(
|
||||||
time: Long = System.currentTimeMillis()
|
time: Long = System.currentTimeMillis()
|
||||||
) = DateFormat.format(
|
) = DateFormat.format(
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2) {
|
"yyyy-MM-dd HH:mm:ss",
|
||||||
// Up to API level 17, 'k' was interpreted as 'H', which wasn't
|
|
||||||
// implemented until API level 18.
|
|
||||||
"yyyy-MM-dd kk:mm:ss"
|
|
||||||
} else {
|
|
||||||
"yyyy-MM-dd HH:mm:ss"
|
|
||||||
},
|
|
||||||
time
|
time
|
||||||
).toString()
|
).toString() + String.format(
|
||||||
|
Locale.getDefault(),
|
||||||
|
":%03d",
|
||||||
|
time % 1000
|
||||||
|
)
|
||||||
|
|
||||||
private fun Parcel.writeSizedByteArray(array: ByteArray?) {
|
private fun Bundle.getBitMatrix(): BitMatrix? {
|
||||||
val size = array?.size ?: 0
|
val width = getInt(SCAN_SYMBOL_WIDTH, 0)
|
||||||
writeInt(size)
|
val height = getInt(SCAN_SYMBOL_HEIGHT, 0)
|
||||||
if (size > 0) {
|
val data = getByteArray(SCAN_SYMBOL_DATA)
|
||||||
writeByteArray(array)
|
if (width < 1 || height < 1 || data == null) {
|
||||||
|
return null
|
||||||
}
|
}
|
||||||
|
return BitMatrix(width, height, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun Parcel.readSizedByteArray(): ByteArray? {
|
private const val SCAN_TEXT = "text"
|
||||||
val size = readInt()
|
private const val SCAN_RAW = "raw"
|
||||||
return if (size > 0) {
|
private const val SCAN_FORMAT = "format"
|
||||||
val array = ByteArray(size)
|
private const val SCAN_ERROR_CORRECTION_LEVEL = "error_correction_level"
|
||||||
readByteArray(array)
|
private const val SCAN_VERSION = "version"
|
||||||
array
|
private const val SCAN_DATA_MASK = "data_mask"
|
||||||
} else {
|
private const val SCAN_SYMBOL_WIDTH = "symbol_width"
|
||||||
null
|
private const val SCAN_SYMBOL_HEIGHT = "symbol_height"
|
||||||
}
|
private const val SCAN_SYMBOL_DATA = "symbol_data"
|
||||||
}
|
private const val SCAN_SEQUENCE_SIZE = "sequence_size"
|
||||||
|
private const val SCAN_SEQUENCE_INDEX = "sequence_index"
|
||||||
|
private const val SCAN_SEQUENCE_ID = "sequence_id"
|
||||||
|
private const val SCAN_COUNTRY = "country"
|
||||||
|
private const val SCAN_ADD_ON = "add_on"
|
||||||
|
private const val SCAN_PRICE = "price"
|
||||||
|
private const val SCAN_ISSUE_NUMBER = "issue_number"
|
||||||
|
private const val SCAN_DATE_TIME = "date_time"
|
||||||
|
private const val SCAN_ID = "id"
|
||||||
|
private const val SCAN_LABEL = "label"
|
||||||
|
private const val SCAN_PINNED = "pinned"
|
||||||
|
|||||||
@@ -1,352 +0,0 @@
|
|||||||
package de.markusfisch.android.binaryeye.fragment
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.app.AlertDialog
|
|
||||||
import android.content.Context
|
|
||||||
import android.graphics.Bitmap
|
|
||||||
import android.os.Build
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.support.v4.app.Fragment
|
|
||||||
import android.view.*
|
|
||||||
import android.widget.EditText
|
|
||||||
import de.markusfisch.android.binaryeye.R
|
|
||||||
import de.markusfisch.android.binaryeye.app.hasWritePermission
|
|
||||||
import de.markusfisch.android.binaryeye.content.copyToClipboard
|
|
||||||
import de.markusfisch.android.binaryeye.content.shareFile
|
|
||||||
import de.markusfisch.android.binaryeye.content.shareText
|
|
||||||
import de.markusfisch.android.binaryeye.graphics.COLOR_BLACK
|
|
||||||
import de.markusfisch.android.binaryeye.graphics.COLOR_WHITE
|
|
||||||
import de.markusfisch.android.binaryeye.io.addSuffixIfNotGiven
|
|
||||||
import de.markusfisch.android.binaryeye.io.toSaveResult
|
|
||||||
import de.markusfisch.android.binaryeye.io.writeExternalFile
|
|
||||||
import de.markusfisch.android.binaryeye.view.doOnApplyWindowInsets
|
|
||||||
import de.markusfisch.android.binaryeye.view.setPaddingFromWindowInsets
|
|
||||||
import de.markusfisch.android.binaryeye.widget.ConfinedScalingImageView
|
|
||||||
import de.markusfisch.android.binaryeye.widget.toast
|
|
||||||
import de.markusfisch.android.zxingcpp.ZxingCpp
|
|
||||||
import de.markusfisch.android.zxingcpp.ZxingCpp.Format
|
|
||||||
import kotlinx.coroutines.*
|
|
||||||
import java.io.File
|
|
||||||
import java.io.FileOutputStream
|
|
||||||
import java.io.IOException
|
|
||||||
import java.io.OutputStream
|
|
||||||
import java.util.*
|
|
||||||
import kotlin.math.min
|
|
||||||
|
|
||||||
class BarcodeFragment : Fragment() {
|
|
||||||
private enum class FileType {
|
|
||||||
PNG, SVG, TXT
|
|
||||||
}
|
|
||||||
|
|
||||||
private val parentJob = Job()
|
|
||||||
private val scope = CoroutineScope(Dispatchers.IO + parentJob)
|
|
||||||
|
|
||||||
private lateinit var barcode: Barcode
|
|
||||||
|
|
||||||
override fun onCreate(state: Bundle?) {
|
|
||||||
super.onCreate(state)
|
|
||||||
setHasOptionsMenu(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateView(
|
|
||||||
inflater: LayoutInflater,
|
|
||||||
container: ViewGroup?,
|
|
||||||
state: Bundle?
|
|
||||||
): View? {
|
|
||||||
val ac = activity ?: return null
|
|
||||||
ac.setTitle(R.string.view_barcode)
|
|
||||||
|
|
||||||
val view = inflater.inflate(
|
|
||||||
R.layout.fragment_barcode,
|
|
||||||
container,
|
|
||||||
false
|
|
||||||
)
|
|
||||||
|
|
||||||
val bitmap: Bitmap
|
|
||||||
try {
|
|
||||||
barcode = arguments?.toBarcode() ?: throw IllegalArgumentException(
|
|
||||||
"Illegal arguments"
|
|
||||||
)
|
|
||||||
// Catch exceptions from encoding.
|
|
||||||
bitmap = barcode.bitmap()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
var message = e.message
|
|
||||||
if (message == null || message.isEmpty()) {
|
|
||||||
message = getString(R.string.error_encoding_barcode)
|
|
||||||
}
|
|
||||||
message?.let {
|
|
||||||
ac.toast(message)
|
|
||||||
}
|
|
||||||
fragmentManager.popBackStack()
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
val imageView = view.findViewById<ConfinedScalingImageView>(
|
|
||||||
R.id.barcode
|
|
||||||
)
|
|
||||||
imageView.setImageBitmap(bitmap)
|
|
||||||
imageView.post {
|
|
||||||
// Make sure to invoke this after ScalingImageView.onLayout().
|
|
||||||
imageView.minWidth = min(
|
|
||||||
imageView.minWidth / 2f,
|
|
||||||
barcode.size.toFloat()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
view.findViewById<View>(R.id.share).setOnClickListener {
|
|
||||||
it.context?.apply {
|
|
||||||
pickFileType(R.string.share_as) { fileType ->
|
|
||||||
shareAs(fileType)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
(view.findViewById(R.id.inset_layout) as View).setPaddingFromWindowInsets()
|
|
||||||
imageView.doOnApplyWindowInsets { v, insets ->
|
|
||||||
(v as ConfinedScalingImageView).insets.set(insets)
|
|
||||||
}
|
|
||||||
|
|
||||||
return view
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun Bundle.toBarcode() = Barcode(
|
|
||||||
getString(CONTENT) ?: throw IllegalArgumentException(
|
|
||||||
"content cannot be null"
|
|
||||||
),
|
|
||||||
Format.valueOf(
|
|
||||||
getString(FORMAT) ?: throw IllegalArgumentException(
|
|
||||||
"format cannot be null"
|
|
||||||
)
|
|
||||||
),
|
|
||||||
getInt(SIZE),
|
|
||||||
getInt(EC_LEVEL),
|
|
||||||
Colors.values()[getInt(COLORS)]
|
|
||||||
)
|
|
||||||
|
|
||||||
override fun onDestroyView() {
|
|
||||||
super.onDestroyView()
|
|
||||||
parentJob.cancel()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
|
||||||
inflater.inflate(R.menu.fragment_barcode, menu)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
|
||||||
return when (item.itemId) {
|
|
||||||
R.id.copy_to_clipboard -> {
|
|
||||||
context.apply {
|
|
||||||
copyToClipboard(barcode.text())
|
|
||||||
toast(R.string.copied_to_clipboard)
|
|
||||||
}
|
|
||||||
true
|
|
||||||
}
|
|
||||||
R.id.export_to_file -> {
|
|
||||||
context.pickFileType(R.string.export_as) {
|
|
||||||
askForFileNameAndSave(it)
|
|
||||||
}
|
|
||||||
true
|
|
||||||
}
|
|
||||||
else -> super.onOptionsItemSelected(item)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun Context.pickFileType(
|
|
||||||
title: Int,
|
|
||||||
action: (FileType) -> Unit
|
|
||||||
) {
|
|
||||||
val fileTypes = FileType.values()
|
|
||||||
AlertDialog.Builder(this)
|
|
||||||
.setTitle(title)
|
|
||||||
.setItems(fileTypes.map { it.name }.toTypedArray()) { _, which ->
|
|
||||||
action(fileTypes[which])
|
|
||||||
}
|
|
||||||
.show()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Dialogs do not have a parent view.
|
|
||||||
@SuppressLint("InflateParams")
|
|
||||||
private fun askForFileNameAndSave(fileType: FileType) {
|
|
||||||
val ac = activity ?: return
|
|
||||||
// Write permission is only required before Android Q.
|
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q &&
|
|
||||||
!ac.hasWritePermission { askForFileNameAndSave(fileType) }
|
|
||||||
) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
val view = ac.layoutInflater.inflate(R.layout.dialog_save_file, null)
|
|
||||||
val editText = view.findViewById<EditText>(R.id.file_name)
|
|
||||||
editText.setText(
|
|
||||||
encodeFileName("${barcode.format}_${barcode.content}")
|
|
||||||
)
|
|
||||||
AlertDialog.Builder(ac)
|
|
||||||
.setView(view)
|
|
||||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
|
||||||
val fileName = editText.text.toString()
|
|
||||||
when (fileType) {
|
|
||||||
FileType.PNG -> saveAs(
|
|
||||||
addSuffixIfNotGiven(fileName, ".png"),
|
|
||||||
MIME_PNG
|
|
||||||
) {
|
|
||||||
barcode.bitmap().saveAsPng(it)
|
|
||||||
}
|
|
||||||
FileType.SVG -> saveAs(
|
|
||||||
addSuffixIfNotGiven(fileName, ".svg"),
|
|
||||||
MIME_SVG
|
|
||||||
) { outputStream ->
|
|
||||||
outputStream.write(barcode.svg().toByteArray())
|
|
||||||
}
|
|
||||||
FileType.TXT -> saveAs(
|
|
||||||
addSuffixIfNotGiven(fileName, ".txt"),
|
|
||||||
MIME_TXT
|
|
||||||
) { outputStream ->
|
|
||||||
outputStream.write(barcode.text().toByteArray())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
|
||||||
}
|
|
||||||
.show()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun saveAs(
|
|
||||||
fileName: String,
|
|
||||||
mimeType: String,
|
|
||||||
write: (outputStream: OutputStream) -> Unit
|
|
||||||
) {
|
|
||||||
val ac = activity ?: return
|
|
||||||
scope.launch(Dispatchers.IO) {
|
|
||||||
val message = ac.writeExternalFile(fileName, mimeType, write).toSaveResult()
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
ac.toast(message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun Context.shareAs(fileType: FileType) {
|
|
||||||
when (fileType) {
|
|
||||||
FileType.PNG -> share(barcode.bitmap())
|
|
||||||
FileType.SVG -> shareText(barcode.svg(), MIME_SVG)
|
|
||||||
FileType.TXT -> shareText(barcode.text())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun Context.share(bitmap: Bitmap) {
|
|
||||||
scope.launch(Dispatchers.IO) {
|
|
||||||
val file = File(
|
|
||||||
externalCacheDir,
|
|
||||||
"shared_barcode.png"
|
|
||||||
)
|
|
||||||
val success = try {
|
|
||||||
FileOutputStream(file).use {
|
|
||||||
bitmap.saveAsPng(it)
|
|
||||||
}
|
|
||||||
true
|
|
||||||
} catch (e: IOException) {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
if (success) {
|
|
||||||
shareFile(file, "image/png")
|
|
||||||
} else {
|
|
||||||
toast(R.string.error_saving_file)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private const val CONTENT = "content"
|
|
||||||
private const val FORMAT = "format"
|
|
||||||
private const val SIZE = "size"
|
|
||||||
private const val EC_LEVEL = "ec_level"
|
|
||||||
private const val COLORS = "colors"
|
|
||||||
private const val MIME_PNG = "image/png"
|
|
||||||
private const val MIME_SVG = "image/svg+xmg"
|
|
||||||
private const val MIME_TXT = "text/plain"
|
|
||||||
|
|
||||||
fun newInstance(
|
|
||||||
content: String,
|
|
||||||
format: Format,
|
|
||||||
size: Int,
|
|
||||||
ecLevel: Int = -1,
|
|
||||||
colors: Int = 0
|
|
||||||
): Fragment {
|
|
||||||
val args = Bundle()
|
|
||||||
args.putString(CONTENT, content)
|
|
||||||
args.putString(FORMAT, format.name)
|
|
||||||
args.putInt(SIZE, size)
|
|
||||||
args.putInt(EC_LEVEL, ecLevel)
|
|
||||||
args.putInt(COLORS, colors)
|
|
||||||
val fragment = BarcodeFragment()
|
|
||||||
fragment.arguments = args
|
|
||||||
return fragment
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private data class Barcode(
|
|
||||||
val content: String,
|
|
||||||
val format: Format,
|
|
||||||
val size: Int,
|
|
||||||
val ecLevel: Int,
|
|
||||||
val colors: Colors
|
|
||||||
) {
|
|
||||||
private var _bitmap: Bitmap? = null
|
|
||||||
fun bitmap(): Bitmap {
|
|
||||||
val b = _bitmap ?: ZxingCpp.encodeAsBitmap(
|
|
||||||
content, format, size, size, -1, ecLevel,
|
|
||||||
setColor = colors.foregroundColor(),
|
|
||||||
unsetColor = colors.backgroundColor()
|
|
||||||
)
|
|
||||||
_bitmap = b
|
|
||||||
return b
|
|
||||||
}
|
|
||||||
|
|
||||||
private var _svg: String? = null
|
|
||||||
fun svg(): String {
|
|
||||||
val s = _svg ?: ZxingCpp.encodeAsSvg(
|
|
||||||
content, format, -1, ecLevel
|
|
||||||
)
|
|
||||||
_svg = s
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
|
|
||||||
private var _text: String? = null
|
|
||||||
fun text(): String {
|
|
||||||
val t = _text ?: ZxingCpp.encodeAsText(
|
|
||||||
content, format, -1, ecLevel,
|
|
||||||
inverted = colors == Colors.BLACK_ON_WHITE
|
|
||||||
)
|
|
||||||
_text = t
|
|
||||||
return t
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private enum class Colors {
|
|
||||||
BLACK_ON_WHITE,
|
|
||||||
WHITE_ON_BLACK,
|
|
||||||
BLACK_ON_TRANSPARENT,
|
|
||||||
WHITE_ON_TRANSPARENT;
|
|
||||||
|
|
||||||
fun foregroundColor(): Int = when (this) {
|
|
||||||
BLACK_ON_WHITE,
|
|
||||||
BLACK_ON_TRANSPARENT -> COLOR_BLACK
|
|
||||||
WHITE_ON_BLACK,
|
|
||||||
WHITE_ON_TRANSPARENT -> COLOR_WHITE
|
|
||||||
}
|
|
||||||
|
|
||||||
fun backgroundColor(): Int = when (this) {
|
|
||||||
BLACK_ON_WHITE -> COLOR_WHITE
|
|
||||||
WHITE_ON_BLACK -> COLOR_BLACK
|
|
||||||
BLACK_ON_TRANSPARENT,
|
|
||||||
WHITE_ON_TRANSPARENT -> 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun Bitmap.saveAsPng(outputStream: OutputStream, quality: Int = 90) =
|
|
||||||
compress(Bitmap.CompressFormat.PNG, quality, outputStream)
|
|
||||||
|
|
||||||
private val fileNameCharacters = "[^A-Za-z0-9]".toRegex()
|
|
||||||
private fun encodeFileName(name: String): String =
|
|
||||||
fileNameCharacters.replace(name, "_").take(16).trim('_').lowercase(Locale.getDefault())
|
|
||||||
@@ -1,584 +0,0 @@
|
|||||||
package de.markusfisch.android.binaryeye.fragment
|
|
||||||
|
|
||||||
import android.os.Build
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.support.design.widget.FloatingActionButton
|
|
||||||
import android.support.v4.app.Fragment
|
|
||||||
import android.text.Editable
|
|
||||||
import android.text.Html
|
|
||||||
import android.text.TextWatcher
|
|
||||||
import android.text.method.LinkMovementMethod
|
|
||||||
import android.view.*
|
|
||||||
import android.widget.*
|
|
||||||
import de.markusfisch.android.binaryeye.R
|
|
||||||
import de.markusfisch.android.binaryeye.actions.ActionRegistry
|
|
||||||
import de.markusfisch.android.binaryeye.actions.IAction
|
|
||||||
import de.markusfisch.android.binaryeye.actions.wifi.WifiAction
|
|
||||||
import de.markusfisch.android.binaryeye.actions.wifi.WifiConnector
|
|
||||||
import de.markusfisch.android.binaryeye.activity.MainActivity
|
|
||||||
import de.markusfisch.android.binaryeye.adapter.prettifyFormatName
|
|
||||||
import de.markusfisch.android.binaryeye.app.*
|
|
||||||
import de.markusfisch.android.binaryeye.content.copyToClipboard
|
|
||||||
import de.markusfisch.android.binaryeye.content.shareText
|
|
||||||
import de.markusfisch.android.binaryeye.content.toHexString
|
|
||||||
import de.markusfisch.android.binaryeye.database.Recreation
|
|
||||||
import de.markusfisch.android.binaryeye.database.Scan
|
|
||||||
import de.markusfisch.android.binaryeye.database.toRecreation
|
|
||||||
import de.markusfisch.android.binaryeye.io.askForFileName
|
|
||||||
import de.markusfisch.android.binaryeye.io.toSaveResult
|
|
||||||
import de.markusfisch.android.binaryeye.io.writeExternalFile
|
|
||||||
import de.markusfisch.android.binaryeye.view.setPaddingFromWindowInsets
|
|
||||||
import de.markusfisch.android.binaryeye.widget.toast
|
|
||||||
import kotlinx.coroutines.*
|
|
||||||
import kotlin.math.roundToInt
|
|
||||||
|
|
||||||
class DecodeFragment : Fragment() {
|
|
||||||
private lateinit var contentView: EditText
|
|
||||||
private lateinit var formatView: TextView
|
|
||||||
private lateinit var dataView: TableLayout
|
|
||||||
private lateinit var metaView: TableLayout
|
|
||||||
private lateinit var hexView: TextView
|
|
||||||
private lateinit var stampView: TextView
|
|
||||||
private lateinit var recreationView: ImageView
|
|
||||||
private lateinit var fab: FloatingActionButton
|
|
||||||
private lateinit var format: String
|
|
||||||
|
|
||||||
private val parentJob = Job()
|
|
||||||
private val scope: CoroutineScope = CoroutineScope(Dispatchers.Main + parentJob)
|
|
||||||
private val dp: Float
|
|
||||||
get() = resources.displayMetrics.density
|
|
||||||
private val content: String
|
|
||||||
get() = contentView.text.toString()
|
|
||||||
|
|
||||||
private var closeAutomatically = false
|
|
||||||
private var action = ActionRegistry.DEFAULT_ACTION
|
|
||||||
private var isBinary = false
|
|
||||||
private var originalBytes: ByteArray = ByteArray(0)
|
|
||||||
private var id = 0L
|
|
||||||
private var recreation: Recreation? = null
|
|
||||||
|
|
||||||
override fun onCreate(state: Bundle?) {
|
|
||||||
super.onCreate(state)
|
|
||||||
setHasOptionsMenu(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateView(
|
|
||||||
inflater: LayoutInflater,
|
|
||||||
container: ViewGroup?,
|
|
||||||
state: Bundle?
|
|
||||||
): View {
|
|
||||||
activity?.setTitle(R.string.content)
|
|
||||||
|
|
||||||
val view = inflater.inflate(
|
|
||||||
R.layout.fragment_decode,
|
|
||||||
container,
|
|
||||||
false
|
|
||||||
)
|
|
||||||
|
|
||||||
closeAutomatically = prefs.closeAutomatically &&
|
|
||||||
activity?.intent?.hasExtra(MainActivity.DECODED) == true
|
|
||||||
|
|
||||||
val scan = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
|
||||||
arguments?.getParcelable(SCAN, Scan::class.java)
|
|
||||||
} else {
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
arguments?.getParcelable(SCAN) as Scan?
|
|
||||||
} ?: throw IllegalArgumentException("DecodeFragment needs a Scan")
|
|
||||||
|
|
||||||
val originalContent = scan.content
|
|
||||||
isBinary = scan.raw != null
|
|
||||||
originalBytes = scan.raw ?: originalContent.toByteArray()
|
|
||||||
format = scan.format
|
|
||||||
id = scan.id
|
|
||||||
|
|
||||||
contentView = view.findViewById(R.id.content)
|
|
||||||
formatView = view.findViewById(R.id.format)
|
|
||||||
dataView = view.findViewById(R.id.data)
|
|
||||||
metaView = view.findViewById(R.id.meta)
|
|
||||||
hexView = view.findViewById(R.id.hex)
|
|
||||||
stampView = view.findViewById(R.id.stamp)
|
|
||||||
recreationView = view.findViewById(R.id.recreation)
|
|
||||||
fab = view.findViewById(R.id.open)
|
|
||||||
|
|
||||||
initContentAndFab(originalContent)
|
|
||||||
|
|
||||||
if (prefs.showMetaData) {
|
|
||||||
metaView.fillMetaView(scan)
|
|
||||||
} else {
|
|
||||||
metaView.visibility = View.GONE
|
|
||||||
}
|
|
||||||
if (prefs.showRecreation) {
|
|
||||||
recreation = scan.toRecreation(
|
|
||||||
(200f * dp).roundToInt()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
updateViewsAndFab(originalContent, originalBytes)
|
|
||||||
|
|
||||||
(view.findViewById(R.id.inset_layout) as View).setPaddingFromWindowInsets()
|
|
||||||
(view.findViewById(R.id.scroll_view) as View).setPaddingFromWindowInsets()
|
|
||||||
|
|
||||||
return view
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroy() {
|
|
||||||
super.onDestroy()
|
|
||||||
parentJob.cancel()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun initContentAndFab(originalContent: String) {
|
|
||||||
if (isBinary) {
|
|
||||||
contentView.setText(String(originalBytes).foldNonAlNum())
|
|
||||||
contentView.isEnabled = false
|
|
||||||
fab.setImageResource(R.drawable.ic_action_save)
|
|
||||||
fab.setOnClickListener {
|
|
||||||
askForFileNameAndSave(originalBytes)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
contentView.setText(originalContent)
|
|
||||||
contentView.addTextChangedListener(object : TextWatcher {
|
|
||||||
override fun afterTextChanged(s: Editable?) {
|
|
||||||
updateViewsAndFab(s?.toString() ?: "")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun beforeTextChanged(
|
|
||||||
s: CharSequence?,
|
|
||||||
start: Int,
|
|
||||||
count: Int,
|
|
||||||
after: Int
|
|
||||||
) = Unit
|
|
||||||
|
|
||||||
override fun onTextChanged(
|
|
||||||
s: CharSequence?,
|
|
||||||
start: Int,
|
|
||||||
before: Int,
|
|
||||||
count: Int
|
|
||||||
) = Unit
|
|
||||||
})
|
|
||||||
fab.setOnClickListener {
|
|
||||||
executeAction(content)
|
|
||||||
}
|
|
||||||
if (prefs.openImmediately) {
|
|
||||||
executeAction(content)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun updateViewsAndFab(text: String, bytes: ByteArray? = null) {
|
|
||||||
val b = bytes ?: text.toByteArray()
|
|
||||||
resolveActionAndUpdateFab(b)
|
|
||||||
updateViews(text, b)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun resolveActionAndUpdateFab(bytes: ByteArray) {
|
|
||||||
val prevAction = action
|
|
||||||
if (!prevAction.canExecuteOn(bytes)) {
|
|
||||||
action = ActionRegistry.getAction(bytes)
|
|
||||||
}
|
|
||||||
if (prevAction !== action) {
|
|
||||||
updateFab(action)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun updateFab(action: IAction) {
|
|
||||||
fab.setImageResource(action.iconResId)
|
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
|
||||||
fab.setOnLongClickListener { v ->
|
|
||||||
v.context.toast(action.titleResId)
|
|
||||||
true
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
fab.tooltipText = getString(action.titleResId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun updateViews(text: String, bytes: ByteArray) {
|
|
||||||
formatView.text = resources.getQuantityString(
|
|
||||||
R.plurals.barcode_info,
|
|
||||||
bytes.size,
|
|
||||||
prettifyFormatName(format),
|
|
||||||
bytes.size
|
|
||||||
)
|
|
||||||
hexView.showIf(prefs.showHexDump) { v ->
|
|
||||||
v.text = hexDump(bytes)
|
|
||||||
}
|
|
||||||
recreationView.showIf(prefs.showRecreation) { v ->
|
|
||||||
val r = recreation ?: return@showIf
|
|
||||||
v.setImageBitmap(r.encode(text))
|
|
||||||
v.setOnClickListener {
|
|
||||||
fragmentManager?.addFragment(
|
|
||||||
BarcodeFragment.newInstance(
|
|
||||||
text,
|
|
||||||
r.format,
|
|
||||||
r.size,
|
|
||||||
r.ecLevel
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
dataView.fillDataView(text)
|
|
||||||
stampView.setTrackingLink(bytes, format)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun TableLayout.fillDataView(text: String) {
|
|
||||||
val items = LinkedHashMap<Int, String?>()
|
|
||||||
if (action is WifiAction) {
|
|
||||||
WifiConnector.parseMap(text)?.let { wifiData ->
|
|
||||||
items.putAll(
|
|
||||||
linkedMapOf(
|
|
||||||
R.string.entry_type to getString(R.string.wifi_network),
|
|
||||||
R.string.wifi_ssid to wifiData["S"],
|
|
||||||
R.string.wifi_password to wifiData["P"],
|
|
||||||
R.string.wifi_type to wifiData["T"],
|
|
||||||
R.string.wifi_hidden to wifiData["H"],
|
|
||||||
R.string.wifi_eap to wifiData["E"],
|
|
||||||
R.string.wifi_identity to wifiData["I"],
|
|
||||||
R.string.wifi_anonymous_identity to wifiData["A"],
|
|
||||||
R.string.wifi_phase2 to wifiData["PH2"]
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fill(items)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun TableLayout.fillMetaView(scan: Scan) {
|
|
||||||
val items = linkedMapOf(
|
|
||||||
R.string.error_correction_level to scan.errorCorrectionLevel,
|
|
||||||
R.string.sequence_size to scan.sequenceSize.positiveToString(),
|
|
||||||
R.string.sequence_index to scan.sequenceIndex.positiveToString(),
|
|
||||||
R.string.sequence_id to scan.sequenceId,
|
|
||||||
R.string.gtin_country to scan.country,
|
|
||||||
R.string.gtin_add_on to scan.addOn,
|
|
||||||
R.string.gtin_price to scan.price,
|
|
||||||
R.string.gtin_issue_number to scan.issueNumber,
|
|
||||||
)
|
|
||||||
if (!scan.version.isNullOrBlank()) {
|
|
||||||
val versionString = if (scan.format == "QR_CODE") {
|
|
||||||
getString(
|
|
||||||
R.string.qr_version_and_modules,
|
|
||||||
scan.version,
|
|
||||||
(scan.version.toIntOrNull() ?: 0) * 4 + 17
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
scan.version
|
|
||||||
}
|
|
||||||
items.putAll(
|
|
||||||
linkedMapOf(
|
|
||||||
R.string.barcode_version_number to versionString
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
fill(items)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun TableLayout.fill(
|
|
||||||
items: LinkedHashMap<Int, String?>
|
|
||||||
) {
|
|
||||||
removeAllViews()
|
|
||||||
visibility = if (items.isEmpty()) View.GONE else {
|
|
||||||
val ctx = context
|
|
||||||
val spaceBetween = (16f * dp).roundToInt()
|
|
||||||
items.forEach { item ->
|
|
||||||
val text = item.value
|
|
||||||
if (!text.isNullOrBlank()) {
|
|
||||||
val tr = TableRow(ctx)
|
|
||||||
val keyView = TextView(ctx)
|
|
||||||
keyView.setText(item.key)
|
|
||||||
val valueView = TextView(ctx)
|
|
||||||
valueView.setPadding(spaceBetween, 0, 0, 0)
|
|
||||||
valueView.text = text
|
|
||||||
tr.addView(keyView)
|
|
||||||
tr.addView(valueView)
|
|
||||||
addView(tr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
View.VISIBLE
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
|
||||||
inflater.inflate(R.menu.fragment_decode, menu)
|
|
||||||
if (isBinary) {
|
|
||||||
menu.findItem(R.id.copy_to_clipboard).isVisible = false
|
|
||||||
menu.findItem(R.id.create).isVisible = false
|
|
||||||
}
|
|
||||||
if (id > 0L) {
|
|
||||||
menu.findItem(R.id.remove).isVisible = true
|
|
||||||
}
|
|
||||||
if (action is WifiAction) {
|
|
||||||
menu.findItem(R.id.copy_password).isVisible = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
|
||||||
return when (item.itemId) {
|
|
||||||
R.id.copy_password -> {
|
|
||||||
copyPasswordToClipboard()
|
|
||||||
maybeBackOrFinish()
|
|
||||||
true
|
|
||||||
}
|
|
||||||
R.id.copy_to_clipboard -> {
|
|
||||||
copyToClipboard(textOrHex())
|
|
||||||
maybeBackOrFinish()
|
|
||||||
true
|
|
||||||
}
|
|
||||||
R.id.share -> {
|
|
||||||
context?.apply {
|
|
||||||
shareText(textOrHex())
|
|
||||||
maybeBackOrFinish()
|
|
||||||
}
|
|
||||||
true
|
|
||||||
}
|
|
||||||
R.id.create -> {
|
|
||||||
fragmentManager?.addFragment(
|
|
||||||
EncodeFragment.newInstance(content, format)
|
|
||||||
)
|
|
||||||
true
|
|
||||||
}
|
|
||||||
R.id.remove -> {
|
|
||||||
db.removeScan(id)
|
|
||||||
backOrFinish()
|
|
||||||
true
|
|
||||||
}
|
|
||||||
else -> super.onOptionsItemSelected(item)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun textOrHex() = if (isBinary) {
|
|
||||||
originalBytes.toHexString()
|
|
||||||
} else {
|
|
||||||
content
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun copyPasswordToClipboard() {
|
|
||||||
val ac = action
|
|
||||||
if (ac is WifiAction) {
|
|
||||||
ac.password?.let { password ->
|
|
||||||
activity?.apply {
|
|
||||||
copyToClipboard(password, isSensitive = true)
|
|
||||||
toast(R.string.copied_password_to_clipboard)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun copyToClipboard(text: String, isSensitive: Boolean = false) {
|
|
||||||
activity?.apply {
|
|
||||||
copyToClipboard(text, isSensitive)
|
|
||||||
toast(R.string.copied_to_clipboard)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun maybeBackOrFinish() {
|
|
||||||
if (closeAutomatically) {
|
|
||||||
backOrFinish()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun backOrFinish() {
|
|
||||||
val fm = fragmentManager
|
|
||||||
if (fm != null && fm.backStackEntryCount > 0) {
|
|
||||||
fm.popBackStack()
|
|
||||||
} else {
|
|
||||||
activity?.finish()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun executeAction(content: String) {
|
|
||||||
val ac = activity ?: return
|
|
||||||
if (content.isNotEmpty()) {
|
|
||||||
if (action is WifiAction &&
|
|
||||||
Build.VERSION.SDK_INT < Build.VERSION_CODES.Q &&
|
|
||||||
!ac.hasLocationPermission { executeAction(content) }
|
|
||||||
) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
scope.launch {
|
|
||||||
action.execute(ac, content.toByteArray())
|
|
||||||
maybeBackOrFinish()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun askForFileNameAndSave(raw: ByteArray) {
|
|
||||||
val ac = activity ?: return
|
|
||||||
// Write permission is only required before Android Q.
|
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q &&
|
|
||||||
!ac.hasWritePermission { askForFileNameAndSave(raw) }
|
|
||||||
) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
scope.launch(Dispatchers.Main) {
|
|
||||||
val name = ac.askForFileName() ?: return@launch
|
|
||||||
withContext(Dispatchers.IO) {
|
|
||||||
val message = ac.writeExternalFile(
|
|
||||||
name,
|
|
||||||
"application/octet-stream"
|
|
||||||
) {
|
|
||||||
it.write(raw)
|
|
||||||
}.toSaveResult()
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
ac.toast(message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private const val SCAN = "scan"
|
|
||||||
|
|
||||||
fun newInstance(scan: Scan): Fragment {
|
|
||||||
val args = Bundle()
|
|
||||||
args.putParcelable(SCAN, scan)
|
|
||||||
val fragment = DecodeFragment()
|
|
||||||
fragment.arguments = args
|
|
||||||
return fragment
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private inline fun <T : View> T.showIf(
|
|
||||||
visible: Boolean,
|
|
||||||
block: (T) -> Unit
|
|
||||||
) {
|
|
||||||
visibility = if (visible) {
|
|
||||||
block.invoke(this)
|
|
||||||
View.VISIBLE
|
|
||||||
} else {
|
|
||||||
View.GONE
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val nonAlNum = "[^a-zA-Z0-9]".toRegex()
|
|
||||||
private val multipleDots = "[…]+".toRegex()
|
|
||||||
private fun String.foldNonAlNum() = replace(nonAlNum, "…")
|
|
||||||
.replace(multipleDots, "…")
|
|
||||||
|
|
||||||
private fun hexDump(bytes: ByteArray, charsPerLine: Int = 33): String {
|
|
||||||
if (charsPerLine < 4 || bytes.isEmpty()) {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
val dump = StringBuilder()
|
|
||||||
val hex = StringBuilder()
|
|
||||||
val ascii = StringBuilder()
|
|
||||||
val itemsPerLine = (charsPerLine - 1) / 4
|
|
||||||
val len = bytes.size
|
|
||||||
var i = 0
|
|
||||||
while (true) {
|
|
||||||
val ord = bytes[i]
|
|
||||||
hex.append(String.format("%02X ", ord))
|
|
||||||
ascii.append(if (ord > 31) ord.toInt().toChar() else " ")
|
|
||||||
++i
|
|
||||||
val posInLine = i % itemsPerLine
|
|
||||||
val atEnd = i >= len
|
|
||||||
var atLineEnd = posInLine == 0
|
|
||||||
if (atEnd && !atLineEnd) {
|
|
||||||
for (j in posInLine until itemsPerLine) {
|
|
||||||
hex.append(" ")
|
|
||||||
}
|
|
||||||
atLineEnd = true
|
|
||||||
}
|
|
||||||
if (atLineEnd) {
|
|
||||||
dump.append(hex.toString())
|
|
||||||
dump.append(" ")
|
|
||||||
dump.append(ascii.toString())
|
|
||||||
dump.append("\n")
|
|
||||||
hex.setLength(0)
|
|
||||||
ascii.setLength(0)
|
|
||||||
}
|
|
||||||
if (atEnd) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return dump.toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun Int.positiveToString() = if (this > -1) this.toString() else ""
|
|
||||||
|
|
||||||
private fun TextView.setTrackingLink(bytes: ByteArray, format: String) {
|
|
||||||
val trackingLink = generateDpTrackingLink(bytes, format)
|
|
||||||
if (trackingLink != null) {
|
|
||||||
text = trackingLink.fromHtml()
|
|
||||||
isClickable = true
|
|
||||||
movementMethod = LinkMovementMethod.getInstance()
|
|
||||||
} else {
|
|
||||||
visibility = View.GONE
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun generateDpTrackingLink(bytes: ByteArray, format: String): String? {
|
|
||||||
// Check for Deutsche Post Matrixcode stamp.
|
|
||||||
var isStamp = false
|
|
||||||
var rawData = bytes
|
|
||||||
if (format == "DATA_MATRIX" &&
|
|
||||||
bytes.toString(Charsets.ISO_8859_1).startsWith("DEA5")
|
|
||||||
) {
|
|
||||||
if (bytes.size == 47) {
|
|
||||||
isStamp = true
|
|
||||||
} else if (bytes.size > 47) {
|
|
||||||
// Transform back to original data.
|
|
||||||
rawData = bytes.toString(Charsets.UTF_8).toByteArray(
|
|
||||||
Charsets.ISO_8859_1
|
|
||||||
)
|
|
||||||
if (rawData.size == 47) {
|
|
||||||
isStamp = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isStamp) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
val hex = StringBuilder()
|
|
||||||
hex.append(String.format("%02X", rawData[9]))
|
|
||||||
hex.append(String.format("%02X", rawData[10]))
|
|
||||||
hex.append(String.format("%02X", rawData[11]))
|
|
||||||
hex.append(String.format("%02X", rawData[12]))
|
|
||||||
hex.append(String.format("%02X", rawData[13]))
|
|
||||||
hex.append(String.format("%X", (rawData[4].toInt() and 0x0f).toByte()))
|
|
||||||
hex.append(String.format("%02X", rawData[5]))
|
|
||||||
hex.append(String.format("%02X", rawData[6]))
|
|
||||||
hex.append(String.format("%02X", rawData[7]))
|
|
||||||
hex.append(String.format("%02X", rawData[8]))
|
|
||||||
val hexString = hex.toString()
|
|
||||||
val trackingNumber = hexString + String.format(
|
|
||||||
"%X",
|
|
||||||
crc4(hexString.toByteArray(Charsets.ISO_8859_1))
|
|
||||||
)
|
|
||||||
return "<a href=\"https://www.deutschepost.de/de/s/sendungsverfolgung/verfolgen.html?piececode=$trackingNumber\">Deutsche Post: $trackingNumber</a>"
|
|
||||||
}
|
|
||||||
|
|
||||||
// CRC-4 with polynomial x^4 + x + 1.
|
|
||||||
private fun crc4(input: ByteArray): Int {
|
|
||||||
var crc = 0
|
|
||||||
var i = 0
|
|
||||||
while (i < input.size) {
|
|
||||||
val c = input[i].toInt()
|
|
||||||
var j = 0x80
|
|
||||||
while (j != 0) {
|
|
||||||
var bit = crc and 0x8
|
|
||||||
crc = crc shl 1
|
|
||||||
if (c and j != 0) {
|
|
||||||
bit = bit xor 0x8
|
|
||||||
}
|
|
||||||
if (bit != 0) {
|
|
||||||
crc = crc xor 0x3
|
|
||||||
}
|
|
||||||
j = j ushr 1
|
|
||||||
}
|
|
||||||
++i
|
|
||||||
}
|
|
||||||
crc = crc and 0xF
|
|
||||||
return crc
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun String.fromHtml() = if (
|
|
||||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.N
|
|
||||||
) {
|
|
||||||
Html.fromHtml(this, Html.FROM_HTML_MODE_LEGACY)
|
|
||||||
} else {
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
Html.fromHtml(this)
|
|
||||||
}
|
|
||||||
@@ -1,288 +0,0 @@
|
|||||||
package de.markusfisch.android.binaryeye.fragment
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.support.v4.app.Fragment
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.widget.*
|
|
||||||
import de.markusfisch.android.binaryeye.R
|
|
||||||
import de.markusfisch.android.binaryeye.adapter.prettifyFormatName
|
|
||||||
import de.markusfisch.android.binaryeye.app.addFragment
|
|
||||||
import de.markusfisch.android.binaryeye.app.prefs
|
|
||||||
import de.markusfisch.android.binaryeye.text.unescape
|
|
||||||
import de.markusfisch.android.binaryeye.view.hideSoftKeyboard
|
|
||||||
import de.markusfisch.android.binaryeye.view.setPaddingFromWindowInsets
|
|
||||||
import de.markusfisch.android.binaryeye.widget.toast
|
|
||||||
import de.markusfisch.android.zxingcpp.ZxingCpp.Format
|
|
||||||
|
|
||||||
class EncodeFragment : Fragment() {
|
|
||||||
private lateinit var formatView: Spinner
|
|
||||||
private lateinit var ecLabel: TextView
|
|
||||||
private lateinit var ecSpinner: Spinner
|
|
||||||
private lateinit var colorsLabel: TextView
|
|
||||||
private lateinit var colorsSpinner: Spinner
|
|
||||||
private lateinit var sizeView: TextView
|
|
||||||
private lateinit var sizeBarView: SeekBar
|
|
||||||
private lateinit var contentView: EditText
|
|
||||||
private lateinit var unescapeCheckBox: CheckBox
|
|
||||||
|
|
||||||
private val formats = arrayListOf(
|
|
||||||
Format.AZTEC,
|
|
||||||
Format.CODABAR,
|
|
||||||
Format.CODE_39,
|
|
||||||
Format.CODE_128,
|
|
||||||
Format.DATA_MATRIX,
|
|
||||||
Format.EAN_8,
|
|
||||||
Format.EAN_13,
|
|
||||||
Format.ITF,
|
|
||||||
Format.PDF_417,
|
|
||||||
Format.QR_CODE,
|
|
||||||
Format.UPC_A
|
|
||||||
)
|
|
||||||
|
|
||||||
override fun onCreateView(
|
|
||||||
inflater: LayoutInflater,
|
|
||||||
container: ViewGroup?,
|
|
||||||
state: Bundle?
|
|
||||||
): View? {
|
|
||||||
val ac = activity ?: return null
|
|
||||||
ac.setTitle(R.string.compose_barcode)
|
|
||||||
|
|
||||||
val view = inflater.inflate(
|
|
||||||
R.layout.fragment_encode,
|
|
||||||
container,
|
|
||||||
false
|
|
||||||
)
|
|
||||||
|
|
||||||
formatView = view.findViewById(R.id.format)
|
|
||||||
val formatAdapter = ArrayAdapter(
|
|
||||||
ac,
|
|
||||||
android.R.layout.simple_spinner_item,
|
|
||||||
formats.map { prettifyFormatName(it.name) }
|
|
||||||
)
|
|
||||||
formatAdapter.setDropDownViewResource(
|
|
||||||
android.R.layout.simple_spinner_dropdown_item
|
|
||||||
)
|
|
||||||
formatView.adapter = formatAdapter
|
|
||||||
formatView.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
|
|
||||||
override fun onItemSelected(
|
|
||||||
parentView: AdapterView<*>?,
|
|
||||||
selectedItemView: View?,
|
|
||||||
position: Int,
|
|
||||||
id: Long
|
|
||||||
) {
|
|
||||||
val format = formats[position]
|
|
||||||
val arrayId = when (format) {
|
|
||||||
Format.AZTEC -> R.array.aztec_error_correction_levels
|
|
||||||
Format.QR_CODE -> R.array.qr_error_correction_levels
|
|
||||||
Format.PDF_417 -> R.array.pdf417_error_correction_levels
|
|
||||||
else -> 0
|
|
||||||
}
|
|
||||||
if (arrayId > 0) {
|
|
||||||
ecSpinner.setEntries(arrayId)
|
|
||||||
val idx = format.unpackEcLevel(
|
|
||||||
prefs.indexOfLastSelectedEcLevel
|
|
||||||
)
|
|
||||||
if (idx < ecSpinner.adapter.count) {
|
|
||||||
ecSpinner.setSelection(idx)
|
|
||||||
}
|
|
||||||
true
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}.setVisibility(ecLabel, ecSpinner)
|
|
||||||
format.canBeInverted().setVisibility(
|
|
||||||
colorsLabel, colorsSpinner
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onNothingSelected(parentView: AdapterView<*>?) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
ecLabel = view.findViewById(R.id.error_correction_label)
|
|
||||||
ecSpinner = view.findViewById(R.id.error_correction_level)
|
|
||||||
ecSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
|
|
||||||
override fun onItemSelected(
|
|
||||||
parentView: AdapterView<*>?,
|
|
||||||
selectedItemView: View?,
|
|
||||||
position: Int,
|
|
||||||
id: Long
|
|
||||||
) {
|
|
||||||
val format = formats[formatView.selectedItemPosition]
|
|
||||||
prefs.indexOfLastSelectedEcLevel = format.packEcLevel(
|
|
||||||
prefs.indexOfLastSelectedEcLevel,
|
|
||||||
position
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onNothingSelected(parentView: AdapterView<*>?) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
colorsLabel = view.findViewById(R.id.colors_label)
|
|
||||||
colorsSpinner = view.findViewById(R.id.colors)
|
|
||||||
|
|
||||||
sizeView = view.findViewById(R.id.size_display)
|
|
||||||
sizeBarView = view.findViewById(R.id.size_bar)
|
|
||||||
initSizeBar()
|
|
||||||
|
|
||||||
contentView = view.findViewById(R.id.content)
|
|
||||||
unescapeCheckBox = view.findViewById(R.id.unescape)
|
|
||||||
unescapeCheckBox.isChecked = prefs.expandEscapeSequences
|
|
||||||
|
|
||||||
val args = arguments
|
|
||||||
args?.getString(CONTENT)?.let {
|
|
||||||
contentView.setText(it)
|
|
||||||
}
|
|
||||||
|
|
||||||
val barcodeFormat = args?.getString(FORMAT)
|
|
||||||
if (barcodeFormat != null) {
|
|
||||||
formatView.setSelection(
|
|
||||||
formats.indexOf(barcodeFormat.toFormat())
|
|
||||||
)
|
|
||||||
} else if (state == null) {
|
|
||||||
formatView.post {
|
|
||||||
formatView.setSelection(prefs.indexOfLastSelectedFormat)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
view.findViewById<View>(R.id.encode).setOnClickListener {
|
|
||||||
it.context.encode()
|
|
||||||
}
|
|
||||||
|
|
||||||
(view.findViewById(R.id.inset_layout) as View).setPaddingFromWindowInsets()
|
|
||||||
(view.findViewById(R.id.scroll_view) as View).setPaddingFromWindowInsets()
|
|
||||||
|
|
||||||
return view
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPause() {
|
|
||||||
super.onPause()
|
|
||||||
prefs.indexOfLastSelectedFormat = formatView.selectedItemPosition
|
|
||||||
prefs.expandEscapeSequences = unescapeCheckBox.isChecked
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun Context.encode() {
|
|
||||||
var content = contentView.text.toString()
|
|
||||||
if (unescapeCheckBox.isChecked) {
|
|
||||||
try {
|
|
||||||
content = content.unescape()
|
|
||||||
} catch (e: IllegalArgumentException) {
|
|
||||||
toast(e.message ?: "Invalid escape sequence")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (content.isEmpty()) {
|
|
||||||
toast(R.string.error_no_content)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
hideSoftKeyboard(contentView)
|
|
||||||
val format = formats[formatView.selectedItemPosition]
|
|
||||||
fragmentManager?.addFragment(
|
|
||||||
BarcodeFragment.newInstance(
|
|
||||||
content,
|
|
||||||
format,
|
|
||||||
getSize(sizeBarView.progress),
|
|
||||||
format.getErrorCorrectionLevel(ecSpinner.selectedItemPosition),
|
|
||||||
if (format.canBeInverted()) {
|
|
||||||
colorsSpinner.selectedItemPosition
|
|
||||||
} else 0
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun initSizeBar() {
|
|
||||||
updateSize(sizeBarView.progress)
|
|
||||||
sizeBarView.setOnSeekBarChangeListener(
|
|
||||||
object : SeekBar.OnSeekBarChangeListener {
|
|
||||||
override fun onProgressChanged(
|
|
||||||
seekBar: SeekBar,
|
|
||||||
progressValue: Int,
|
|
||||||
fromUser: Boolean
|
|
||||||
) {
|
|
||||||
updateSize(progressValue)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onStartTrackingTouch(seekBar: SeekBar) {}
|
|
||||||
|
|
||||||
override fun onStopTrackingTouch(seekBar: SeekBar) {}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun updateSize(power: Int) {
|
|
||||||
val size = getSize(power)
|
|
||||||
sizeView.text = getString(R.string.width_by_height, size, size)
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private const val CONTENT = "content"
|
|
||||||
private const val FORMAT = "format"
|
|
||||||
|
|
||||||
fun newInstance(
|
|
||||||
content: String? = null,
|
|
||||||
format: String? = null
|
|
||||||
): Fragment {
|
|
||||||
val args = Bundle()
|
|
||||||
content?.let { args.putString(CONTENT, content) }
|
|
||||||
format?.let { args.putString(FORMAT, it) }
|
|
||||||
val fragment = EncodeFragment()
|
|
||||||
fragment.arguments = args
|
|
||||||
return fragment
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun Boolean.setVisibility(vararg views: View) {
|
|
||||||
val visibility = if (this) View.VISIBLE else View.GONE
|
|
||||||
for (view in views) {
|
|
||||||
view.visibility = visibility
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun Format.packEcLevel(packed: Int, level: Int): Int {
|
|
||||||
val s = ecLevelShift()
|
|
||||||
return (level shl s) or (packed and (15 shl s).inv())
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun Format.unpackEcLevel(packed: Int) =
|
|
||||||
(packed shr ecLevelShift()) and 15
|
|
||||||
|
|
||||||
private fun Format.ecLevelShift() = when (this) {
|
|
||||||
Format.AZTEC -> 0
|
|
||||||
Format.QR_CODE -> 4
|
|
||||||
Format.PDF_417 -> 8
|
|
||||||
else -> throw IllegalArgumentException("$this does not have error levels")
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun Format.canBeInverted() = when (this) {
|
|
||||||
Format.AZTEC,
|
|
||||||
Format.DATA_MATRIX,
|
|
||||||
Format.QR_CODE -> true
|
|
||||||
else -> false
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun String.toFormat(default: Format = Format.QR_CODE): Format = try {
|
|
||||||
Format.valueOf(this)
|
|
||||||
} catch (_: IllegalArgumentException) {
|
|
||||||
default
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun Format.getErrorCorrectionLevel(position: Int) = when (this) {
|
|
||||||
Format.AZTEC -> position
|
|
||||||
Format.QR_CODE -> (position + 1) * 2
|
|
||||||
Format.PDF_417 -> position
|
|
||||||
else -> 0
|
|
||||||
}.coerceIn(0, 8)
|
|
||||||
|
|
||||||
private fun Spinner.setEntries(resId: Int) = ArrayAdapter.createFromResource(
|
|
||||||
this.context,
|
|
||||||
resId,
|
|
||||||
android.R.layout.simple_spinner_item
|
|
||||||
).also { aa ->
|
|
||||||
aa.setDropDownViewResource(
|
|
||||||
android.R.layout.simple_spinner_dropdown_item
|
|
||||||
)
|
|
||||||
adapter = aa
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getSize(power: Int) = 128 * (power + 1)
|
|
||||||
+211
-51
@@ -9,74 +9,140 @@ import android.content.SharedPreferences.OnSharedPreferenceChangeListener
|
|||||||
import android.net.wifi.WifiManager
|
import android.net.wifi.WifiManager
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.support.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import android.support.v14.preference.MultiSelectListPreference
|
import androidx.preference.ListPreference
|
||||||
import android.support.v7.preference.ListPreference
|
import androidx.preference.MultiSelectListPreference
|
||||||
import android.support.v7.preference.Preference
|
import androidx.preference.Preference
|
||||||
import android.support.v7.preference.PreferenceFragmentCompat
|
import androidx.preference.PreferenceFragmentCompat
|
||||||
import android.support.v7.preference.PreferenceGroup
|
import androidx.preference.PreferenceGroup
|
||||||
import de.markusfisch.android.binaryeye.R
|
import de.markusfisch.android.binaryeye.R
|
||||||
|
import de.markusfisch.android.binaryeye.activity.AutomatedActionsActivity
|
||||||
|
import de.markusfisch.android.binaryeye.activity.IgnoreCodesActivity
|
||||||
|
import de.markusfisch.android.binaryeye.activity.NetworkSuggestionsActivity
|
||||||
|
import de.markusfisch.android.binaryeye.activity.ProfilesActivity
|
||||||
import de.markusfisch.android.binaryeye.activity.SplashActivity
|
import de.markusfisch.android.binaryeye.activity.SplashActivity
|
||||||
import de.markusfisch.android.binaryeye.app.addFragment
|
|
||||||
import de.markusfisch.android.binaryeye.app.hasBluetoothPermission
|
import de.markusfisch.android.binaryeye.app.hasBluetoothPermission
|
||||||
import de.markusfisch.android.binaryeye.app.prefs
|
import de.markusfisch.android.binaryeye.app.prefs
|
||||||
|
import de.markusfisch.android.binaryeye.bluetooth.setBluetoothHosts
|
||||||
import de.markusfisch.android.binaryeye.media.beepConfirm
|
import de.markusfisch.android.binaryeye.media.beepConfirm
|
||||||
import de.markusfisch.android.binaryeye.preference.UrlPreference
|
import de.markusfisch.android.binaryeye.preference.UrlPreference
|
||||||
import de.markusfisch.android.binaryeye.view.setPaddingFromWindowInsets
|
import de.markusfisch.android.binaryeye.view.setPaddingFromWindowInsets
|
||||||
import de.markusfisch.android.binaryeye.view.systemBarRecyclerViewScrollListener
|
import de.markusfisch.android.binaryeye.view.systemBarRecyclerViewScrollListener
|
||||||
import de.markusfisch.android.binaryeye.widget.toast
|
import de.markusfisch.android.binaryeye.widget.toast
|
||||||
import de.markusfisch.android.binaryeye.bluetooth.setBluetoothHosts
|
|
||||||
|
|
||||||
class PreferencesFragment : PreferenceFragmentCompat() {
|
class PreferencesFragment : PreferenceFragmentCompat() {
|
||||||
|
private var lastProfile: String? = null
|
||||||
|
|
||||||
private val changeListener = object : OnSharedPreferenceChangeListener {
|
private val changeListener = object : OnSharedPreferenceChangeListener {
|
||||||
override fun onSharedPreferenceChanged(
|
override fun onSharedPreferenceChanged(
|
||||||
sharedPreferences: SharedPreferences,
|
sharedPreferences: SharedPreferences?,
|
||||||
key: String
|
key: String?
|
||||||
) {
|
) {
|
||||||
val preference = findPreference(key) ?: return
|
key ?: return
|
||||||
prefs.update()
|
val preference = findPreference<Preference>(key) ?: return
|
||||||
when (preference.key) {
|
if (preference.key != PROFILE) {
|
||||||
"custom_locale" -> activity?.restartApp()
|
prefs.update()
|
||||||
"beep_tone_name" -> {
|
|
||||||
beepConfirm()
|
|
||||||
setSummary(preference)
|
|
||||||
}
|
|
||||||
"send_scan_bluetooth" -> {
|
|
||||||
if (prefs.sendScanBluetooth &&
|
|
||||||
activity?.hasBluetoothPermission() == false
|
|
||||||
) {
|
|
||||||
prefs.sendScanBluetooth = false
|
|
||||||
}
|
|
||||||
setSummary(preference)
|
|
||||||
}
|
|
||||||
else -> setSummary(preference)
|
|
||||||
}
|
}
|
||||||
|
when (preference.key) {
|
||||||
|
"custom_locale" -> {
|
||||||
|
activity?.restartApp()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
"beep_tone_name" -> beepConfirm()
|
||||||
|
|
||||||
|
"send_scan_bluetooth" -> if (
|
||||||
|
prefs.sendScanBluetooth &&
|
||||||
|
activity?.hasBluetoothPermission() == false
|
||||||
|
) {
|
||||||
|
prefs.sendScanBluetooth = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setSummary(preference)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreatePreferences(state: Bundle?, rootKey: String?) {
|
override fun onCreatePreferences(state: Bundle?, rootKey: String?) {
|
||||||
addPreferencesFromResource(R.xml.preferences)
|
loadPreferences(rootKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadPreferences(rootKey: String? = null) {
|
||||||
|
preferenceScreen?.sharedPreferences
|
||||||
|
?.unregisterOnSharedPreferenceChangeListener(changeListener)
|
||||||
|
|
||||||
|
preferenceManager.apply {
|
||||||
|
sharedPreferencesName = prefs.profile ?: "${context.packageName}_preferences"
|
||||||
|
sharedPreferencesMode = Context.MODE_PRIVATE
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh the preferences.
|
||||||
|
preferenceScreen = null
|
||||||
|
setPreferencesFromResource(R.xml.preferences, rootKey)
|
||||||
|
preferenceScreen.sharedPreferences
|
||||||
|
?.registerOnSharedPreferenceChangeListener(changeListener)
|
||||||
|
setSummaries(preferenceScreen)
|
||||||
|
setIcons()
|
||||||
|
|
||||||
|
wireProfiles()
|
||||||
|
wireAutomatedActions()
|
||||||
|
wireIgnoreCodes()
|
||||||
setBluetoothResources()
|
setBluetoothResources()
|
||||||
wireClearNetworkPreferences()
|
wireClearNetworkPreferences()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun wireProfiles() {
|
||||||
|
findPreference<Preference>(PROFILE)?.apply {
|
||||||
|
updateProfileSummary(this)
|
||||||
|
onPreferenceClickListener = Preference.OnPreferenceClickListener {
|
||||||
|
startActivity(Intent(activity, ProfilesActivity::class.java))
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun wireAutomatedActions() {
|
||||||
|
findPreference<Preference>(AUTOMATED_ACTIONS)?.apply {
|
||||||
|
updateAutomatedActionsSummary(this)
|
||||||
|
onPreferenceClickListener = Preference.OnPreferenceClickListener {
|
||||||
|
startActivity(
|
||||||
|
Intent(activity, AutomatedActionsActivity::class.java)
|
||||||
|
)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun wireIgnoreCodes() {
|
||||||
|
findPreference<Preference>(IGNORE_CODES)?.apply {
|
||||||
|
updateIgnoreCodesSummary(this)
|
||||||
|
onPreferenceClickListener = Preference.OnPreferenceClickListener {
|
||||||
|
startActivity(
|
||||||
|
Intent(activity, IgnoreCodesActivity::class.java)
|
||||||
|
)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun setBluetoothResources() {
|
private fun setBluetoothResources() {
|
||||||
if (prefs.sendScanBluetooth &&
|
if (prefs.sendScanBluetooth &&
|
||||||
activity?.hasBluetoothPermission() == true
|
activity?.hasBluetoothPermission() == true
|
||||||
) {
|
) {
|
||||||
setBluetoothHosts(
|
findPreference<ListPreference>("send_scan_bluetooth_host")?.let {
|
||||||
findPreference("send_scan_bluetooth_host") as ListPreference
|
setBluetoothHosts(it)
|
||||||
)
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun wireClearNetworkPreferences() {
|
private fun wireClearNetworkPreferences() {
|
||||||
findPreference("clear_network_suggestions").apply {
|
findPreference<Preference>("clear_network_suggestions")?.apply {
|
||||||
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.Q) {
|
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.Q) {
|
||||||
// From R+ we can query past network suggestions and
|
// From R+ we can query past network suggestions and
|
||||||
// make them editable.
|
// make them editable.
|
||||||
setOnPreferenceClickListener {
|
setOnPreferenceClickListener {
|
||||||
fragmentManager.addFragment(NetworkSuggestionsFragment())
|
startActivity(
|
||||||
|
Intent(activity, NetworkSuggestionsActivity::class.java)
|
||||||
|
)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
} else if (Build.VERSION.SDK_INT == Build.VERSION_CODES.Q) {
|
} else if (Build.VERSION.SDK_INT == Build.VERSION_CODES.Q) {
|
||||||
@@ -85,7 +151,7 @@ class PreferencesFragment : PreferenceFragmentCompat() {
|
|||||||
// adding network suggestions on Q as well, so we
|
// adding network suggestions on Q as well, so we
|
||||||
// need to keep this option.
|
// need to keep this option.
|
||||||
setOnPreferenceClickListener {
|
setOnPreferenceClickListener {
|
||||||
context.askToClearNetworkSuggestions()
|
context?.askToClearNetworkSuggestions()
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -121,24 +187,33 @@ class PreferencesFragment : PreferenceFragmentCompat() {
|
|||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
|
if (lastProfile != prefs.profile) {
|
||||||
|
loadPreferences()
|
||||||
|
}
|
||||||
activity?.setTitle(R.string.preferences)
|
activity?.setTitle(R.string.preferences)
|
||||||
|
findPreference<Preference>(AUTOMATED_ACTIONS)?.let {
|
||||||
|
updateAutomatedActionsSummary(it)
|
||||||
|
}
|
||||||
|
findPreference<Preference>(IGNORE_CODES)?.let {
|
||||||
|
updateIgnoreCodesSummary(it)
|
||||||
|
}
|
||||||
|
findPreference<Preference>(PROFILE)?.let {
|
||||||
|
updateProfileSummary(it)
|
||||||
|
}
|
||||||
listView.setPaddingFromWindowInsets()
|
listView.setPaddingFromWindowInsets()
|
||||||
listView.removeOnScrollListener(systemBarRecyclerViewScrollListener)
|
listView.removeOnScrollListener(systemBarRecyclerViewScrollListener)
|
||||||
listView.addOnScrollListener(systemBarRecyclerViewScrollListener)
|
listView.addOnScrollListener(systemBarRecyclerViewScrollListener)
|
||||||
preferenceScreen.sharedPreferences
|
|
||||||
.registerOnSharedPreferenceChangeListener(changeListener)
|
|
||||||
setSummaries(preferenceScreen)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPause() {
|
override fun onPause() {
|
||||||
super.onPause()
|
super.onPause()
|
||||||
preferenceScreen.sharedPreferences
|
preferenceScreen.sharedPreferences
|
||||||
.unregisterOnSharedPreferenceChangeListener(changeListener)
|
?.unregisterOnSharedPreferenceChangeListener(changeListener)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDisplayPreferenceDialog(preference: Preference) {
|
override fun onDisplayPreferenceDialog(preference: Preference) {
|
||||||
if (preference is UrlPreference) {
|
if (preference is UrlPreference) {
|
||||||
val fm = fragmentManager
|
val fm = parentFragmentManager
|
||||||
UrlDialogFragment.newInstance(preference.key).apply {
|
UrlDialogFragment.newInstance(preference.key).apply {
|
||||||
setTargetFragment(this@PreferencesFragment, 0)
|
setTargetFragment(this@PreferencesFragment, 0)
|
||||||
show(fm, null)
|
show(fm, null)
|
||||||
@@ -162,35 +237,120 @@ class PreferencesFragment : PreferenceFragmentCompat() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun setSummary(preference: Preference) {
|
private fun setSummary(preference: Preference) {
|
||||||
|
if (preference.key == AUTOMATED_ACTIONS) {
|
||||||
|
updateAutomatedActionsSummary(preference)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (preference.key == IGNORE_CODES) {
|
||||||
|
updateIgnoreCodesSummary(preference)
|
||||||
|
return
|
||||||
|
}
|
||||||
when (preference) {
|
when (preference) {
|
||||||
is UrlPreference -> {
|
is UrlPreference -> {
|
||||||
preference.setSummary(preference.getUrl())
|
preference.summary = preference.getUrl()
|
||||||
}
|
}
|
||||||
|
|
||||||
is ListPreference -> {
|
is ListPreference -> {
|
||||||
preference.setSummary(preference.entry)
|
preference.setSummary(preference.entry)
|
||||||
}
|
}
|
||||||
|
|
||||||
is MultiSelectListPreference -> {
|
is MultiSelectListPreference -> {
|
||||||
preference.setSummary(
|
preference.summary = preference.values.joinToString(", ") {
|
||||||
preference.values.joinToString(", ") {
|
it.replace(Regex("_"), " ")
|
||||||
it.replace(Regex("_"), " ")
|
}
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
is PreferenceGroup -> {
|
is PreferenceGroup -> {
|
||||||
setSummaries(preference)
|
setSummaries(preference)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun setIcons() {
|
||||||
|
setIcon(PROFILE, R.drawable.ic_action_profile)
|
||||||
|
setIcon("formats", R.drawable.ic_action_scan)
|
||||||
|
setIcon("show_crop_handle", R.drawable.ic_action_crop)
|
||||||
|
setIcon("show_crosshairs", R.drawable.ic_action_search)
|
||||||
|
setIcon("zoom_by_swiping", R.drawable.ic_action_search)
|
||||||
|
setIcon("auto_rotate", R.drawable.ic_action_down)
|
||||||
|
setIcon("try_harder", R.drawable.ic_action_search)
|
||||||
|
setIcon("bulk_mode", R.drawable.ic_action_bulk_mode)
|
||||||
|
setIcon("bulk_mode_delay", R.drawable.ic_action_delay)
|
||||||
|
setIcon("show_toast_in_bulk_mode", R.drawable.ic_action_toast)
|
||||||
|
setIcon("vibrate", R.drawable.ic_action_vibrate)
|
||||||
|
setIcon("beep", R.drawable.ic_action_beep)
|
||||||
|
setIcon("beep_tone_name", R.drawable.ic_action_notification_sound)
|
||||||
|
setIcon("use_history", R.drawable.ic_action_history)
|
||||||
|
setIcon("ignore_duplicates_name", R.drawable.ic_action_remove)
|
||||||
|
setIcon(IGNORE_CODES, R.drawable.ic_action_remove)
|
||||||
|
setIcon("copy_immediately", R.drawable.ic_action_copy)
|
||||||
|
setIcon("send_scan_active", R.drawable.ic_action_forward)
|
||||||
|
setIcon("send_scan_url", R.drawable.ic_action_link)
|
||||||
|
setIcon("send_scan_type", R.drawable.ic_action_link)
|
||||||
|
setIcon("send_scan_bluetooth", R.drawable.ic_action_bluetooth)
|
||||||
|
setIcon("send_scan_bluetooth_host", R.drawable.ic_action_bluetooth)
|
||||||
|
setIcon("open_immediately", R.drawable.ic_action_open)
|
||||||
|
setIcon("show_meta_data", R.drawable.ic_action_info)
|
||||||
|
setIcon("show_hex_dump", R.drawable.ic_action_info)
|
||||||
|
setIcon("show_checksum", R.drawable.ic_action_info)
|
||||||
|
setIcon("show_recreation", R.drawable.ic_action_scan)
|
||||||
|
setIcon("close_automatically", R.drawable.ic_action_back)
|
||||||
|
setIcon("default_search_url", R.drawable.ic_action_search)
|
||||||
|
setIcon("open_with_url", R.drawable.ic_action_link)
|
||||||
|
setIcon(AUTOMATED_ACTIONS, R.drawable.ic_action_create)
|
||||||
|
setIcon("clear_network_suggestions", R.drawable.ic_action_wifi)
|
||||||
|
setIcon("brighten_screen", R.drawable.ic_action_bright)
|
||||||
|
setIcon("custom_locale", R.drawable.ic_action_preferences)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setIcon(key: String, iconResId: Int) {
|
||||||
|
findPreference<Preference>(key)?.setIcon(iconResId)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateAutomatedActionsSummary(preference: Preference) {
|
||||||
|
val count = prefs.automatedActions.size
|
||||||
|
preference.summary = if (count == 0) {
|
||||||
|
getString(R.string.automated_actions_none)
|
||||||
|
} else {
|
||||||
|
resources.getQuantityString(
|
||||||
|
R.plurals.automated_actions_count,
|
||||||
|
count,
|
||||||
|
count
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateIgnoreCodesSummary(preference: Preference) {
|
||||||
|
val count = prefs.ignoreCodes.size
|
||||||
|
preference.summary = if (count == 0) {
|
||||||
|
getString(R.string.ignore_codes_none)
|
||||||
|
} else {
|
||||||
|
resources.getQuantityString(
|
||||||
|
R.plurals.ignore_codes_count,
|
||||||
|
count,
|
||||||
|
count
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateProfileSummary(preference: Preference) {
|
||||||
|
preference.summary = prefs.profile ?: getString(R.string.profile_default)
|
||||||
|
lastProfile = prefs.profile
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val PROFILE = "profile"
|
||||||
|
private const val AUTOMATED_ACTIONS = "automated_actions"
|
||||||
|
private const val IGNORE_CODES = "ignore_codes"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun Activity.restartApp() {
|
private fun Activity.restartApp() {
|
||||||
val intent = Intent(this, SplashActivity::class.java)
|
val intent = Intent(this, SplashActivity::class.java)
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
|
intent.addFlags(
|
||||||
intent.addFlags(
|
Intent.FLAG_ACTIVITY_NEW_TASK or
|
||||||
Intent.FLAG_ACTIVITY_NEW_TASK or
|
Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||||
Intent.FLAG_ACTIVITY_CLEAR_TASK
|
)
|
||||||
)
|
|
||||||
}
|
|
||||||
startActivity(intent)
|
startActivity(intent)
|
||||||
finish()
|
finish()
|
||||||
// Restart to begin with an unmodified Locale to follow system settings.
|
// Restart to begin with an unmodified Locale to follow system settings.
|
||||||
|
|||||||
@@ -1,23 +1,24 @@
|
|||||||
package de.markusfisch.android.binaryeye.fragment
|
package de.markusfisch.android.binaryeye.fragment
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.support.v7.preference.PreferenceDialogFragmentCompat
|
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
|
import androidx.preference.PreferenceDialogFragmentCompat
|
||||||
import de.markusfisch.android.binaryeye.R
|
import de.markusfisch.android.binaryeye.R
|
||||||
import de.markusfisch.android.binaryeye.app.prefs
|
import de.markusfisch.android.binaryeye.app.prefs
|
||||||
import de.markusfisch.android.binaryeye.database.Scan
|
import de.markusfisch.android.binaryeye.database.Scan
|
||||||
import de.markusfisch.android.binaryeye.net.sendAsync
|
import de.markusfisch.android.binaryeye.net.sendAsync
|
||||||
import de.markusfisch.android.binaryeye.preference.UrlPreference
|
import de.markusfisch.android.binaryeye.preference.UrlPreference
|
||||||
|
import de.markusfisch.android.zxingcpp.ZxingCpp.BarcodeFormat
|
||||||
|
|
||||||
class UrlDialogFragment : PreferenceDialogFragmentCompat() {
|
class UrlDialogFragment : PreferenceDialogFragmentCompat() {
|
||||||
private var urlView: TextView? = null
|
private var urlView: TextView? = null
|
||||||
private var testButton: TextView? = null
|
private var testButton: TextView? = null
|
||||||
|
|
||||||
override fun onBindDialogView(view: View?) {
|
override fun onBindDialogView(view: View) {
|
||||||
super.onBindDialogView(view)
|
super.onBindDialogView(view)
|
||||||
urlView = view?.findViewById(R.id.url)
|
urlView = view.findViewById(R.id.url)
|
||||||
testButton = view?.findViewById(R.id.test_url)
|
testButton = view.findViewById(R.id.test_url)
|
||||||
testButton?.setOnClickListener {
|
testButton?.setOnClickListener {
|
||||||
testUrl(testButton)
|
testUrl(testButton)
|
||||||
}
|
}
|
||||||
@@ -50,7 +51,7 @@ class UrlDialogFragment : PreferenceDialogFragmentCompat() {
|
|||||||
Scan(
|
Scan(
|
||||||
"test",
|
"test",
|
||||||
null,
|
null,
|
||||||
"none"
|
BarcodeFormat.None
|
||||||
).sendAsync(url, prefs.sendScanType) { code, body ->
|
).sendAsync(url, prefs.sendScanType) { code, body ->
|
||||||
textView.text = when {
|
textView.text = when {
|
||||||
code != null -> "$code"
|
code != null -> "$code"
|
||||||
@@ -79,10 +80,5 @@ private fun completeUrl(template: String): String {
|
|||||||
if (!s.startsWith("http")) {
|
if (!s.startsWith("http")) {
|
||||||
s = "http://${s}"
|
s = "http://${s}"
|
||||||
}
|
}
|
||||||
if (prefs.sendScanType == "0" &&
|
|
||||||
!s.matches(".*/[a-zA-Z._-]*\\?[a-zA-Z0-9&=_-]+=$".toRegex())
|
|
||||||
) {
|
|
||||||
s = "${s}?content="
|
|
||||||
}
|
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ fun ContentResolver.loadImageUri(uri: Uri): Bitmap? = try {
|
|||||||
openInputStream(uri)?.use {
|
openInputStream(uri)?.use {
|
||||||
BitmapFactory.decodeStream(it, null, options)
|
BitmapFactory.decodeStream(it, null, options)
|
||||||
}
|
}
|
||||||
} catch (e: IOException) {
|
} catch (_: IOException) {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,17 +55,17 @@ fun Bitmap.crop(
|
|||||||
pivotX: Float,
|
pivotX: Float,
|
||||||
pivotY: Float
|
pivotY: Float
|
||||||
) = try {
|
) = try {
|
||||||
val erected = erect(rotation, pivotX, pivotY)
|
val b = rotate(rotation, pivotX, pivotY)
|
||||||
val w = erected.width
|
val w = b.width
|
||||||
val h = erected.height
|
val h = b.height
|
||||||
val x = max(0, (rect.left * w).roundToInt())
|
val x = max(0, (rect.left * w).roundToInt())
|
||||||
val y = max(0, (rect.top * h).roundToInt())
|
val y = max(0, (rect.top * h).roundToInt())
|
||||||
Bitmap.createBitmap(
|
Bitmap.createBitmap(
|
||||||
erected,
|
b,
|
||||||
x,
|
x,
|
||||||
y,
|
y,
|
||||||
min(w - x, (rect.right * w).roundToInt() - x),
|
min(w, (rect.right * w).roundToInt()) - x,
|
||||||
min(h - y, (rect.bottom * h).roundToInt() - y)
|
min(h, (rect.bottom * h).roundToInt()) - y
|
||||||
)
|
)
|
||||||
} catch (e: OutOfMemoryError) {
|
} catch (e: OutOfMemoryError) {
|
||||||
null
|
null
|
||||||
@@ -73,13 +73,11 @@ fun Bitmap.crop(
|
|||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun Bitmap.erect(
|
private fun Bitmap.rotate(
|
||||||
rotation: Float,
|
rotation: Float,
|
||||||
pivotX: Float,
|
pivotX: Float,
|
||||||
pivotY: Float
|
pivotY: Float
|
||||||
): Bitmap = if (
|
): Bitmap = if (rotation % 360f != 0f) {
|
||||||
rotation % 360f != 0f
|
|
||||||
) {
|
|
||||||
Bitmap.createBitmap(
|
Bitmap.createBitmap(
|
||||||
this,
|
this,
|
||||||
0,
|
0,
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ package de.markusfisch.android.binaryeye.graphics
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.DashPathEffect
|
import android.graphics.DashPathEffect
|
||||||
import android.graphics.Paint
|
import android.graphics.Paint
|
||||||
import android.support.v4.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import de.markusfisch.android.binaryeye.R
|
import de.markusfisch.android.binaryeye.R
|
||||||
|
|
||||||
fun Context.getDashedBorderPaint() = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
fun Context.getDashedBorderPaint() = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||||
|
|||||||
@@ -5,26 +5,22 @@ import android.graphics.Bitmap
|
|||||||
import android.graphics.Canvas
|
import android.graphics.Canvas
|
||||||
import android.graphics.drawable.BitmapDrawable
|
import android.graphics.drawable.BitmapDrawable
|
||||||
import android.graphics.drawable.Drawable
|
import android.graphics.drawable.Drawable
|
||||||
import android.os.Build
|
import androidx.core.content.res.ResourcesCompat
|
||||||
|
import androidx.core.graphics.createBitmap
|
||||||
|
|
||||||
fun Resources.getBitmapFromDrawable(
|
fun Resources.getBitmapFromDrawable(
|
||||||
resId: Int
|
resId: Int
|
||||||
): Bitmap = getBitmapFromDrawable(getDrawableCompat(resId))
|
): Bitmap = getBitmapFromDrawable(
|
||||||
|
ResourcesCompat.getDrawable(this, resId, null) ?: throw Resources.NotFoundException(
|
||||||
fun Resources.getDrawableCompat(
|
"Drawable resource ID #0x${resId.toString(16)}"
|
||||||
resId: Int
|
)
|
||||||
): Drawable = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
)
|
||||||
getDrawable(resId, null)
|
|
||||||
} else {
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
getDrawable(resId)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getBitmapFromDrawable(drawable: Drawable): Bitmap {
|
private fun getBitmapFromDrawable(drawable: Drawable): Bitmap {
|
||||||
if (drawable is BitmapDrawable) {
|
if (drawable is BitmapDrawable) {
|
||||||
return drawable.bitmap
|
return drawable.bitmap
|
||||||
}
|
}
|
||||||
val bitmap = Bitmap.createBitmap(
|
val bitmap = createBitmap(
|
||||||
drawable.intrinsicWidth,
|
drawable.intrinsicWidth,
|
||||||
drawable.intrinsicHeight,
|
drawable.intrinsicHeight,
|
||||||
Bitmap.Config.ARGB_8888
|
Bitmap.Config.ARGB_8888
|
||||||
|
|||||||
@@ -10,7 +10,9 @@ data class FrameMetrics(
|
|||||||
var width: Int = 0,
|
var width: Int = 0,
|
||||||
var height: Int = 0,
|
var height: Int = 0,
|
||||||
var orientation: Int = 0
|
var orientation: Int = 0
|
||||||
)
|
) {
|
||||||
|
fun isValid() = width > 0 && height > 0
|
||||||
|
}
|
||||||
|
|
||||||
fun Rect.setFrameRoi(
|
fun Rect.setFrameRoi(
|
||||||
frameMetrics: FrameMetrics,
|
frameMetrics: FrameMetrics,
|
||||||
@@ -54,6 +56,7 @@ fun Matrix.setFrameToView(
|
|||||||
uprightWidth = frameMetrics.height
|
uprightWidth = frameMetrics.height
|
||||||
uprightHeight = frameMetrics.width
|
uprightHeight = frameMetrics.width
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
uprightWidth = frameMetrics.width
|
uprightWidth = frameMetrics.width
|
||||||
uprightHeight = frameMetrics.height
|
uprightHeight = frameMetrics.height
|
||||||
|
|||||||
@@ -2,14 +2,16 @@ package de.markusfisch.android.binaryeye.graphics
|
|||||||
|
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.graphics.Canvas
|
import android.graphics.Canvas
|
||||||
|
import androidx.core.graphics.createBitmap
|
||||||
|
|
||||||
fun Bitmap.fixTransparency(): Bitmap {
|
fun Bitmap.fixTransparency(): Bitmap {
|
||||||
val result = analyzeTransparency()
|
val result = analyzeTransparency()
|
||||||
return if (result.hasTransparentPixels) {
|
val cfg = config
|
||||||
val copy = Bitmap.createBitmap(
|
return if (cfg != null && result.hasTransparentPixels) {
|
||||||
|
val copy = createBitmap(
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
config
|
cfg
|
||||||
)
|
)
|
||||||
Canvas(copy).apply {
|
Canvas(copy).apply {
|
||||||
drawColor(result.backgroundColor)
|
drawColor(result.backgroundColor)
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ import android.content.Context
|
|||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Environment
|
import android.os.Environment
|
||||||
import android.provider.MediaStore
|
import android.provider.MediaStore
|
||||||
import android.support.annotation.MainThread
|
|
||||||
import android.widget.EditText
|
import android.widget.EditText
|
||||||
|
import androidx.annotation.MainThread
|
||||||
import de.markusfisch.android.binaryeye.R
|
import de.markusfisch.android.binaryeye.R
|
||||||
import de.markusfisch.android.binaryeye.app.alertDialog
|
import de.markusfisch.android.binaryeye.app.alertDialog
|
||||||
import java.io.File
|
import java.io.File
|
||||||
@@ -53,7 +53,7 @@ fun Context.writeExternalFile(
|
|||||||
): Boolean = try {
|
): Boolean = try {
|
||||||
openExternalOutputStream(fileName, mimeType).use { write(it) }
|
openExternalOutputStream(fileName, mimeType).use { write(it) }
|
||||||
true
|
true
|
||||||
} catch (e: IOException) {
|
} catch (_: IOException) {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,7 +61,6 @@ private fun Context.openExternalOutputStream(
|
|||||||
fileName: String,
|
fileName: String,
|
||||||
mimeType: String
|
mimeType: String
|
||||||
): OutputStream = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
): OutputStream = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
val file = File(
|
val file = File(
|
||||||
Environment.getExternalStoragePublicDirectory(
|
Environment.getExternalStoragePublicDirectory(
|
||||||
Environment.DIRECTORY_DOWNLOADS
|
Environment.DIRECTORY_DOWNLOADS
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ private var errorToneGenerator: ToneGenerator? = null
|
|||||||
|
|
||||||
fun beepConfirm() {
|
fun beepConfirm() {
|
||||||
val tg = confirmToneGenerator ?: ToneGenerator(
|
val tg = confirmToneGenerator ?: ToneGenerator(
|
||||||
AudioManager.STREAM_NOTIFICATION,
|
AudioManager.STREAM_MUSIC,
|
||||||
ToneGenerator.MAX_VOLUME
|
ToneGenerator.MAX_VOLUME
|
||||||
)
|
)
|
||||||
confirmToneGenerator = tg
|
confirmToneGenerator = tg
|
||||||
@@ -22,7 +22,7 @@ fun beepError() {
|
|||||||
ToneGenerator.MAX_VOLUME
|
ToneGenerator.MAX_VOLUME
|
||||||
)
|
)
|
||||||
errorToneGenerator = tg
|
errorToneGenerator = tg
|
||||||
tg.startTone(ToneGenerator.TONE_SUP_ERROR)
|
tg.startTone(ToneGenerator.TONE_SUP_ERROR, 1000)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun releaseToneGenerators() {
|
fun releaseToneGenerators() {
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package de.markusfisch.android.binaryeye.net
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
|
|
||||||
|
private const val SCHEME = "binaryeye"
|
||||||
|
private const val HOSTNAME = "markusfisch.de"
|
||||||
|
private const val ENCODE_PATH = "encode"
|
||||||
|
|
||||||
|
fun isScanDeeplink(text: String) = listOf(
|
||||||
|
"$SCHEME://scan",
|
||||||
|
"http://$HOSTNAME/BinaryEye",
|
||||||
|
"https://$HOSTNAME/BinaryEye"
|
||||||
|
).any { text.startsWith(it) }
|
||||||
|
|
||||||
|
fun isEncodeDeeplink(text: String) = listOf(
|
||||||
|
"$SCHEME://$ENCODE_PATH",
|
||||||
|
"http://$HOSTNAME/$ENCODE_PATH",
|
||||||
|
"https://$HOSTNAME/$ENCODE_PATH"
|
||||||
|
).any { text.startsWith(it) }
|
||||||
|
|
||||||
|
fun createEncodeDeeplink(format: String, content: String): String {
|
||||||
|
return Uri.Builder()
|
||||||
|
.scheme("https")
|
||||||
|
.authority(HOSTNAME)
|
||||||
|
.appendPath(ENCODE_PATH)
|
||||||
|
.appendQueryParameter("format", format)
|
||||||
|
.appendQueryParameter("content", content)
|
||||||
|
.build()
|
||||||
|
.toString()
|
||||||
|
}
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
package de.markusfisch.android.binaryeye.net
|
package de.markusfisch.android.binaryeye.net
|
||||||
|
|
||||||
import de.markusfisch.android.binaryeye.content.toHexString
|
|
||||||
import de.markusfisch.android.binaryeye.database.Scan
|
import de.markusfisch.android.binaryeye.database.Scan
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -33,14 +32,19 @@ private fun Scan.send(url: String, type: String): Response {
|
|||||||
"1" -> request(
|
"1" -> request(
|
||||||
url + "?" + asUrlArguments()
|
url + "?" + asUrlArguments()
|
||||||
)
|
)
|
||||||
|
|
||||||
"2" -> request(url) { con ->
|
"2" -> request(url) { con ->
|
||||||
con.requestMethod = "POST"
|
con.requestMethod = "POST"
|
||||||
con.setRequestProperty("Content-Type", "application/x-www-form-urlencoded")
|
con.setRequestProperty(
|
||||||
|
"Content-Type",
|
||||||
|
"application/x-www-form-urlencoded"
|
||||||
|
)
|
||||||
con.outputStream.apply {
|
con.outputStream.apply {
|
||||||
write(asUrlArguments().toByteArray())
|
write(asUrlArguments().toByteArray())
|
||||||
close()
|
close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
"3" -> request(url) { con ->
|
"3" -> request(url) { con ->
|
||||||
con.requestMethod = "POST"
|
con.requestMethod = "POST"
|
||||||
con.setRequestProperty("Content-Type", "application/json")
|
con.setRequestProperty("Content-Type", "application/json")
|
||||||
@@ -49,8 +53,9 @@ private fun Scan.send(url: String, type: String): Response {
|
|||||||
close()
|
close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> request(
|
else -> request(
|
||||||
url + content.urlEncode()
|
url + text.urlEncode()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -64,9 +69,9 @@ private fun Scan.asJson() = JSONObject().apply {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun Scan.getMap(): Map<String, String> = mapOf(
|
private fun Scan.getMap(): Map<String, String> = mapOf(
|
||||||
"content" to content,
|
"content" to text,
|
||||||
"raw" to raw?.toHexString(),
|
"raw" to raw?.toHexString(),
|
||||||
"format" to format,
|
"format" to format.name,
|
||||||
"errorCorrectionLevel" to errorCorrectionLevel,
|
"errorCorrectionLevel" to errorCorrectionLevel,
|
||||||
"version" to version,
|
"version" to version,
|
||||||
"sequenceSize" to sequenceSize.toString(),
|
"sequenceSize" to sequenceSize.toString(),
|
||||||
|
|||||||
@@ -5,6 +5,6 @@ import java.net.URLEncoder
|
|||||||
|
|
||||||
fun String.urlEncode(): String = try {
|
fun String.urlEncode(): String = try {
|
||||||
URLEncoder.encode(this, "UTF-8")
|
URLEncoder.encode(this, "UTF-8")
|
||||||
} catch (e: UnsupportedEncodingException) {
|
} catch (_: UnsupportedEncodingException) {
|
||||||
this
|
this
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package de.markusfisch.android.binaryeye.os
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.view.WindowManager
|
||||||
|
|
||||||
|
fun Activity.getScreenBrightness(): Float {
|
||||||
|
val layoutParams = window.attributes
|
||||||
|
return if (layoutParams.screenBrightness ==
|
||||||
|
WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_NONE
|
||||||
|
) {
|
||||||
|
android.provider.Settings.System.getInt(
|
||||||
|
contentResolver,
|
||||||
|
android.provider.Settings.System.SCREEN_BRIGHTNESS
|
||||||
|
) / 255.0f
|
||||||
|
} else {
|
||||||
|
layoutParams.screenBrightness
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Activity.setScreenBrightness(brightness: Float) {
|
||||||
|
val layoutParams = window.attributes
|
||||||
|
layoutParams.screenBrightness = brightness
|
||||||
|
window.attributes = layoutParams
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
package de.markusfisch.android.binaryeye.preference
|
||||||
|
|
||||||
|
import android.content.SharedPreferences
|
||||||
|
import android.graphics.Point
|
||||||
|
import androidx.core.content.edit
|
||||||
|
import org.json.JSONObject
|
||||||
|
|
||||||
|
fun SharedPreferences.storeCropHandle(
|
||||||
|
name: String,
|
||||||
|
viewWidth: Int,
|
||||||
|
viewHeight: Int,
|
||||||
|
cropHandle: Point
|
||||||
|
) {
|
||||||
|
edit {
|
||||||
|
putString(
|
||||||
|
cropHandleKey(name, viewWidth, viewHeight),
|
||||||
|
JSONObject().apply {
|
||||||
|
put("x", cropHandle.x)
|
||||||
|
put("y", cropHandle.y)
|
||||||
|
}.toString()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun SharedPreferences.restoreCropHandle(
|
||||||
|
name: String,
|
||||||
|
viewWidth: Int,
|
||||||
|
viewHeight: Int,
|
||||||
|
default: Point = Point(-2, -2) // -2 means set default roi.
|
||||||
|
): Point {
|
||||||
|
val s = getString(
|
||||||
|
cropHandleKey(name, viewWidth, viewHeight), null
|
||||||
|
) ?: return default
|
||||||
|
return try {
|
||||||
|
val json = JSONObject(s)
|
||||||
|
Point(
|
||||||
|
json.getInt("x"),
|
||||||
|
json.getInt("y")
|
||||||
|
)
|
||||||
|
} catch (_: Exception) {
|
||||||
|
return default
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun cropHandleKey(
|
||||||
|
name: String,
|
||||||
|
viewWidth: Int,
|
||||||
|
viewHeight: Int
|
||||||
|
) = "${name}:${viewWidth}:${viewHeight}"
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
package de.markusfisch.android.binaryeye.preference
|
||||||
|
|
||||||
|
import org.json.JSONArray
|
||||||
|
import org.json.JSONObject
|
||||||
|
|
||||||
|
data class IgnoreCode(
|
||||||
|
val pattern: String,
|
||||||
|
val format: String? = null
|
||||||
|
) {
|
||||||
|
private val compiledRegex: Regex? by lazy {
|
||||||
|
try {
|
||||||
|
Regex(pattern)
|
||||||
|
} catch (_: Exception) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun matches(content: String, format: String): Boolean =
|
||||||
|
(this.format == null || this.format == format) &&
|
||||||
|
compiledRegex?.containsMatchIn(content) == true
|
||||||
|
|
||||||
|
fun toJson(): JSONObject = JSONObject().apply {
|
||||||
|
put(KEY_PATTERN, pattern)
|
||||||
|
put(KEY_FORMAT, format ?: "")
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val KEY_PATTERN = "pattern"
|
||||||
|
private const val KEY_FORMAT = "format"
|
||||||
|
|
||||||
|
fun fromJson(obj: JSONObject): IgnoreCode? {
|
||||||
|
val pattern = obj.optString(KEY_PATTERN, "").trim()
|
||||||
|
if (pattern.isEmpty()) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
val format = obj.optString(KEY_FORMAT, "").trim()
|
||||||
|
return IgnoreCode(
|
||||||
|
pattern = pattern,
|
||||||
|
format = format.ifEmpty { null }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun fromJsonArray(json: String): MutableList<IgnoreCode> {
|
||||||
|
val items = ArrayList<IgnoreCode>()
|
||||||
|
val array = try {
|
||||||
|
JSONArray(json)
|
||||||
|
} catch (_: Exception) {
|
||||||
|
JSONArray()
|
||||||
|
}
|
||||||
|
for (i in 0 until array.length()) {
|
||||||
|
when (val item = array.opt(i)) {
|
||||||
|
is String -> {
|
||||||
|
val pattern = item.trim()
|
||||||
|
if (pattern.isNotEmpty()) {
|
||||||
|
items.add(IgnoreCode(pattern))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
is JSONObject -> {
|
||||||
|
fromJson(item)?.let { items.add(it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return items
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toJsonArray(items: List<IgnoreCode>): String {
|
||||||
|
val array = JSONArray()
|
||||||
|
items.forEach { item ->
|
||||||
|
array.put(item.toJson())
|
||||||
|
}
|
||||||
|
return array.toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,60 +1,75 @@
|
|||||||
package de.markusfisch.android.binaryeye.preference
|
package de.markusfisch.android.binaryeye.preference
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
|
import android.graphics.Point
|
||||||
import android.media.ToneGenerator
|
import android.media.ToneGenerator
|
||||||
import android.os.Build
|
|
||||||
import android.preference.PreferenceManager
|
import android.preference.PreferenceManager
|
||||||
import android.support.annotation.RequiresApi
|
import androidx.core.content.edit
|
||||||
import de.markusfisch.android.zxingcpp.ZxingCpp.Format
|
import de.markusfisch.android.binaryeye.automation.AutomatedAction
|
||||||
|
import de.markusfisch.android.binaryeye.automation.AutomatedAction.Companion.fromJsonArray
|
||||||
|
import de.markusfisch.android.binaryeye.automation.AutomatedAction.Companion.toJsonArray
|
||||||
|
import de.markusfisch.android.binaryeye.zxingcpp.migrateBarcodeFormatName
|
||||||
|
import de.markusfisch.android.zxingcpp.ZxingCpp.BarcodeFormat
|
||||||
|
import de.markusfisch.android.binaryeye.preference.IgnoreCode.Companion.fromJsonArray as ignoreCodesFromJsonArray
|
||||||
|
import de.markusfisch.android.binaryeye.preference.IgnoreCode.Companion.toJsonArray as ignoreCodesToJsonArray
|
||||||
|
|
||||||
class Preferences {
|
class Preferences {
|
||||||
|
lateinit var defaultPreferences: SharedPreferences
|
||||||
lateinit var preferences: SharedPreferences
|
lateinit var preferences: SharedPreferences
|
||||||
|
|
||||||
|
val profiles = ArrayList<String>()
|
||||||
|
|
||||||
|
var profile: String? = null
|
||||||
|
set(value) {
|
||||||
|
defaultPreferences.edit {
|
||||||
|
putString(PROFILE, value)
|
||||||
|
}
|
||||||
|
field = value
|
||||||
|
}
|
||||||
var barcodeFormats = setOf(
|
var barcodeFormats = setOf(
|
||||||
Format.AZTEC.name,
|
BarcodeFormat.Aztec.name,
|
||||||
Format.CODABAR.name,
|
BarcodeFormat.Codabar.name,
|
||||||
Format.CODE_39.name,
|
BarcodeFormat.Code39.name,
|
||||||
Format.CODE_93.name,
|
BarcodeFormat.Code39Ext.name,
|
||||||
Format.CODE_128.name,
|
BarcodeFormat.Code32.name,
|
||||||
Format.DATA_BAR.name,
|
BarcodeFormat.PZN.name,
|
||||||
Format.DATA_BAR_EXPANDED.name,
|
BarcodeFormat.Code93.name,
|
||||||
Format.DATA_MATRIX.name,
|
BarcodeFormat.Code128.name,
|
||||||
Format.EAN_8.name,
|
BarcodeFormat.DataBar.name,
|
||||||
Format.EAN_13.name,
|
BarcodeFormat.DataBarOmni.name,
|
||||||
Format.ITF.name,
|
BarcodeFormat.DataBarStk.name,
|
||||||
Format.MAXICODE.name,
|
BarcodeFormat.DataBarLtd.name,
|
||||||
Format.PDF_417.name,
|
BarcodeFormat.DataBarExp.name,
|
||||||
Format.QR_CODE.name,
|
BarcodeFormat.DataBarExpStk.name,
|
||||||
Format.MICRO_QR_CODE.name,
|
BarcodeFormat.DataMatrix.name,
|
||||||
Format.UPC_A.name,
|
BarcodeFormat.DXFilmEdge.name,
|
||||||
Format.UPC_E.name,
|
BarcodeFormat.EAN8.name,
|
||||||
|
BarcodeFormat.EAN13.name,
|
||||||
|
BarcodeFormat.ITF.name,
|
||||||
|
BarcodeFormat.MaxiCode.name,
|
||||||
|
BarcodeFormat.PDF417.name,
|
||||||
|
BarcodeFormat.QRCode.name,
|
||||||
|
BarcodeFormat.MicroQRCode.name,
|
||||||
|
BarcodeFormat.RMQRCode.name,
|
||||||
|
BarcodeFormat.UPCA.name,
|
||||||
|
BarcodeFormat.UPCE.name,
|
||||||
)
|
)
|
||||||
@RequiresApi(Build.VERSION_CODES.HONEYCOMB)
|
|
||||||
set(value) {
|
set(value) {
|
||||||
apply(BARCODE_FORMATS, value)
|
apply(BARCODE_FORMATS, value)
|
||||||
field = value
|
field = value
|
||||||
}
|
}
|
||||||
var cropHandleX = -2 // -2 means set default roi.
|
|
||||||
set(value) {
|
|
||||||
apply(CROP_HANDLE_X, value)
|
|
||||||
field = value
|
|
||||||
}
|
|
||||||
var cropHandleY = -2
|
|
||||||
set(value) {
|
|
||||||
apply(CROP_HANDLE_Y, value)
|
|
||||||
field = value
|
|
||||||
}
|
|
||||||
var cropHandleOrientation = 0
|
|
||||||
set(value) {
|
|
||||||
apply(CROP_HANDLE_ORIENTATION, value)
|
|
||||||
field = value
|
|
||||||
}
|
|
||||||
var showCropHandle = true
|
var showCropHandle = true
|
||||||
set(value) {
|
set(value) {
|
||||||
apply(SHOW_CROP_HANDLE, value)
|
apply(SHOW_CROP_HANDLE, value)
|
||||||
field = value
|
field = value
|
||||||
}
|
}
|
||||||
|
var showCrosshairs = false
|
||||||
|
set(value) {
|
||||||
|
apply(SHOW_CROSSHAIRS, value)
|
||||||
|
field = value
|
||||||
|
}
|
||||||
var zoomBySwiping = true
|
var zoomBySwiping = true
|
||||||
set(value) {
|
set(value) {
|
||||||
apply(ZOOM_BY_SWIPING, value)
|
apply(ZOOM_BY_SWIPING, value)
|
||||||
@@ -105,11 +120,13 @@ class Preferences {
|
|||||||
apply(USE_HISTORY, value)
|
apply(USE_HISTORY, value)
|
||||||
field = value
|
field = value
|
||||||
}
|
}
|
||||||
var ignoreConsecutiveDuplicates = true
|
var ignoreDuplicatesName = "ignore_consecutive_duplicates"
|
||||||
set(value) {
|
set(value) {
|
||||||
apply(IGNORE_CONSECUTIVE_DUPLICATES, value)
|
apply(IGNORE_DUPLICATES_NAME, value)
|
||||||
field = value
|
field = value
|
||||||
}
|
}
|
||||||
|
var ignoreCodes = mutableListOf(IgnoreCode(DEFAULT_IGNORE_CODE_PATTERN))
|
||||||
|
private set
|
||||||
var copyImmediately = false
|
var copyImmediately = false
|
||||||
set(value) {
|
set(value) {
|
||||||
apply(COPY_IMMEDIATELY, value)
|
apply(COPY_IMMEDIATELY, value)
|
||||||
@@ -130,6 +147,11 @@ class Preferences {
|
|||||||
apply(SHOW_HEX_DUMP, value)
|
apply(SHOW_HEX_DUMP, value)
|
||||||
field = value
|
field = value
|
||||||
}
|
}
|
||||||
|
var showChecksum = ""
|
||||||
|
set(value) {
|
||||||
|
apply(SHOW_CHECKSUM, value)
|
||||||
|
field = value
|
||||||
|
}
|
||||||
var showRecreation = true
|
var showRecreation = true
|
||||||
set(value) {
|
set(value) {
|
||||||
apply(SHOW_RECREATION, value)
|
apply(SHOW_RECREATION, value)
|
||||||
@@ -150,7 +172,7 @@ class Preferences {
|
|||||||
apply(OPEN_WITH_URL, value)
|
apply(OPEN_WITH_URL, value)
|
||||||
field = value
|
field = value
|
||||||
}
|
}
|
||||||
var sendScanActive = true
|
var sendScanActive = false
|
||||||
set(value) {
|
set(value) {
|
||||||
apply(SEND_SCAN_ACTIVE, value)
|
apply(SEND_SCAN_ACTIVE, value)
|
||||||
field = value
|
field = value
|
||||||
@@ -177,7 +199,9 @@ class Preferences {
|
|||||||
}
|
}
|
||||||
var customLocale: String = ""
|
var customLocale: String = ""
|
||||||
set(value) {
|
set(value) {
|
||||||
apply(CUSTOM_LOCALE, value)
|
// Make sure this setting is written immediately because
|
||||||
|
// the app is about to restart.
|
||||||
|
commit(CUSTOM_LOCALE, value)
|
||||||
field = value
|
field = value
|
||||||
}
|
}
|
||||||
var indexOfLastSelectedFormat: Int = 0
|
var indexOfLastSelectedFormat: Int = 0
|
||||||
@@ -190,7 +214,7 @@ class Preferences {
|
|||||||
apply(INDEX_OF_LAST_SELECTED_EC_LEVEL, value)
|
apply(INDEX_OF_LAST_SELECTED_EC_LEVEL, value)
|
||||||
field = value
|
field = value
|
||||||
}
|
}
|
||||||
var freeRotation = true
|
var freeRotation = false
|
||||||
set(value) {
|
set(value) {
|
||||||
apply(FREE_ROTATION, value)
|
apply(FREE_ROTATION, value)
|
||||||
field = value
|
field = value
|
||||||
@@ -200,28 +224,111 @@ class Preferences {
|
|||||||
apply(EXPAND_ESCAPE_SEQUENCES, value)
|
apply(EXPAND_ESCAPE_SEQUENCES, value)
|
||||||
field = value
|
field = value
|
||||||
}
|
}
|
||||||
|
var addQuietZone = true
|
||||||
|
set(value) {
|
||||||
|
apply(ADD_QUIET_ZONE, value)
|
||||||
|
field = value
|
||||||
|
}
|
||||||
|
var brightenScreen = false
|
||||||
|
set(value) {
|
||||||
|
apply(BRIGHTEN_SCREEN, value)
|
||||||
|
field = value
|
||||||
|
}
|
||||||
|
var previewScale = 0f
|
||||||
|
set(value) {
|
||||||
|
apply(PREVIEW_SCALE, value)
|
||||||
|
field = value
|
||||||
|
}
|
||||||
|
var automatedActions = mutableListOf<AutomatedAction>()
|
||||||
|
private set
|
||||||
|
|
||||||
fun init(context: Context) {
|
fun init(context: Context) {
|
||||||
preferences = PreferenceManager.getDefaultSharedPreferences(context)
|
// I'm not including a support library just to get the
|
||||||
|
// default preferences. Dependencies are a burden.
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
defaultPreferences = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
|
|
||||||
|
loadProfiles()
|
||||||
|
load(context, defaultPreferences.getString(PROFILE, null))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun load(context: Context, profileName: String?) {
|
||||||
|
preferences = if (profileName.isNullOrEmpty()) {
|
||||||
|
profile = null
|
||||||
|
defaultPreferences
|
||||||
|
} else {
|
||||||
|
profile = profileName
|
||||||
|
context.getSharedPreferences(
|
||||||
|
profileName,
|
||||||
|
Context.MODE_PRIVATE
|
||||||
|
)
|
||||||
|
}
|
||||||
update()
|
update()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun update() {
|
fun addProfile(name: String): Boolean {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
|
if (!name.matches("^[a-zA-Z0-9_-]+$".toRegex()) ||
|
||||||
preferences.getStringSet(BARCODE_FORMATS, barcodeFormats)?.let {
|
profiles.contains(name)
|
||||||
barcodeFormats = it
|
) {
|
||||||
}
|
return false
|
||||||
}
|
}
|
||||||
cropHandleX = preferences.getInt(CROP_HANDLE_X, cropHandleX)
|
profiles.add(name)
|
||||||
cropHandleY = preferences.getInt(CROP_HANDLE_Y, cropHandleY)
|
saveProfiles()
|
||||||
cropHandleOrientation = preferences.getInt(
|
return true
|
||||||
CROP_HANDLE_ORIENTATION,
|
}
|
||||||
cropHandleOrientation
|
|
||||||
)
|
fun removeProfile(name: String): Boolean {
|
||||||
|
if (!profiles.remove(name)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
saveProfiles()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
fun saveProfiles() {
|
||||||
|
defaultPreferences.edit {
|
||||||
|
putString(PROFILES, org.json.JSONArray(profiles).toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun loadProfiles() {
|
||||||
|
profiles.clear()
|
||||||
|
org.json.JSONArray(
|
||||||
|
defaultPreferences.getString(PROFILES, "[]")
|
||||||
|
).let {
|
||||||
|
profiles.addAll(Array(it.length()) { i -> it.getString(i) })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun update() {
|
||||||
|
preferences.getStringSet(BARCODE_FORMATS, barcodeFormats)?.let {
|
||||||
|
val migrated = migrateBarcodeFormats(it)
|
||||||
|
if (migrated != it) {
|
||||||
|
apply(BARCODE_FORMATS, migrated)
|
||||||
|
}
|
||||||
|
barcodeFormats = addFormatsOnUpdate(
|
||||||
|
migrated,
|
||||||
|
BarcodeFormat.Code39Ext,
|
||||||
|
BarcodeFormat.Code32,
|
||||||
|
BarcodeFormat.PZN,
|
||||||
|
BarcodeFormat.DataBarOmni,
|
||||||
|
BarcodeFormat.DataBarStk,
|
||||||
|
BarcodeFormat.DataBarLtd,
|
||||||
|
BarcodeFormat.DataBarExp,
|
||||||
|
BarcodeFormat.DataBarExpStk,
|
||||||
|
BarcodeFormat.RMQRCode,
|
||||||
|
BarcodeFormat.DXFilmEdge
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
showCropHandle = preferences.getBoolean(
|
showCropHandle = preferences.getBoolean(
|
||||||
SHOW_CROP_HANDLE,
|
SHOW_CROP_HANDLE,
|
||||||
showCropHandle
|
showCropHandle
|
||||||
)
|
)
|
||||||
|
showCrosshairs = preferences.getBoolean(
|
||||||
|
SHOW_CROSSHAIRS,
|
||||||
|
showCrosshairs
|
||||||
|
)
|
||||||
zoomBySwiping = preferences.getBoolean(ZOOM_BY_SWIPING, zoomBySwiping)
|
zoomBySwiping = preferences.getBoolean(ZOOM_BY_SWIPING, zoomBySwiping)
|
||||||
autoRotate = preferences.getBoolean(AUTO_ROTATE, autoRotate)
|
autoRotate = preferences.getBoolean(AUTO_ROTATE, autoRotate)
|
||||||
tryHarder = preferences.getBoolean(TRY_HARDER, tryHarder)
|
tryHarder = preferences.getBoolean(TRY_HARDER, tryHarder)
|
||||||
@@ -240,9 +347,29 @@ class Preferences {
|
|||||||
beepToneName = it
|
beepToneName = it
|
||||||
}
|
}
|
||||||
useHistory = preferences.getBoolean(USE_HISTORY, useHistory)
|
useHistory = preferences.getBoolean(USE_HISTORY, useHistory)
|
||||||
ignoreConsecutiveDuplicates = preferences.getBoolean(
|
|
||||||
IGNORE_CONSECUTIVE_DUPLICATES,
|
// Map old setting to new one if it wasn't on default value.
|
||||||
ignoreConsecutiveDuplicates
|
val ignoreConsecutiveDuplicates = "ignore_consecutive_duplicates"
|
||||||
|
if (!preferences.getBoolean(ignoreConsecutiveDuplicates, true)) {
|
||||||
|
preferences.edit {
|
||||||
|
remove(ignoreConsecutiveDuplicates)
|
||||||
|
}
|
||||||
|
ignoreDuplicatesName = "accept_duplicates"
|
||||||
|
}
|
||||||
|
|
||||||
|
preferences.getString(
|
||||||
|
IGNORE_DUPLICATES_NAME,
|
||||||
|
ignoreDuplicatesName
|
||||||
|
)?.also {
|
||||||
|
ignoreDuplicatesName = it
|
||||||
|
}
|
||||||
|
ignoreCodes = ignoreCodesFromJsonArray(
|
||||||
|
preferences.getString(
|
||||||
|
IGNORE_CODES,
|
||||||
|
ignoreCodesToJsonArray(
|
||||||
|
listOf(IgnoreCode(DEFAULT_IGNORE_CODE_PATTERN))
|
||||||
|
)
|
||||||
|
) ?: "[]"
|
||||||
)
|
)
|
||||||
copyImmediately = preferences.getBoolean(
|
copyImmediately = preferences.getBoolean(
|
||||||
COPY_IMMEDIATELY,
|
COPY_IMMEDIATELY,
|
||||||
@@ -254,6 +381,9 @@ class Preferences {
|
|||||||
)
|
)
|
||||||
showMetaData = preferences.getBoolean(SHOW_META_DATA, showMetaData)
|
showMetaData = preferences.getBoolean(SHOW_META_DATA, showMetaData)
|
||||||
showHexDump = preferences.getBoolean(SHOW_HEX_DUMP, showHexDump)
|
showHexDump = preferences.getBoolean(SHOW_HEX_DUMP, showHexDump)
|
||||||
|
preferences.getString(SHOW_CHECKSUM, showChecksum)?.also {
|
||||||
|
showChecksum = it
|
||||||
|
}
|
||||||
showRecreation = preferences.getBoolean(SHOW_RECREATION, showRecreation)
|
showRecreation = preferences.getBoolean(SHOW_RECREATION, showRecreation)
|
||||||
closeAutomatically = preferences.getBoolean(
|
closeAutomatically = preferences.getBoolean(
|
||||||
CLOSE_AUTOMATICALLY,
|
CLOSE_AUTOMATICALLY,
|
||||||
@@ -301,8 +431,67 @@ class Preferences {
|
|||||||
EXPAND_ESCAPE_SEQUENCES,
|
EXPAND_ESCAPE_SEQUENCES,
|
||||||
expandEscapeSequences
|
expandEscapeSequences
|
||||||
)
|
)
|
||||||
|
addQuietZone = preferences.getBoolean(ADD_QUIET_ZONE, addQuietZone)
|
||||||
|
brightenScreen = preferences.getBoolean(
|
||||||
|
BRIGHTEN_SCREEN,
|
||||||
|
brightenScreen
|
||||||
|
)
|
||||||
|
previewScale = preferences.getFloat(
|
||||||
|
PREVIEW_SCALE,
|
||||||
|
previewScale
|
||||||
|
)
|
||||||
|
automatedActions = fromJsonArray(
|
||||||
|
preferences.getString(AUTOMATED_ACTIONS, "[]") ?: "[]"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun setAutomatedActions(actions: List<AutomatedAction>) {
|
||||||
|
automatedActions = actions.toMutableList()
|
||||||
|
apply(AUTOMATED_ACTIONS, toJsonArray(actions))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setIgnoreCodes(patterns: List<IgnoreCode>) {
|
||||||
|
ignoreCodes = patterns.toMutableList()
|
||||||
|
apply(IGNORE_CODES, ignoreCodesToJsonArray(patterns))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun shouldIgnoreHistoryContent(content: String, format: String): Boolean =
|
||||||
|
ignoreCodes.any { it.matches(content, format) }
|
||||||
|
|
||||||
|
private fun addFormatsOnUpdate(
|
||||||
|
restored: Set<String>,
|
||||||
|
vararg formats: BarcodeFormat
|
||||||
|
) = restored.toMutableSet().apply {
|
||||||
|
for (format in formats) {
|
||||||
|
val name = "${format.name}_added"
|
||||||
|
if (!preferences.getBoolean(name, false)) {
|
||||||
|
preferences.edit {
|
||||||
|
putBoolean(name, true)
|
||||||
|
}
|
||||||
|
add(format.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun migrateBarcodeFormats(formats: Set<String>): Set<String> {
|
||||||
|
return formats.mapTo(mutableSetOf()) { format ->
|
||||||
|
format.migrateBarcodeFormatName()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun restoreCropHandle(
|
||||||
|
name: String,
|
||||||
|
viewWidth: Int,
|
||||||
|
viewHeight: Int
|
||||||
|
) = preferences.restoreCropHandle(name, viewWidth, viewHeight)
|
||||||
|
|
||||||
|
fun storeCropHandle(
|
||||||
|
name: String,
|
||||||
|
viewWidth: Int,
|
||||||
|
viewHeight: Int,
|
||||||
|
cropHandle: Point
|
||||||
|
) = preferences.storeCropHandle(name, viewWidth, viewHeight, cropHandle)
|
||||||
|
|
||||||
fun beepTone() = when (beepToneName) {
|
fun beepTone() = when (beepToneName) {
|
||||||
"tone_cdma_confirm" -> ToneGenerator.TONE_CDMA_CONFIRM
|
"tone_cdma_confirm" -> ToneGenerator.TONE_CDMA_CONFIRM
|
||||||
"tone_sup_radio_ack" -> ToneGenerator.TONE_SUP_RADIO_ACK
|
"tone_sup_radio_ack" -> ToneGenerator.TONE_SUP_RADIO_ACK
|
||||||
@@ -312,35 +501,59 @@ class Preferences {
|
|||||||
else -> ToneGenerator.TONE_PROP_BEEP
|
else -> ToneGenerator.TONE_PROP_BEEP
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun put(label: String, value: Boolean) =
|
fun ignoreDuplicates() = when (ignoreDuplicatesName) {
|
||||||
preferences.edit().putBoolean(label, value)
|
"ignore_consecutive_duplicates" -> IgnoreDuplicates.Consecutive
|
||||||
|
"ignore_any_duplicates" -> IgnoreDuplicates.Any
|
||||||
private fun put(label: String, value: String) =
|
else -> IgnoreDuplicates.Never
|
||||||
preferences.edit().putString(label, value)
|
}
|
||||||
|
|
||||||
private fun apply(label: String, value: Boolean) {
|
private fun apply(label: String, value: Boolean) {
|
||||||
put(label, value).apply()
|
preferences.edit {
|
||||||
|
putBoolean(label, value)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun apply(label: String, value: String) {
|
private fun apply(label: String, value: String) {
|
||||||
preferences.edit().putString(label, value).apply()
|
preferences.edit {
|
||||||
|
putString(label, value)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun apply(label: String, value: Int) {
|
private fun apply(label: String, value: Int) {
|
||||||
preferences.edit().putInt(label, value).apply()
|
preferences.edit {
|
||||||
|
putInt(label, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun apply(label: String, value: Float) {
|
||||||
|
preferences.edit {
|
||||||
|
putFloat(label, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("ApplySharedPref")
|
||||||
|
private fun commit(label: String, value: String) {
|
||||||
|
preferences.edit(true) {
|
||||||
|
putString(label, value)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.HONEYCOMB)
|
|
||||||
private fun apply(label: String, value: Set<String>) {
|
private fun apply(label: String, value: Set<String>) {
|
||||||
preferences.edit().putStringSet(label, value).apply()
|
preferences.edit {
|
||||||
|
putStringSet(label, value)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
enum class IgnoreDuplicates {
|
||||||
|
Consecutive, Any, Never
|
||||||
|
}
|
||||||
|
|
||||||
|
private const val PROFILES = "profiles"
|
||||||
|
private const val PROFILE = "profile"
|
||||||
private const val BARCODE_FORMATS = "formats"
|
private const val BARCODE_FORMATS = "formats"
|
||||||
private const val CROP_HANDLE_X = "crop_handle_x"
|
|
||||||
private const val CROP_HANDLE_Y = "crop_handle_y"
|
|
||||||
private const val CROP_HANDLE_ORIENTATION = "crop_handle_orientation"
|
|
||||||
private const val SHOW_CROP_HANDLE = "show_crop_handle"
|
private const val SHOW_CROP_HANDLE = "show_crop_handle"
|
||||||
|
private const val SHOW_CROSSHAIRS = "show_crosshairs"
|
||||||
private const val ZOOM_BY_SWIPING = "zoom_by_swiping"
|
private const val ZOOM_BY_SWIPING = "zoom_by_swiping"
|
||||||
private const val AUTO_ROTATE = "auto_rotate"
|
private const val AUTO_ROTATE = "auto_rotate"
|
||||||
private const val TRY_HARDER = "try_harder"
|
private const val TRY_HARDER = "try_harder"
|
||||||
@@ -351,11 +564,13 @@ class Preferences {
|
|||||||
private const val BEEP = "beep"
|
private const val BEEP = "beep"
|
||||||
private const val BEEP_TONE_NAME = "beep_tone_name"
|
private const val BEEP_TONE_NAME = "beep_tone_name"
|
||||||
private const val USE_HISTORY = "use_history"
|
private const val USE_HISTORY = "use_history"
|
||||||
private const val IGNORE_CONSECUTIVE_DUPLICATES = "ignore_consecutive_duplicates"
|
private const val IGNORE_DUPLICATES_NAME = "ignore_duplicates_name"
|
||||||
|
private const val IGNORE_CODES = "ignore_codes"
|
||||||
private const val OPEN_IMMEDIATELY = "open_immediately"
|
private const val OPEN_IMMEDIATELY = "open_immediately"
|
||||||
private const val COPY_IMMEDIATELY = "copy_immediately"
|
private const val COPY_IMMEDIATELY = "copy_immediately"
|
||||||
private const val SHOW_META_DATA = "show_meta_data"
|
private const val SHOW_META_DATA = "show_meta_data"
|
||||||
private const val SHOW_HEX_DUMP = "show_hex_dump"
|
private const val SHOW_HEX_DUMP = "show_hex_dump"
|
||||||
|
private const val SHOW_CHECKSUM = "show_checksum"
|
||||||
private const val SHOW_RECREATION = "show_recreation"
|
private const val SHOW_RECREATION = "show_recreation"
|
||||||
private const val CLOSE_AUTOMATICALLY = "close_automatically"
|
private const val CLOSE_AUTOMATICALLY = "close_automatically"
|
||||||
private const val DEFAULT_SEARCH_URL = "default_search_url"
|
private const val DEFAULT_SEARCH_URL = "default_search_url"
|
||||||
@@ -370,5 +585,10 @@ class Preferences {
|
|||||||
private const val INDEX_OF_LAST_SELECTED_EC_LEVEL = "index_of_last_selected_ec_level"
|
private const val INDEX_OF_LAST_SELECTED_EC_LEVEL = "index_of_last_selected_ec_level"
|
||||||
private const val FREE_ROTATION = "free_rotation"
|
private const val FREE_ROTATION = "free_rotation"
|
||||||
private const val EXPAND_ESCAPE_SEQUENCES = "expand_escape_sequences"
|
private const val EXPAND_ESCAPE_SEQUENCES = "expand_escape_sequences"
|
||||||
|
private const val ADD_QUIET_ZONE = "add_quiet_zone"
|
||||||
|
private const val BRIGHTEN_SCREEN = "brighten_screen"
|
||||||
|
private const val PREVIEW_SCALE = "preview_scale"
|
||||||
|
private const val AUTOMATED_ACTIONS = "automated_actions"
|
||||||
|
private const val DEFAULT_IGNORE_CODE_PATTERN = "^FIDO://.*"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
package de.markusfisch.android.binaryeye.preference
|
package de.markusfisch.android.binaryeye.preference
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.support.v7.preference.DialogPreference
|
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
|
import androidx.preference.DialogPreference
|
||||||
import de.markusfisch.android.binaryeye.R
|
import de.markusfisch.android.binaryeye.R
|
||||||
|
|
||||||
class UrlPreference(
|
class UrlPreference(
|
||||||
context: Context?,
|
context: Context,
|
||||||
attrs: AttributeSet?
|
attrs: AttributeSet?
|
||||||
) : DialogPreference(context, attrs) {
|
) : DialogPreference(context, attrs) {
|
||||||
private var url: String? = null
|
private var url: String? = null
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
package de.markusfisch.android.binaryeye.service
|
package de.markusfisch.android.binaryeye.service
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.app.PendingIntent
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.service.quicksettings.TileService
|
import android.service.quicksettings.TileService
|
||||||
import android.support.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import de.markusfisch.android.binaryeye.activity.CameraActivity
|
import de.markusfisch.android.binaryeye.activity.CameraActivity
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.N)
|
@RequiresApi(Build.VERSION_CODES.N)
|
||||||
@@ -12,6 +14,21 @@ class ScanTileService : TileService() {
|
|||||||
super.onClick()
|
super.onClick()
|
||||||
val intent = Intent(applicationContext, CameraActivity::class.java)
|
val intent = Intent(applicationContext, CameraActivity::class.java)
|
||||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||||
startActivityAndCollapse(intent)
|
// TileService#startActivityAndCollapse(Intent) is only ever run
|
||||||
|
// below UpsideDownCake, but Lint doesn't understand this yet.
|
||||||
|
@SuppressLint("StartActivityAndCollapseDeprecated")
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
startActivityAndCollapse(intent)
|
||||||
|
} else {
|
||||||
|
startActivityAndCollapse(
|
||||||
|
PendingIntent.getActivity(
|
||||||
|
this,
|
||||||
|
0,
|
||||||
|
intent,
|
||||||
|
PendingIntent.FLAG_IMMUTABLE
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ fun String.unescape(): String {
|
|||||||
i += l
|
i += l
|
||||||
substring(from, i).toInt(8).toChar()
|
substring(from, i).toInt(8).toChar()
|
||||||
}
|
}
|
||||||
|
|
||||||
'x', 'u' -> {
|
'x', 'u' -> {
|
||||||
val l = if (ec == 'u') 4 else 2
|
val l = if (ec == 'u') 4 else 2
|
||||||
val hexCode = try {
|
val hexCode = try {
|
||||||
@@ -60,6 +61,7 @@ fun String.unescape(): String {
|
|||||||
i += l
|
i += l
|
||||||
unicodeValue.toChar()
|
unicodeValue.toChar()
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> throw IllegalArgumentException(
|
else -> throw IllegalArgumentException(
|
||||||
"Unknown escape code [$ec] at index $i"
|
"Unknown escape code [$ec] at index $i"
|
||||||
)
|
)
|
||||||
@@ -68,7 +70,13 @@ fun String.unescape(): String {
|
|||||||
val sb = StringBuilder()
|
val sb = StringBuilder()
|
||||||
while (i < len) {
|
while (i < len) {
|
||||||
val ch = get(i++)
|
val ch = get(i++)
|
||||||
sb.append(if (ch == '\\' && i < len) expand() else ch)
|
sb.append(
|
||||||
|
if (ch == '\\' && i < len) {
|
||||||
|
expand()
|
||||||
|
} else {
|
||||||
|
ch
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
return sb.toString()
|
return sb.toString()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ private fun Context.isSilent(): Boolean {
|
|||||||
return when (am.ringerMode) {
|
return when (am.ringerMode) {
|
||||||
AudioManager.RINGER_MODE_SILENT,
|
AudioManager.RINGER_MODE_SILENT,
|
||||||
AudioManager.RINGER_MODE_VIBRATE -> true
|
AudioManager.RINGER_MODE_VIBRATE -> true
|
||||||
|
|
||||||
else -> false
|
else -> false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,15 +2,16 @@ package de.markusfisch.android.binaryeye.view
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.ContextWrapper
|
import android.content.ContextWrapper
|
||||||
import android.graphics.drawable.ColorDrawable
|
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.support.v4.content.ContextCompat
|
|
||||||
import android.support.v7.app.AppCompatActivity
|
|
||||||
import android.support.v7.widget.LinearLayoutManager
|
|
||||||
import android.support.v7.widget.RecyclerView
|
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.WindowManager
|
import android.view.WindowManager
|
||||||
import android.widget.AbsListView
|
import android.widget.AbsListView
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.appcompat.widget.Toolbar
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.core.graphics.drawable.toDrawable
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import de.markusfisch.android.binaryeye.R
|
import de.markusfisch.android.binaryeye.R
|
||||||
|
|
||||||
val systemBarListViewScrollListener = object : AbsListView.OnScrollListener {
|
val systemBarListViewScrollListener = object : AbsListView.OnScrollListener {
|
||||||
@@ -25,7 +26,8 @@ val systemBarListViewScrollListener = object : AbsListView.OnScrollListener {
|
|||||||
view.post {
|
view.post {
|
||||||
val scrolled = firstVisibleItem > 0 ||
|
val scrolled = firstVisibleItem > 0 ||
|
||||||
(totalItemCount > 0 && firstChildScrolled(view))
|
(totalItemCount > 0 && firstChildScrolled(view))
|
||||||
val scrollable = scrolled || totalItemCount > 0 && lastChildOutOfView(view)
|
val scrollable = scrolled ||
|
||||||
|
totalItemCount > 0 && lastChildOutOfView(view)
|
||||||
colorSystemAndToolBars(view.context, scrolled, scrollable)
|
colorSystemAndToolBars(view.context, scrolled, scrollable)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -37,9 +39,11 @@ val systemBarListViewScrollListener = object : AbsListView.OnScrollListener {
|
|||||||
val systemBarRecyclerViewScrollListener = object : RecyclerView.OnScrollListener() {
|
val systemBarRecyclerViewScrollListener = object : RecyclerView.OnScrollListener() {
|
||||||
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
||||||
val layoutManager = recyclerView.layoutManager as LinearLayoutManager
|
val layoutManager = recyclerView.layoutManager as LinearLayoutManager
|
||||||
val scrolled = layoutManager.findFirstCompletelyVisibleItemPosition() != 0
|
val scrolled =
|
||||||
val scrollable = scrolled || layoutManager.findLastVisibleItemPosition() <
|
layoutManager.findFirstCompletelyVisibleItemPosition() != 0
|
||||||
recyclerView.adapter.itemCount - 1
|
val scrollable = scrolled ||
|
||||||
|
layoutManager.findLastVisibleItemPosition() <
|
||||||
|
(recyclerView.adapter?.itemCount ?: 0) - 1
|
||||||
colorSystemAndToolBars(recyclerView.context, scrolled, scrollable)
|
colorSystemAndToolBars(recyclerView.context, scrolled, scrollable)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,19 +61,25 @@ private fun lastChildOutOfView(listView: AbsListView): Boolean {
|
|||||||
return child != null && child.bottom >= listView.height
|
return child != null && child.bottom >= listView.height
|
||||||
}
|
}
|
||||||
|
|
||||||
fun initSystemBars(activity: AppCompatActivity) {
|
fun AppCompatActivity.initBars() {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
// Keeps the soft keyboard from repositioning the layout.
|
||||||
// Keeps the soft keyboard from repositioning the layout.
|
window.setSoftInputMode(
|
||||||
val window = activity.window
|
WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE
|
||||||
window.setSoftInputMode(
|
)
|
||||||
WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE
|
window.decorView.systemUiVisibility =
|
||||||
)
|
View.SYSTEM_UI_FLAG_LAYOUT_STABLE or
|
||||||
window.decorView.systemUiVisibility =
|
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or
|
||||||
View.SYSTEM_UI_FLAG_LAYOUT_STABLE or
|
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
|
||||||
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or
|
colorSystemAndToolBars(this)
|
||||||
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
|
setPaddingFromWindowInsets(
|
||||||
}
|
findViewById(R.id.main),
|
||||||
colorSystemAndToolBars(activity)
|
(findViewById(R.id.toolbar) as Toolbar).apply {
|
||||||
|
setSupportActionBar(this)
|
||||||
|
},
|
||||||
|
findViewById<View>(R.id.navbar)?.apply {
|
||||||
|
setBackgroundColor(translucentPrimaryColor)
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private var statusBarColorLocked = false
|
private var statusBarColorLocked = false
|
||||||
@@ -82,7 +92,6 @@ fun unlockStatusBarColor() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private var translucentPrimaryColor = 0
|
private var translucentPrimaryColor = 0
|
||||||
private val actionBarBackground = ColorDrawable()
|
|
||||||
fun colorSystemAndToolBars(
|
fun colorSystemAndToolBars(
|
||||||
context: Context,
|
context: Context,
|
||||||
scrolled: Boolean = false,
|
scrolled: Boolean = false,
|
||||||
@@ -96,27 +105,29 @@ fun colorSystemAndToolBars(
|
|||||||
}
|
}
|
||||||
val topColor = if (scrolled) translucentPrimaryColor else 0
|
val topColor = if (scrolled) translucentPrimaryColor else 0
|
||||||
val activity = getAppCompatActivity(context) ?: return
|
val activity = getAppCompatActivity(context) ?: return
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
// System bars no longer have a background from SDK35+.
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.VANILLA_ICE_CREAM) {
|
||||||
val window = activity.window
|
val window = activity.window
|
||||||
if (!statusBarColorLocked) {
|
if (!statusBarColorLocked) {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
window.statusBarColor = topColor
|
window.statusBarColor = topColor
|
||||||
}
|
}
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
window.navigationBarColor = if (scrolled || scrollable) {
|
window.navigationBarColor = if (scrolled || scrollable) {
|
||||||
translucentPrimaryColor
|
translucentPrimaryColor
|
||||||
} else {
|
} else {
|
||||||
0
|
0
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
activity.supportActionBar?.setBackgroundDrawable(
|
activity.findViewById<View>(R.id.navbar)?.apply {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
|
visibility = if (scrolled || scrollable) {
|
||||||
// Avoid allocation on Honeycomb and better.
|
View.VISIBLE
|
||||||
actionBarBackground.color = topColor
|
} else {
|
||||||
actionBarBackground
|
View.GONE
|
||||||
} else {
|
}
|
||||||
// ColorDrawable.setColor() doesn't exist pre Honeycomb.
|
|
||||||
ColorDrawable(topColor)
|
|
||||||
}
|
}
|
||||||
)
|
}
|
||||||
|
activity.supportActionBar?.setBackgroundDrawable(topColor.toDrawable())
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getAppCompatActivity(context: Context): AppCompatActivity? {
|
private fun getAppCompatActivity(context: Context): AppCompatActivity? {
|
||||||
|
|||||||
@@ -2,15 +2,45 @@ package de.markusfisch.android.binaryeye.view
|
|||||||
|
|
||||||
import android.graphics.Rect
|
import android.graphics.Rect
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.support.annotation.RequiresApi
|
|
||||||
import android.support.v4.view.ViewCompat
|
|
||||||
import android.support.v4.view.WindowInsetsCompat
|
|
||||||
import android.support.v7.widget.Toolbar
|
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
import android.view.WindowInsets
|
||||||
|
import androidx.appcompat.widget.Toolbar
|
||||||
|
import androidx.core.view.ViewCompat
|
||||||
|
import androidx.core.view.WindowInsetsCompat
|
||||||
|
|
||||||
private var toolbarHeight = 0
|
private var toolbarHeight = 0
|
||||||
fun recordToolbarHeight(toolbar: Toolbar) {
|
fun setPaddingFromWindowInsets(
|
||||||
|
mainLayout: View,
|
||||||
|
toolbar: Toolbar,
|
||||||
|
navbar: View?
|
||||||
|
) {
|
||||||
toolbarHeight = toolbar.layoutParams.height
|
toolbarHeight = toolbar.layoutParams.height
|
||||||
|
// SDK 35+ is edge-to-edge by default, so the toolbar needs to extend
|
||||||
|
// below the status bar.
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.VANILLA_ICE_CREAM) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
mainLayout.setOnApplyWindowInsetsListener { _, insets ->
|
||||||
|
val systemBarInsets = insets.getInsets(
|
||||||
|
WindowInsets.Type.systemBars()
|
||||||
|
)
|
||||||
|
|
||||||
|
val statusBarTop = systemBarInsets.top
|
||||||
|
if (toolbar.paddingTop == 0 && statusBarTop > 0) {
|
||||||
|
toolbar.setPadding(0, statusBarTop, 0, 0)
|
||||||
|
toolbar.layoutParams.height += statusBarTop
|
||||||
|
}
|
||||||
|
|
||||||
|
val navBarBottom = systemBarInsets.bottom
|
||||||
|
if (navBarBottom > 0) {
|
||||||
|
mainLayout.setPadding(0, 0, 0, navBarBottom);
|
||||||
|
navbar?.apply {
|
||||||
|
layoutParams.height = navBarBottom
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
insets
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun View.setPaddingFromWindowInsets() {
|
fun View.setPaddingFromWindowInsets() {
|
||||||
@@ -22,19 +52,15 @@ fun View.setPaddingFromWindowInsets() {
|
|||||||
// For the original post see here:
|
// For the original post see here:
|
||||||
// https://medium.com/androiddevelopers/windowinsets-listeners-to-layouts-8f9ccc8fa4d1
|
// https://medium.com/androiddevelopers/windowinsets-listeners-to-layouts-8f9ccc8fa4d1
|
||||||
fun View.doOnApplyWindowInsets(f: (View, Rect) -> Unit) {
|
fun View.doOnApplyWindowInsets(f: (View, Rect) -> Unit) {
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
|
ViewCompat.setOnApplyWindowInsetsListener(this) { v, insets ->
|
||||||
f(this, insetsWithToolbar())
|
f(v, insetsWithToolbar(insets))
|
||||||
} else {
|
insets
|
||||||
ViewCompat.setOnApplyWindowInsetsListener(this) { v, insets ->
|
|
||||||
f(v, insetsWithToolbar(insets))
|
|
||||||
insets
|
|
||||||
}
|
|
||||||
// It's important to explicitly request the insets (again) in
|
|
||||||
// case the view was created in Fragment.onCreateView() because
|
|
||||||
// setOnApplyWindowInsetsListener() won't fire when the view
|
|
||||||
// isn't attached.
|
|
||||||
requestApplyInsetsWhenAttached()
|
|
||||||
}
|
}
|
||||||
|
// It's important to explicitly request the insets (again) in
|
||||||
|
// case the view was created in Fragment.onCreateView() because
|
||||||
|
// setOnApplyWindowInsetsListener() won't fire when the view
|
||||||
|
// isn't attached.
|
||||||
|
requestApplyInsetsWhenAttached()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun insetsWithToolbar(insets: WindowInsetsCompat? = null) = Rect(
|
private fun insetsWithToolbar(insets: WindowInsetsCompat? = null) = Rect(
|
||||||
@@ -44,7 +70,6 @@ private fun insetsWithToolbar(insets: WindowInsetsCompat? = null) = Rect(
|
|||||||
insets?.systemWindowInsetBottom ?: 0
|
insets?.systemWindowInsetBottom ?: 0
|
||||||
)
|
)
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.KITKAT)
|
|
||||||
private fun View.requestApplyInsetsWhenAttached() {
|
private fun View.requestApplyInsetsWhenAttached() {
|
||||||
if (isAttachedToWindow) {
|
if (isAttachedToWindow) {
|
||||||
ViewCompat.requestApplyInsets(this)
|
ViewCompat.requestApplyInsets(this)
|
||||||
|
|||||||
+5
@@ -10,6 +10,8 @@ import de.markusfisch.android.scalingimageview.widget.ScalingImageView
|
|||||||
open class ConfinedScalingImageView : ScalingImageView {
|
open class ConfinedScalingImageView : ScalingImageView {
|
||||||
val insets = Rect()
|
val insets = Rect()
|
||||||
|
|
||||||
|
var runAfterLayout: (() -> Unit)? = null
|
||||||
|
|
||||||
constructor(context: Context, attrs: AttributeSet, defStyle: Int) :
|
constructor(context: Context, attrs: AttributeSet, defStyle: Int) :
|
||||||
super(context, attrs, defStyle)
|
super(context, attrs, defStyle)
|
||||||
|
|
||||||
@@ -37,5 +39,8 @@ open class ConfinedScalingImageView : ScalingImageView {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
centerRemap()
|
centerRemap()
|
||||||
|
runAfterLayout?.invoke()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun isInBounds(): Boolean = inBounds()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,7 +27,8 @@ class ConfinedScrollView : ScrollView {
|
|||||||
// not putting it on the queue makes it only work sometimes.
|
// not putting it on the queue makes it only work sometimes.
|
||||||
post {
|
post {
|
||||||
getChildAt(0)?.also { child ->
|
getChildAt(0)?.also { child ->
|
||||||
scrollable = height < child.height + paddingTop + paddingBottom
|
scrollable = height <
|
||||||
|
child.height + paddingTop + paddingBottom
|
||||||
colorSystemAndToolBars(
|
colorSystemAndToolBars(
|
||||||
context,
|
context,
|
||||||
scrollY > 0,
|
scrollY > 0,
|
||||||
|
|||||||
@@ -2,18 +2,21 @@ package de.markusfisch.android.binaryeye.widget
|
|||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.*
|
import android.graphics.Canvas
|
||||||
|
import android.graphics.Paint
|
||||||
|
import android.graphics.Path
|
||||||
|
import android.graphics.Point
|
||||||
|
import android.graphics.Rect
|
||||||
|
import android.graphics.Region
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Parcel
|
|
||||||
import android.os.Parcelable
|
|
||||||
import android.support.v4.content.ContextCompat
|
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
import android.view.MotionEvent
|
import android.view.MotionEvent
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.core.graphics.withSave
|
||||||
import de.markusfisch.android.binaryeye.R
|
import de.markusfisch.android.binaryeye.R
|
||||||
import de.markusfisch.android.binaryeye.app.prefs
|
import de.markusfisch.android.binaryeye.app.prefs
|
||||||
import de.markusfisch.android.binaryeye.graphics.getBitmapFromDrawable
|
import de.markusfisch.android.binaryeye.graphics.getBitmapFromDrawable
|
||||||
import de.markusfisch.android.binaryeye.graphics.getDashedBorderPaint
|
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
@@ -23,26 +26,31 @@ class DetectorView : View {
|
|||||||
val coordinates = FloatArray(32)
|
val coordinates = FloatArray(32)
|
||||||
val roi = Rect()
|
val roi = Rect()
|
||||||
|
|
||||||
|
var cropHandleName: String? = null
|
||||||
var onRoiChange: (() -> Unit)? = null
|
var onRoiChange: (() -> Unit)? = null
|
||||||
var onRoiChanged: (() -> Unit)? = null
|
var onRoiChanged: (() -> Unit)? = null
|
||||||
|
|
||||||
private val currentOrientation = resources.configuration.orientation
|
|
||||||
private val invalidateRunnable: Runnable = Runnable {
|
private val invalidateRunnable: Runnable = Runnable {
|
||||||
coordinatesLast = 0
|
coordinatesLast = 0
|
||||||
invalidate()
|
invalidate()
|
||||||
}
|
}
|
||||||
private val roiPaint = context.getDashedBorderPaint()
|
|
||||||
private val dotPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
private val dotPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||||
color = ContextCompat.getColor(context, R.color.dot)
|
color = ContextCompat.getColor(context, R.color.dot)
|
||||||
style = Paint.Style.FILL
|
style = Paint.Style.FILL
|
||||||
}
|
}
|
||||||
|
private val crosshairPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||||
|
color = ContextCompat.getColor(context, R.color.dot)
|
||||||
|
style = Paint.Style.STROKE
|
||||||
|
strokeWidth = context.resources.displayMetrics.density
|
||||||
|
}
|
||||||
private val dotRadius: Float
|
private val dotRadius: Float
|
||||||
private val handleBitmap = resources.getBitmapFromDrawable(
|
private val handleBitmap = resources.getBitmapFromDrawable(
|
||||||
R.drawable.button_crop
|
R.drawable.button_crop
|
||||||
)
|
)
|
||||||
private val handleXRadius = handleBitmap.width / 2
|
private val handleXRadius = handleBitmap.width / 2
|
||||||
private val handleYRadius = handleBitmap.height / 2
|
private val handleYRadius = handleBitmap.height / 2
|
||||||
private val handlePos = Point(-1, -1)
|
private val inactiveHandlePos = Point(-1, -1)
|
||||||
|
private val handlePos = Point(inactiveHandlePos)
|
||||||
private val handleHome = Point()
|
private val handleHome = Point()
|
||||||
private val center = Point()
|
private val center = Point()
|
||||||
private val touchDown = Point()
|
private val touchDown = Point()
|
||||||
@@ -52,6 +60,8 @@ class DetectorView : View {
|
|||||||
private val fabHeight: Int
|
private val fabHeight: Int
|
||||||
private val padding: Int
|
private val padding: Int
|
||||||
|
|
||||||
|
private var viewWidth = 0
|
||||||
|
private var viewHeight = 0
|
||||||
private var coordinatesLast = 0
|
private var coordinatesLast = 0
|
||||||
private var handleGrabbed = false
|
private var handleGrabbed = false
|
||||||
private var handleActive = false
|
private var handleActive = false
|
||||||
@@ -78,37 +88,38 @@ class DetectorView : View {
|
|||||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) :
|
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) :
|
||||||
super(context, attrs, defStyleAttr)
|
super(context, attrs, defStyleAttr)
|
||||||
|
|
||||||
fun saveCropHandlePos() {
|
fun storeCropHandlePos() {
|
||||||
val pos = getCropHandlePos()
|
val name = cropHandleName ?: return
|
||||||
prefs.cropHandleX = pos.x
|
val pos = if (handleActive) {
|
||||||
prefs.cropHandleY = pos.y
|
handlePos
|
||||||
prefs.cropHandleOrientation = currentOrientation
|
} else {
|
||||||
}
|
inactiveHandlePos
|
||||||
|
}
|
||||||
private fun getCropHandlePos() = if (handleActive) {
|
prefs.storeCropHandle(
|
||||||
handlePos
|
name,
|
||||||
} else {
|
viewWidth,
|
||||||
Point(-1, -1)
|
viewHeight,
|
||||||
}
|
pos
|
||||||
|
|
||||||
fun restoreCropHandlePos() {
|
|
||||||
setCropHandlePos(
|
|
||||||
prefs.cropHandleX,
|
|
||||||
prefs.cropHandleY,
|
|
||||||
prefs.cropHandleOrientation
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setCropHandlePos(x: Int, y: Int, orientation: Int) {
|
fun updateCropHandlePos() {
|
||||||
// Always set handlePos even if it's invalid because
|
val name = cropHandleName ?: return
|
||||||
// handlePos.x may be -2 which is a signal to set the
|
if (viewWidth < 1 || viewHeight < 1) {
|
||||||
// default ROI.
|
return
|
||||||
if (orientation == currentOrientation) {
|
}
|
||||||
handlePos.set(x, y)
|
val cropHandle = prefs.restoreCropHandle(name, viewWidth, viewHeight)
|
||||||
} else {
|
if (cropHandle.x == -2) {
|
||||||
handlePos.set(y, x)
|
setHandleToDefaultRoi()
|
||||||
|
} else {
|
||||||
|
handlePos.set(cropHandle.x, cropHandle.y)
|
||||||
|
handleActive = handlePos.x > -1
|
||||||
|
}
|
||||||
|
if (handleActive) {
|
||||||
|
updateClipRect()
|
||||||
|
} else {
|
||||||
|
reset()
|
||||||
}
|
}
|
||||||
handleActive = handlePos.x > -1
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun update(numberOfCoordinates: Int) {
|
fun update(numberOfCoordinates: Int) {
|
||||||
@@ -118,31 +129,6 @@ class DetectorView : View {
|
|||||||
postDelayed(invalidateRunnable, 500)
|
postDelayed(invalidateRunnable, 500)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSaveInstanceState(): Parcelable? {
|
|
||||||
if (!handleActive) {
|
|
||||||
return super.onSaveInstanceState()
|
|
||||||
}
|
|
||||||
return SavedState(super.onSaveInstanceState()).apply {
|
|
||||||
savedHandlePos.set(getCropHandlePos())
|
|
||||||
savedOrientation = currentOrientation
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onRestoreInstanceState(state: Parcelable) {
|
|
||||||
super.onRestoreInstanceState(
|
|
||||||
if (state is SavedState) {
|
|
||||||
setCropHandlePos(
|
|
||||||
state.savedHandlePos.x,
|
|
||||||
state.savedHandlePos.y,
|
|
||||||
state.savedOrientation
|
|
||||||
)
|
|
||||||
state.superState
|
|
||||||
} else {
|
|
||||||
state
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("ClickableViewAccessibility")
|
@SuppressLint("ClickableViewAccessibility")
|
||||||
override fun onTouchEvent(event: MotionEvent?): Boolean {
|
override fun onTouchEvent(event: MotionEvent?): Boolean {
|
||||||
event ?: return super.onTouchEvent(event)
|
event ?: return super.onTouchEvent(event)
|
||||||
@@ -162,6 +148,7 @@ class DetectorView : View {
|
|||||||
false
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
MotionEvent.ACTION_MOVE -> {
|
MotionEvent.ACTION_MOVE -> {
|
||||||
if (handleGrabbed) {
|
if (handleGrabbed) {
|
||||||
handlePos.set(x, y)
|
handlePos.set(x, y)
|
||||||
@@ -176,6 +163,7 @@ class DetectorView : View {
|
|||||||
false
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
MotionEvent.ACTION_CANCEL -> {
|
MotionEvent.ACTION_CANCEL -> {
|
||||||
if (handleGrabbed) {
|
if (handleGrabbed) {
|
||||||
snap(x, y)
|
snap(x, y)
|
||||||
@@ -183,6 +171,7 @@ class DetectorView : View {
|
|||||||
}
|
}
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
MotionEvent.ACTION_UP -> {
|
MotionEvent.ACTION_UP -> {
|
||||||
if (handleGrabbed) {
|
if (handleGrabbed) {
|
||||||
if (!handleActive) {
|
if (!handleActive) {
|
||||||
@@ -199,6 +188,7 @@ class DetectorView : View {
|
|||||||
}
|
}
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> super.onTouchEvent(event)
|
else -> super.onTouchEvent(event)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -235,34 +225,36 @@ class DetectorView : View {
|
|||||||
roi.set(0, 0, 0, 0)
|
roi.set(0, 0, 0, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
|
override fun onLayout(
|
||||||
|
changed: Boolean,
|
||||||
|
left: Int,
|
||||||
|
top: Int,
|
||||||
|
right: Int,
|
||||||
|
bottom: Int
|
||||||
|
) {
|
||||||
super.onLayout(changed, left, top, right, bottom)
|
super.onLayout(changed, left, top, right, bottom)
|
||||||
val width = right - left
|
viewWidth = right - left
|
||||||
val height = bottom - top
|
viewHeight = bottom - top
|
||||||
center.set(
|
center.set(
|
||||||
left + (width / 2),
|
left + (viewWidth / 2),
|
||||||
top + (height / 2)
|
top + (viewHeight / 2)
|
||||||
)
|
)
|
||||||
minY = padding * 2
|
minY = padding * 2
|
||||||
maxY = height - minY
|
maxY = viewHeight - minY
|
||||||
handleHome.set(
|
handleHome.set(
|
||||||
width - handleXRadius - paddingRight - padding,
|
viewWidth - handleXRadius - paddingRight - padding,
|
||||||
height - handleYRadius - paddingBottom - fabHeight
|
viewHeight - handleYRadius - paddingBottom - fabHeight
|
||||||
)
|
)
|
||||||
if (handlePos.x == -2) {
|
updateCropHandlePos()
|
||||||
setHandleToDefaultRoi()
|
|
||||||
}
|
|
||||||
if (handleActive) {
|
|
||||||
updateClipRect()
|
|
||||||
} else {
|
|
||||||
reset()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDraw(canvas: Canvas) {
|
override fun onDraw(canvas: Canvas) {
|
||||||
if (handleActive) {
|
if (handleActive) {
|
||||||
canvas.drawClip()
|
canvas.drawClip()
|
||||||
}
|
}
|
||||||
|
if (prefs.showCrosshairs) {
|
||||||
|
canvas.drawCrosshairs()
|
||||||
|
}
|
||||||
canvas.drawDots()
|
canvas.drawDots()
|
||||||
if (prefs.showCropHandle) {
|
if (prefs.showCropHandle) {
|
||||||
canvas.drawHandle()
|
canvas.drawHandle()
|
||||||
@@ -285,10 +277,8 @@ class DetectorView : View {
|
|||||||
if (minDist < 1) {
|
if (minDist < 1) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// canvas.clipRect() doesn't work reliably below KITKAT.
|
val radius = min(minDist / 2, cornerRadius).toFloat()
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
withSave {
|
||||||
val radius = min(minDist / 2, cornerRadius).toFloat()
|
|
||||||
save()
|
|
||||||
clipOutPathCompat(
|
clipOutPathCompat(
|
||||||
calculateRoundedRectPath(
|
calculateRoundedRectPath(
|
||||||
roi.left.toFloat(),
|
roi.left.toFloat(),
|
||||||
@@ -300,9 +290,6 @@ class DetectorView : View {
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
drawColor(shadeColor)
|
drawColor(shadeColor)
|
||||||
restore()
|
|
||||||
} else {
|
|
||||||
drawRect(roi, roiPaint)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -315,6 +302,28 @@ class DetectorView : View {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun Canvas.drawCrosshairs() {
|
||||||
|
val left: Float
|
||||||
|
val top: Float
|
||||||
|
val right: Float
|
||||||
|
val bottom: Float
|
||||||
|
if (handleActive) {
|
||||||
|
left = roi.left.toFloat()
|
||||||
|
top = roi.top.toFloat()
|
||||||
|
right = roi.right.toFloat()
|
||||||
|
bottom = roi.bottom.toFloat()
|
||||||
|
} else {
|
||||||
|
left = 0f
|
||||||
|
top = 0f
|
||||||
|
right = viewWidth.toFloat()
|
||||||
|
bottom = viewHeight.toFloat()
|
||||||
|
}
|
||||||
|
val cx = (left + right) * .5f
|
||||||
|
val cy = (top + bottom) * .5f
|
||||||
|
drawLine(cx, top, cx, bottom, crosshairPaint)
|
||||||
|
drawLine(left, cy, right, cy, crosshairPaint)
|
||||||
|
}
|
||||||
|
|
||||||
private fun updateClipRect() {
|
private fun updateClipRect() {
|
||||||
clampHandlePos()
|
clampHandlePos()
|
||||||
val dx = abs(handlePos.x - center.x)
|
val dx = abs(handlePos.x - center.x)
|
||||||
@@ -340,37 +349,6 @@ class DetectorView : View {
|
|||||||
private fun clampX(x: Int) = min(center.x * 2, max(0, x))
|
private fun clampX(x: Int) = min(center.x * 2, max(0, x))
|
||||||
|
|
||||||
private fun clampY(y: Int) = min(maxY, max(minY, y))
|
private fun clampY(y: Int) = min(maxY, max(minY, y))
|
||||||
|
|
||||||
internal class SavedState : BaseSavedState {
|
|
||||||
val savedHandlePos = Point()
|
|
||||||
|
|
||||||
var savedOrientation = 0
|
|
||||||
|
|
||||||
constructor(superState: Parcelable?) : super(superState)
|
|
||||||
|
|
||||||
private constructor(parcel: Parcel) : super(parcel) {
|
|
||||||
savedHandlePos.set(
|
|
||||||
parcel.readInt(),
|
|
||||||
parcel.readInt()
|
|
||||||
)
|
|
||||||
savedOrientation = parcel.readInt()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun writeToParcel(out: Parcel, flags: Int) {
|
|
||||||
super.writeToParcel(out, flags)
|
|
||||||
out.writeInt(savedHandlePos.x)
|
|
||||||
out.writeInt(savedHandlePos.y)
|
|
||||||
out.writeInt(savedOrientation)
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
@JvmField
|
|
||||||
val CREATOR = object : Parcelable.Creator<SavedState> {
|
|
||||||
override fun createFromParcel(source: Parcel) = SavedState(source)
|
|
||||||
override fun newArray(size: Int): Array<SavedState?> = arrayOfNulls(size)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun distSq(a: Point, b: Point): Int {
|
private fun distSq(a: Point, b: Point): Int {
|
||||||
|
|||||||
@@ -3,19 +3,29 @@ package de.markusfisch.android.binaryeye.widget
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
|
|
||||||
fun Context.toast(message: Int) = Toast.makeText(
|
private var lastToast: Toast? = null
|
||||||
applicationContext,
|
|
||||||
message,
|
|
||||||
Toast.LENGTH_LONG
|
|
||||||
).show()
|
|
||||||
|
|
||||||
fun Context.toast(message: String) = Toast.makeText(
|
fun <T> Context.toast(message: T) {
|
||||||
applicationContext,
|
lastToast?.cancel()
|
||||||
message.ellipsize(128),
|
lastToast = when (message) {
|
||||||
Toast.LENGTH_LONG
|
is String -> Toast.makeText(
|
||||||
).show()
|
applicationContext,
|
||||||
|
message.ellipsize(),
|
||||||
|
Toast.LENGTH_LONG
|
||||||
|
)
|
||||||
|
|
||||||
private fun String.ellipsize(max: Int) = if (length < max) {
|
is Int -> Toast.makeText(
|
||||||
|
applicationContext,
|
||||||
|
message,
|
||||||
|
Toast.LENGTH_LONG
|
||||||
|
)
|
||||||
|
|
||||||
|
else -> return
|
||||||
|
}
|
||||||
|
lastToast?.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun String.ellipsize(max: Int = 128) = if (length < max) {
|
||||||
this
|
this
|
||||||
} else {
|
} else {
|
||||||
"${take(max)}…"
|
"${take(max)}…"
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package de.markusfisch.android.binaryeye.zxingcpp
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Translate legacy barcode format names from ZXingC++ before 3.x
|
||||||
|
* to current BarcodeFormat enum names.
|
||||||
|
*/
|
||||||
|
fun String.migrateBarcodeFormatName() = when (this) {
|
||||||
|
"NONE" -> "None"
|
||||||
|
"AZTEC" -> "Aztec"
|
||||||
|
"CODABAR" -> "Codabar"
|
||||||
|
"CODE_39" -> "Code39"
|
||||||
|
"CODE_39_EXT" -> "Code39Ext"
|
||||||
|
"CODE_32" -> "Code32"
|
||||||
|
"PZN" -> "PZN"
|
||||||
|
"CODE_93" -> "Code93"
|
||||||
|
"CODE_128" -> "Code128"
|
||||||
|
"DATA_BAR" -> "DataBar"
|
||||||
|
"DATA_BAR_OMNI" -> "DataBarOmni"
|
||||||
|
"DATA_BAR_STK" -> "DataBarStk"
|
||||||
|
"DATA_BAR_LTD" -> "DataBarLtd"
|
||||||
|
"DATA_BAR_EXPANDED" -> "DataBarExp"
|
||||||
|
"DATA_BAR_EXP_STK" -> "DataBarExpStk"
|
||||||
|
"DATA_MATRIX" -> "DataMatrix"
|
||||||
|
"DX_FILM_EDGE" -> "DXFilmEdge"
|
||||||
|
"EAN_8" -> "EAN8"
|
||||||
|
"EAN_13" -> "EAN13"
|
||||||
|
"ITF" -> "ITF"
|
||||||
|
"MAXICODE" -> "MaxiCode"
|
||||||
|
"PDF_417" -> "PDF417"
|
||||||
|
"QR_CODE" -> "QRCode"
|
||||||
|
"MICRO_QR_CODE" -> "MicroQRCode"
|
||||||
|
"RMQR_CODE" -> "RMQRCode"
|
||||||
|
"UPC_A" -> "UPCA"
|
||||||
|
"UPC_E" -> "UPCE"
|
||||||
|
else -> this
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<background android:drawable="@color/accent"/>
|
||||||
|
<foreground android:drawable="@drawable/ic_shortcut_history_foreground"/>
|
||||||
|
</adaptive-icon>
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 835 B |
Binary file not shown.
|
After Width: | Height: | Size: 471 B |
Binary file not shown.
|
After Width: | Height: | Size: 590 B |
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.6 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 2.2 KiB |
@@ -0,0 +1,16 @@
|
|||||||
|
<selector
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:constantSize="true">
|
||||||
|
<item android:state_pressed="false">
|
||||||
|
<shape android:shape="rectangle">
|
||||||
|
<corners android:radius="2dp"/>
|
||||||
|
<solid android:color="@color/selected_row"/>
|
||||||
|
</shape>
|
||||||
|
</item>
|
||||||
|
<item android:state_pressed="true">
|
||||||
|
<shape android:shape="rectangle">
|
||||||
|
<corners android:radius="2dp"/>
|
||||||
|
<solid android:color="@color/pressed_row"/>
|
||||||
|
</shape>
|
||||||
|
</item>
|
||||||
|
</selector>
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:autoMirrored="true" android:height="24dp" android:tint="#FFFFFF" android:viewportHeight="960" android:viewportWidth="960" android:width="24dp">
|
||||||
|
<path android:fillColor="@android:color/white" android:pathData="M313 520l224 224-57 56-320-320 320-320 57 56-224 224h487v80H313z"/>
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#FFFFFF" android:viewportHeight="960" android:viewportWidth="960" android:width="24dp">
|
||||||
|
<path android:fillColor="@android:color/white" android:pathData="M480 340q13 0 21.5-9t8.5-21q0-13-8.5-21.5T480 280q-12 0-21 8.5t-9 21.5q0 12 9 21t21 9zM172 595q-15-26-23.5-54.5T140 480q0-32 8.5-60.5T172 365l64 50q-8 15-12 31t-4 34q0 18 4 34t12 31l-64 50zM62 681q-29-44-45.5-94.5T0 480q0-56 16.5-106.5T62 279l63 50q-21 33-33 71t-12 80q0 42 12 80.5t33 71.5l-63 49zm298 119q-33 0-56.5-23.5T280 720V240q0-33 23.5-56.5T360 160h240q33 0 56.5 23.5T680 240v480q0 33-23.5 56.5T600 800H360zm0-80h240V240H360v480zm427-125l-64-50q8-15 12.5-31t4.5-34q0-18-4.5-34T723 415l64-50q15 26 24 54.5t9 60.5q0 32-9 60.5T787 595zm111 85l-63-49q21-33 33-71t12-80q0-42-12-80.5T835 328l63-49q29 43 45.5 93.5T960 479q0 56-16.5 106.5T898 680zm-538 40V240v480z"/>
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#FFFFFF" android:viewportHeight="960" android:viewportWidth="960" android:width="24dp">
|
||||||
|
<path android:fillColor="@android:color/white" android:pathData="M440 880V576L256 760l-56-56 224-224-224-224 56-56 184 184V80h40l228 228-172 172 172 172-228 228h-40zm80-496l76-76-76-74v150zm0 342l76-74-76-76v150z"/>
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#FFFFFF" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
|
||||||
|
<path android:fillColor="#fff" android:pathData="M20 8.69V4h-4.69L12 0.69 8.69 4H4v4.69L0.69 12 4 15.31V20h4.69L12 23.31 15.31 20H20v-4.69L23.31 12 20 8.69zM12 18c-3.31 0-6-2.69-6-6s2.69-6 6-6 6 2.69 6 6-2.69 6-6 6zm0-10c-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4-1.79-4-4-4z"/>
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#FFFFFF" android:viewportHeight="960" android:viewportWidth="960" android:width="24dp">
|
||||||
|
<path android:fillColor="@android:color/white" android:pathData="M680 920V760H280q-33 0-56.5-23.5T200 680V280H40v-80h160V40h80v640h640v80H760v160h-80zm0-320V280H360v-80h320q33 0 56.5 23.5T760 280v320h-80z"/>
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#FFFFFF" android:viewportHeight="960" android:viewportWidth="960" android:width="24dp">
|
||||||
|
<path android:fillColor="@android:color/white" android:pathData="M340.5 811.5Q275 783 226 734t-77.5-114.5Q120 554 120 480q0-44 10-85.5t29-78q19-36.5 45.5-68T264 192l272 272-56 56-216-216q-30 36-47 80.5T200 480q0 116 82 198t198 82q116 0 198-82t82-198q0-107-68.5-184.5T520 204v76h-80V120h40q74 0 139.5 28.5T734 226q49 49 77.5 114.5T840 480q0 74-28.5 139.5T734 734q-49 49-114.5 77.5T480 840q-74 0-139.5-28.5zm-89-303Q240 497 240 480t11.5-28.5Q263 440 280 440t28.5 11.5Q320 463 320 480t-11.5 28.5Q297 520 280 520t-28.5-11.5zm200 200Q440 697 440 680t11.5-28.5Q463 640 480 640t28.5 11.5Q520 663 520 680t-11.5 28.5Q497 720 480 720t-28.5-11.5zm200-200Q640 497 640 480t11.5-28.5Q663 440 680 440t28.5 11.5Q720 463 720 480t-11.5 28.5Q697 520 680 520t-28.5-11.5z"/>
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#FFFFFF" android:viewportHeight="960" android:viewportWidth="960" android:width="24dp">
|
||||||
|
<path android:fillColor="@android:color/white" android:pathData="M440 160v487L216 423l-56 57 320 320 320-320-56-57-224 224V160h-80z"/>
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:autoMirrored="true" android:height="24dp" android:tint="#FFFFFF" android:viewportHeight="960" android:viewportWidth="960" android:width="24dp">
|
||||||
|
<path android:fillColor="@android:color/white" android:pathData="M640 680l-57-56 184-184-184-184 57-56 240 240-240 240zM80 760V600q0-83 58.5-141.5T280 400h247L383 256l57-56 240 240-240 240-57-56 144-144H280q-50 0-85 35t-35 85v160H80z"/>
|
||||||
|
</vector>
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user