Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7120e18670 | ||
|
|
9fb34965f5 | ||
|
|
c5cbd1140b | ||
|
|
fecdcd7deb | ||
|
|
fd3d2f9934 | ||
|
|
35b6ead662 | ||
|
|
ad62cb5a03 | ||
|
|
e1905b1ff2 | ||
|
|
9aecf0d83b | ||
|
|
2645366baa | ||
|
|
37ea4ac9af | ||
|
|
4a6f5c5f92 | ||
|
|
b1ddb0fe7a | ||
|
|
2f61db731d | ||
|
|
b0de90c702 | ||
|
|
8df09090c9 | ||
|
|
9feef33275 | ||
|
|
147ad22df0 | ||
|
|
be833b7645 | ||
|
|
0eb921d579 | ||
|
|
1d156e0dbd | ||
|
|
82c1225848 | ||
|
|
33c1697d80 | ||
|
|
1d4e5d25ae | ||
|
|
386b47fbed | ||
|
|
b2c2632d76 | ||
|
|
4d64a5704d | ||
|
|
8875937022 | ||
|
|
3d33f62d7b | ||
|
|
a4050cbf8f | ||
|
|
abb362230e | ||
|
|
967cab9c7b | ||
|
|
c8d7be95e9 | ||
|
|
18146ec41a | ||
|
|
5cd6114ba1 | ||
|
|
8a310a1144 | ||
|
|
afc597ab90 | ||
|
|
6d742c4d19 | ||
|
|
1776559d40 | ||
|
|
75c1eb6b64 | ||
|
|
c5cffc00c3 | ||
|
|
c0153e822c | ||
|
|
f070269c2c | ||
|
|
3e8df89db1 | ||
|
|
56237c43cd | ||
|
|
7fc565ae67 | ||
|
|
656c1e36a8 | ||
|
|
42a6043f63 | ||
|
|
6a5fdae584 | ||
|
|
899deb77b5 | ||
|
|
3419194bfd | ||
|
|
3b55d48f65 | ||
|
|
d34673ddc3 | ||
|
|
242bf4c0a1 | ||
|
|
ed2a12eef8 | ||
|
|
4dd5c12e85 | ||
|
|
d3fae01786 | ||
|
|
37b6e6f832 | ||
|
|
e3274fd065 | ||
|
|
c75b9f73ab | ||
|
|
1432669aee | ||
|
|
00a8a94b59 | ||
|
|
c8c3da91ae | ||
|
|
8cbc73b7f8 | ||
|
|
941d875b3e | ||
|
|
1ac578845e | ||
|
|
e816e8bf89 | ||
|
|
54c368fcd8 | ||
|
|
c225e7d9eb | ||
|
|
6a0ce45a65 | ||
|
|
11c97cc4ef | ||
|
|
e3f6732bb8 | ||
|
|
53ae706b9c | ||
|
|
26a7ad2e6e | ||
|
|
9c3984ce7a | ||
|
|
f7d07e0b8a | ||
|
|
3fe0f50289 | ||
|
|
e86cf9d2cc | ||
|
|
4f1482d2c9 | ||
|
|
c42ad7dbea | ||
|
|
fa3cc8c408 | ||
|
|
e4863bad4c | ||
|
|
484bbbc997 | ||
|
|
565643f19e | ||
|
|
4c92045563 | ||
|
|
7116aa7278 | ||
|
|
860e867ad4 | ||
|
|
3b509cd09c | ||
|
|
51ec228fd8 | ||
|
|
9d1dec146d | ||
|
|
7bd9fe36fe | ||
|
|
85dba0604c | ||
|
|
56c70f5974 | ||
|
|
098b7740a0 | ||
|
|
7d92481fa6 | ||
|
|
2e285a5b1d | ||
|
|
ad25c4681f | ||
|
|
c00c87ceca | ||
|
|
e001a92870 | ||
|
|
7a32b00e1f | ||
|
|
50cedf782d | ||
|
|
fb0d22198f | ||
|
|
af528df448 | ||
|
|
a9b5771d57 | ||
|
|
a161e8fc0f | ||
|
|
ec17d07695 | ||
|
|
be2fc2f559 | ||
|
|
de3aa752ff | ||
|
|
411cbb243f | ||
|
|
f76c1521fe | ||
|
|
9b9224b100 | ||
|
|
62d3fd7e94 | ||
|
|
acdac4ca30 | ||
|
|
221cf11576 | ||
|
|
d34fceb48f | ||
|
|
4cc4cef4ca | ||
|
|
969ffd3d0a | ||
|
|
81a505c752 | ||
|
|
eafcf0dba6 | ||
|
|
0539468d3d | ||
|
|
5e9361ac77 | ||
|
|
54fb31d6a5 | ||
|
|
0f7da91e77 | ||
|
|
13bbb3e435 | ||
|
|
e318385032 | ||
|
|
9b1445efb5 | ||
|
|
9a74098f11 | ||
|
|
e22d242248 | ||
|
|
77d419aa7b | ||
|
|
b2fbdceee4 | ||
|
|
8a625dc5d0 | ||
|
|
6d9c6aa53f | ||
|
|
6b03935b9a | ||
|
|
fcac0add8b | ||
|
|
f63b78cc28 | ||
|
|
f34194c5b5 | ||
|
|
f2a22d2312 | ||
|
|
746bcc0706 | ||
|
|
2525b693c7 | ||
|
|
ba9970eb4d | ||
|
|
0495985518 | ||
|
|
b662ba9307 | ||
|
|
c4fe713b6d | ||
|
|
3609c377d2 | ||
|
|
8043ab7eff | ||
|
|
4f7b392611 | ||
|
|
27fb611f4e | ||
|
|
3c693c7bd5 | ||
|
|
b2cc1c3c34 | ||
|
|
e2effb613f | ||
|
|
ae422d9a02 | ||
|
|
ace841814a | ||
|
|
21c3206464 | ||
|
|
f392baca8b | ||
|
|
4cd0fb6418 | ||
|
|
5b948a4c8f | ||
|
|
a75aff2e64 | ||
|
|
a49a49f1d2 | ||
|
|
6db46f1d0d | ||
|
|
1c0062df8b | ||
|
|
d776688d26 | ||
|
|
9856aa9efa | ||
|
|
27ce6cb12b | ||
|
|
9d3a2b8479 | ||
|
|
74f0c050c0 | ||
|
|
af92d1db17 | ||
|
|
307401087e | ||
|
|
a36fd668a9 | ||
|
|
7e022249aa | ||
|
|
d464cc26b0 | ||
|
|
45eb24036f | ||
|
|
ea0212c58e | ||
|
|
170a9944ef | ||
|
|
ccfd356c67 | ||
|
|
91aa1d343e | ||
|
|
b64b3c5c01 | ||
|
|
dfb6c07930 | ||
|
|
6ca93f2502 | ||
|
|
e867651acd | ||
|
|
607e1324ed | ||
|
|
256183b4aa | ||
|
|
733b6bb123 | ||
|
|
27a382f2cb | ||
|
|
b0eb601e5a | ||
|
|
3a3e743575 | ||
|
|
f08efb8238 | ||
|
|
723362dd67 | ||
|
|
dde039a299 | ||
|
|
f2a056c1d0 | ||
|
|
0defe263ad | ||
|
|
9d8d33de06 | ||
|
|
738b4d0f7b | ||
|
|
80378afa84 | ||
|
|
ec07e10208 | ||
|
|
2a6cd169aa | ||
|
|
051454988a | ||
|
|
a388d7cbec | ||
|
|
fa45d9e088 | ||
|
|
41e538fa97 | ||
|
|
1753438288 | ||
|
|
4135dfbd3f | ||
|
|
4de7d5d5d7 | ||
|
|
c4856f97ef | ||
|
|
aa885b8379 | ||
|
|
eab9639242 | ||
|
|
a199d82539 | ||
|
|
469fc2ff5f | ||
|
|
b4261d8571 | ||
|
|
725817151d | ||
|
|
e434300eda | ||
|
|
6b08c422cb | ||
|
|
4bfa7b3582 | ||
|
|
d6f902ae02 | ||
|
|
94023b0671 | ||
|
|
16a9428bc1 | ||
|
|
3db5891e0f | ||
|
|
d03fd72b95 | ||
|
|
97640c2a1f | ||
|
|
3bba6cf10d | ||
|
|
aca97afc78 | ||
|
|
3b46da5571 | ||
|
|
df62881ee6 | ||
|
|
966eea4f3f | ||
|
|
eb2ed05b6d | ||
|
|
f31c1eee0e | ||
|
|
c61db34d27 | ||
|
|
3d85cef8a0 | ||
|
|
276fcccc46 | ||
|
|
caa075f422 | ||
|
|
cc629314ca | ||
|
|
c9242c3475 | ||
|
|
3a87416cdb | ||
|
|
4c4f60bddc | ||
|
|
557652b18e | ||
|
|
a1094626c4 | ||
|
|
e45dd7ff18 | ||
|
|
1c1625b6fd | ||
|
|
9c4cb0daad | ||
|
|
024dd379e8 | ||
|
|
16c4957c79 | ||
|
|
01741b83cd | ||
|
|
4200b3274a | ||
|
|
eabd14eb60 | ||
|
|
778c087930 | ||
|
|
cd646a2e90 | ||
|
|
be6576ac74 | ||
|
|
6460500a4d | ||
|
|
02474dfad2 | ||
|
|
d78fd109ff | ||
|
|
85e3f49270 | ||
|
|
59d40b944b | ||
|
|
1e82e8bd53 | ||
|
|
ab5e9b6e9b | ||
|
|
7336d8d12b | ||
|
|
1c9dd73d9a | ||
|
|
5892002a89 | ||
|
|
540290e97c | ||
|
|
f315916fb8 | ||
|
|
e75bb48360 | ||
|
|
2b1d711168 | ||
|
|
2af5a89430 | ||
|
|
b640fa1372 | ||
|
|
8e24673d08 | ||
|
|
ae54e62ec3 | ||
|
|
420a4cbad3 | ||
|
|
05ec77da27 | ||
|
|
c03abf1148 | ||
|
|
b929df4848 | ||
|
|
ec729b6624 | ||
|
|
67471ee141 | ||
|
|
6999f0a863 | ||
|
|
6574a5d651 | ||
|
|
1639e73738 | ||
|
|
68fa67f54f | ||
|
|
326c34b6c1 | ||
|
|
a43dca3644 | ||
|
|
168ccf7d10 | ||
|
|
a9630b4d53 | ||
|
|
4a138bd425 | ||
|
|
339ff59270 | ||
|
|
d208ec12bc | ||
|
|
893ddefa71 | ||
|
|
5d56365e79 | ||
|
|
3237467503 | ||
|
|
346529469c | ||
|
|
9f4e420dbb | ||
|
|
cf4020c613 | ||
|
|
93edde7ed1 | ||
|
|
362e462c2a | ||
|
|
a7241bc0fb | ||
|
|
f26046f34b | ||
|
|
a70813ea5c | ||
|
|
3f251fdb66 | ||
|
|
b76cf329dd | ||
|
|
e0ad177a62 | ||
|
|
1223bd3679 | ||
|
|
b4e24fa328 | ||
|
|
0368ab5f68 | ||
|
|
c44ee8f063 | ||
|
|
3bfa6345a0 | ||
|
|
77d87ba995 | ||
|
|
a2ac68028f | ||
|
|
aeb945f145 | ||
|
|
aadde397ea | ||
|
|
063fea913e | ||
|
|
9d57889b81 | ||
|
|
233b69623f | ||
|
|
c7d6e60efb | ||
|
|
63688a9994 | ||
|
|
dc74e33b8b | ||
|
|
81f2620a69 | ||
|
|
1c95550f5d | ||
|
|
37a821818d | ||
|
|
29e0bdfbc3 | ||
|
|
d4bbcef011 | ||
|
|
81fc41ea81 | ||
|
|
8cadf42c86 | ||
|
|
e29ec2cc40 | ||
|
|
05952e7f7f | ||
|
|
0e8abb4607 | ||
|
|
cede9186a2 | ||
|
|
c1c9fa050b | ||
|
|
76d86288cc | ||
|
|
c0902f8830 | ||
|
|
1d3546d676 | ||
|
|
58baab6ffd | ||
|
|
3bafa60d59 | ||
|
|
825062ba78 | ||
|
|
d128e3cc24 | ||
|
|
83619834a8 | ||
|
|
cb16220ebb | ||
|
|
7f3af23773 | ||
|
|
9898a14dd3 | ||
|
|
85fc475118 | ||
|
|
987e8531e6 | ||
|
|
4d9a537bef | ||
|
|
05de15488e | ||
|
|
721a3ee801 | ||
|
|
bca000c21b | ||
|
|
80afd0c5b7 | ||
|
|
7416037762 | ||
|
|
325aa00bf8 | ||
|
|
40c2cc9b60 | ||
|
|
1bf16a2fb7 | ||
|
|
2687ce031a | ||
|
|
3213169ea1 | ||
|
|
5c3d6c4b31 | ||
|
|
336f596d08 | ||
|
|
930656fbe5 | ||
|
|
3b45061974 | ||
|
|
983c3e3c38 | ||
|
|
691fe05322 | ||
|
|
569389bb12 | ||
|
|
da886291cf | ||
|
|
05b219b3ea | ||
|
|
7cfefe7791 | ||
|
|
a2fe94285d | ||
|
|
6429a1ba52 | ||
|
|
477ae25fc7 | ||
|
|
a2646ffdb3 | ||
|
|
7a93172876 | ||
|
|
327cbe55d7 | ||
|
|
35ec26868c | ||
|
|
0c7317f09e | ||
|
|
a049db588b | ||
|
|
840691451a | ||
|
|
b50c956a21 | ||
|
|
3820d647e0 | ||
|
|
ce234381c8 | ||
|
|
2abeccdd1b | ||
|
|
ae4d6f8e2a | ||
|
|
78c3253979 | ||
|
|
8026478052 | ||
|
|
0d2ac7751e | ||
|
|
47cb298317 | ||
|
|
c1d0b98990 | ||
|
|
798c10d420 | ||
|
|
0d65f41bb5 | ||
|
|
eb32af9cda | ||
|
|
cb82374712 | ||
|
|
c4f7189435 | ||
|
|
2c037a6322 | ||
|
|
6b9e6ee5b4 | ||
|
|
adecdf7e27 | ||
|
|
d925e136ef | ||
|
|
3ae7e561c4 | ||
|
|
ca619efa14 | ||
|
|
47d5923a04 | ||
|
|
8c22a8ab00 | ||
|
|
8152cb6a24 | ||
|
|
ffed4fc5cf | ||
|
|
d8bd3c1176 | ||
|
|
3b12aa4b7b | ||
|
|
099b522d35 | ||
|
|
166d181469 | ||
|
|
a4a2a67177 | ||
|
|
7fe3de78af | ||
|
|
e75d658092 | ||
|
|
c840462fa2 | ||
|
|
c3475a37d2 | ||
|
|
183fe2c6f2 | ||
|
|
056f5e263e | ||
|
|
386c7a6056 | ||
|
|
80c876f35d | ||
|
|
30ae51d945 | ||
|
|
95b54ed4ab | ||
|
|
3650bda107 | ||
|
|
2bdf13761e | ||
|
|
ee4ee1f778 | ||
|
|
dbb72747e9 | ||
|
|
8e577c0924 | ||
|
|
6ce160447c | ||
|
|
b94a47ccb4 | ||
|
|
ec34b33ce7 | ||
|
|
024b95c12e | ||
|
|
cf4dc2aa4a | ||
|
|
1919ce2935 | ||
|
|
1e3dfb64ae | ||
|
|
f87eaa82dd | ||
|
|
13b90186d3 | ||
|
|
2c4e329663 | ||
|
|
624e129ef7 | ||
|
|
2f19bd7fc9 | ||
|
|
da48539585 | ||
|
|
a1d263837a | ||
|
|
188824ecaa | ||
|
|
f5376290ab | ||
|
|
b888e37556 | ||
|
|
cb43160902 | ||
|
|
a3ad6aa4c5 | ||
|
|
d0f0bedae2 | ||
|
|
3f58349ce8 | ||
|
|
e90d7d5f0e | ||
|
|
b39e5d2721 | ||
|
|
3ede2713b6 | ||
|
|
dc2fc292ce | ||
|
|
3dc50d2d7a | ||
|
|
3d15425460 | ||
|
|
412e27ee81 | ||
|
|
d73ea341c0 | ||
|
|
694ebf83ed | ||
|
|
78bb03fabf | ||
|
|
902215f064 | ||
|
|
eaf38729bd | ||
|
|
bdd89ca539 | ||
|
|
39ecd31455 | ||
|
|
c96a2cf3ec | ||
|
|
ea9e29b98c | ||
|
|
96e1ef5a52 | ||
|
|
ed4b0a75dd | ||
|
|
37354338cb | ||
|
|
3cd2eecc22 | ||
|
|
0e40d3f7c3 | ||
|
|
c8fa01116d | ||
|
|
04539b4c37 | ||
|
|
b9e8f4cfca | ||
|
|
ad06c2bf9e | ||
|
|
2c7ec74b1b | ||
|
|
b34f29c95f | ||
|
|
e11518c3f5 | ||
|
|
57c4fad307 | ||
|
|
856b8e37e1 | ||
|
|
6b8dd8e864 | ||
|
|
c993cd53f1 | ||
|
|
934983f668 | ||
|
|
ccf425faad | ||
|
|
a244763109 | ||
|
|
7ab712e040 | ||
|
|
cb7219b290 | ||
|
|
7f4ba50ee4 | ||
|
|
b1f00f0592 | ||
|
|
80f8f908d2 | ||
|
|
506308ece9 | ||
|
|
baecded9d5 | ||
|
|
ceefee397f | ||
|
|
e45804be31 | ||
|
|
edef1acbfb | ||
|
|
742939182c | ||
|
|
a21bfcdea6 | ||
|
|
bd281e449b | ||
|
|
0e1464a15f | ||
|
|
8a6441ddf3 | ||
|
|
94874c0a9e | ||
|
|
b9a6410959 | ||
|
|
512f5e00dd | ||
|
|
91c2364958 | ||
|
|
1e3f1c553c | ||
|
|
80c65a48a0 | ||
|
|
4c04ffb7db | ||
|
|
cc3949fa71 | ||
|
|
904a0ecbdd | ||
|
|
9155dd026d | ||
|
|
7286a06985 | ||
|
|
a71552a406 | ||
|
|
5857e8c394 | ||
|
|
429af04714 | ||
|
|
f134987572 | ||
|
|
d8a9043f94 | ||
|
|
3782fa8360 | ||
|
|
023e32f796 | ||
|
|
82f3abdc86 | ||
|
|
1345d31a6d | ||
|
|
c56fa39c15 | ||
|
|
7c8997ae9a | ||
|
|
584343c4e9 | ||
|
|
5754f8eb7c | ||
|
|
15a076026c | ||
|
|
1c90451cb9 | ||
|
|
a4939f2f95 | ||
|
|
b19d2b1894 | ||
|
|
242421a765 | ||
|
|
4cf12b71ac | ||
|
|
b8cf3a6fe9 | ||
|
|
333e163624 | ||
|
|
dfe1518676 | ||
|
|
8097c685e8 | ||
|
|
810754d833 | ||
|
|
a0377cb53a | ||
|
|
6ddcc31a8a | ||
|
|
b61fc06688 | ||
|
|
f29f9e134f | ||
|
|
a2b264708d | ||
|
|
2ae859511f | ||
|
|
b554e7f47c | ||
|
|
1085b63d25 | ||
|
|
ac900ec855 | ||
|
|
8f68e66047 | ||
|
|
faf300db3e | ||
|
|
2fbdd03225 | ||
|
|
9f825f9d5d | ||
|
|
5ff1904148 | ||
|
|
16e80ff80e | ||
|
|
1dcd1845a7 | ||
|
|
955f507023 |
@@ -1,8 +1,8 @@
|
||||
*.DS_Store
|
||||
*.swp
|
||||
.gradle
|
||||
.idea
|
||||
build
|
||||
local.properties
|
||||
gradle.properties
|
||||
infer-out
|
||||
*.iml
|
||||
|
||||
@@ -1,5 +1,305 @@
|
||||
# Change Log
|
||||
|
||||
## 1.37.0
|
||||
* Improve formatting date and time in history listing
|
||||
* Add haptic and audible feedback for background request errors
|
||||
|
||||
## 1.36.0
|
||||
* Add a setting to automatically return to scanning screen after copying
|
||||
* Add a button to test the background request
|
||||
* Show HTTP response for background requests
|
||||
* Improve SVG export
|
||||
* Automatically continue with an action after granting a permission
|
||||
* Update Chinese translation
|
||||
* Update French translation
|
||||
* Update Indonesian translation
|
||||
* Update Turkish translation
|
||||
* Update Ukrainian translation
|
||||
|
||||
## 1.35.0
|
||||
* Put images with transparent pixels on white/black
|
||||
* Use cropping limiter when scanning from images too
|
||||
* Support setting the error correction level when creating QR Codes
|
||||
* Support setting a custom deviating locale
|
||||
* Use adaptive icons for shortcut icons
|
||||
* Add exporting barcodes as plain text
|
||||
* Add Ukrainian translation
|
||||
* Add Turkish translation
|
||||
* Add Georgian translation
|
||||
* Update Indonesian translation
|
||||
* Update Russian translation
|
||||
* Update Italian translation
|
||||
* Update Simplified Chinese translation
|
||||
|
||||
## 1.34.0
|
||||
* Use Material Design settings with headers
|
||||
* Add a setting to choose the message type when forwarding scanned content
|
||||
|
||||
## 1.33.0
|
||||
* Add a setting to automatically send the scanned content to a custom URL
|
||||
* Add a setting to automatically put contents into clipboard
|
||||
* Add an explicit button to copy WiFi password into clipboard
|
||||
* Update Indonesian translation
|
||||
|
||||
## 1.32.1
|
||||
* Alternatively remove region of interest by dragging handle to a screen corner
|
||||
* Restrict region of interest to screen space
|
||||
* Remove extra action item to create barcode
|
||||
|
||||
## 1.32.0
|
||||
* Add bulk mode
|
||||
* Remember region of interest permanently
|
||||
* Export generated barcode as SVG
|
||||
* Run detection on images in background
|
||||
* Remove a scan from result view
|
||||
* Update Indonesian translation
|
||||
|
||||
## 1.31.0
|
||||
* Stop detection while region of interest is modified
|
||||
* Fix resetting region of interest
|
||||
* Add Polish translation
|
||||
|
||||
## 1.30.1
|
||||
* Improve usability of cropping limiter
|
||||
* Draw round corners around region of interest
|
||||
* Update Indonesian translation
|
||||
|
||||
## 1.30.0
|
||||
* Add a handle to define a region of interest
|
||||
* Add a setting to show/hide cropping limiter
|
||||
* Add support for VCALENDAR types
|
||||
* Add spanish translation
|
||||
* Add copy to clipboard button to context menu in history listing
|
||||
* Make history actions work on current listing only
|
||||
* Keep camera selection over orientation changes
|
||||
|
||||
## 1.29.0
|
||||
* Add a setting to enable reading of vertical 1D barcodes
|
||||
|
||||
## 1.28.0
|
||||
* Share history as CSV and JSON too
|
||||
* Export history in JSON format
|
||||
* Update Indonesian translation
|
||||
* Fix initializing of RenderScript automatically
|
||||
* Fix highlighting after landscape to landscape rotations
|
||||
|
||||
## 1.27.1
|
||||
* Update Russian translation
|
||||
* Force compat mode if native RenderScript crashed
|
||||
|
||||
## 1.27.0
|
||||
* Export SQLite database
|
||||
* Improve order of preferences
|
||||
* Update Italian translation
|
||||
|
||||
## 1.26.0
|
||||
* Zoom in & out by swiping vertically in camera view
|
||||
* Add a setting for swipe to zoom feature
|
||||
* Optimize vector drawables
|
||||
* Update Indonesian translation
|
||||
|
||||
## 1.25.1
|
||||
* Fix Proguard configuration
|
||||
|
||||
## 1.25.0
|
||||
* Add giving scans individual names in history listing
|
||||
* Add searching the history
|
||||
* Add a setting to search harder for barcodes
|
||||
* Add a setting to control vibration on detection
|
||||
* Add app shortcuts for launchers that support them
|
||||
* Remove saving barcodes for intents from other apps
|
||||
* Show latest possible result point in camera view
|
||||
* Suggest a WiFi network on Android Q+
|
||||
* Always put parsed WiFi password into clipboard
|
||||
* Use native image processing where available
|
||||
* Ignore consecutive duplicates by default
|
||||
* Fix saving external files on Android Q+
|
||||
* Fix order of metadata items
|
||||
|
||||
## 1.24.0
|
||||
* Show barcode metadata
|
||||
* Add a setting to show/hide metadata
|
||||
* Include metadata in CSV export
|
||||
* Update italian translation
|
||||
* Fix resetting flash button
|
||||
* Fix CSV export for Android Q
|
||||
|
||||
## 1.23.0
|
||||
* Add a setting to not save consecutive double scans
|
||||
* Add a setting to show/hide hex dump
|
||||
* Limit history items to just one line
|
||||
* Fix window insets for consecutive screens
|
||||
|
||||
## 1.22.3
|
||||
* Fix broken RenderScript built
|
||||
|
||||
## 1.22.2
|
||||
* Fix getting window insets for Android Lollipop and above
|
||||
|
||||
## 1.22.1
|
||||
* Ensure non-printable content is saved as raw data
|
||||
|
||||
## 1.22.0
|
||||
* Highlight detected barcodes in camera view
|
||||
* Add bing and yandex search engines
|
||||
* Update Chinese Translation
|
||||
* Update Italian translation
|
||||
* Update Indonesian translation
|
||||
|
||||
## 1.21.0
|
||||
* Save and restore history list state
|
||||
* Improve scanning from large images
|
||||
* Fix opening image files by "Open with"
|
||||
|
||||
## 1.20.1
|
||||
* Fix window insets below Android Lollipop
|
||||
|
||||
## 1.20.0
|
||||
* Scroll content below tool and system bars
|
||||
* Preset file name to save with format and content
|
||||
* Add encode menu item to make encoding easier when keyboard is visible
|
||||
* Increase font size of barcode contents in history
|
||||
* Update Hungarian translation
|
||||
* Update Indonesian translation
|
||||
* Fix encoding for old Androids
|
||||
* Fix returning result for ZXing's SCAN intent again
|
||||
* Fix transfering barcode format from history
|
||||
|
||||
## 1.19.0
|
||||
* Allow scaling a new barcode to half its size
|
||||
* Remember last selected barcode format
|
||||
|
||||
## 1.18.0
|
||||
* Make open URL default action after decoding a barcode
|
||||
* Add a setting to open contents without inspection
|
||||
* Highlight codes for picking in shared/loaded images
|
||||
* Improve visability of crop border
|
||||
* Fix returning result for ZXing's SCAN intent
|
||||
|
||||
## 1.17.1
|
||||
* Fix navigating back without choosing an image
|
||||
|
||||
## 1.17.0
|
||||
* Pinch/Zoom shared/loaded images to scan just a section
|
||||
* Add a load file menu item to load an image file
|
||||
* Add a save file menu item to the barcode view
|
||||
* Use discrete style for size seek bar
|
||||
* Fix bar transparency when zooming/pinching
|
||||
|
||||
## 1.16.0
|
||||
* Improve displaying scanned barcodes
|
||||
* Show a message if a barcode cannot be shared
|
||||
* Fix setting system bar transparency when double taping a barcode
|
||||
* Update Dutch translation
|
||||
* Update Hungarian translation
|
||||
* Update Indonesian translation
|
||||
* Update Italian translation
|
||||
|
||||
## 1.15.0
|
||||
* Additionally export history as CSV file
|
||||
* Color tool and system bars when a view is scrolled
|
||||
* Add an intent filter for opening images
|
||||
* Add Russian translation
|
||||
|
||||
## 1.14.0
|
||||
* Add OTP support
|
||||
* Update Indonesian translation
|
||||
|
||||
## 1.13.1
|
||||
* Fix crash caused by missing support for Regex on Android < 6
|
||||
|
||||
## 1.13.0
|
||||
* Add missing extras for ZXing result intents
|
||||
|
||||
## 1.12.0
|
||||
* Add preferences dialog and the option to use a custom URL with read contents
|
||||
* Add Brazilian Portuguese translation
|
||||
* Add Chinese translation
|
||||
* Encode text using UTF-8 encoding when creating a barcode
|
||||
|
||||
## 1.11.2
|
||||
* Downgrade ZXing version
|
||||
* Improve getting system UI metrics
|
||||
|
||||
## 1.11.1
|
||||
* Fix tap to focus on some devices
|
||||
|
||||
## 1.11.0
|
||||
* Add support for VCard and VEvent barcodes contents
|
||||
* Generated barcodes are now zoomable
|
||||
* Fix creating PDF417 barcodes
|
||||
|
||||
## 1.10.0
|
||||
* Add support for special barcode contents (WiFi, SMS, phone, E-mail)
|
||||
* Add Indonesian translation
|
||||
* Update Hungarian translation
|
||||
* Make system bars completely transparent
|
||||
|
||||
## 1.9.1
|
||||
* Pick search engine for content that is not an URL
|
||||
|
||||
## 1.9.0
|
||||
* Try Google when opening content as URL fails
|
||||
* Show generic error message if barcode generation fails
|
||||
|
||||
## 1.8.1
|
||||
* Distribute APK instead of App Bundle
|
||||
|
||||
## 1.8.0
|
||||
* Save binary data into external file
|
||||
* Fix storage of binary data
|
||||
* Remove horizontal orientation line from scanner
|
||||
|
||||
## 1.7.3
|
||||
* Fix crashes on Lineage and remove RenderScript fallback
|
||||
* Improve handling of binary data
|
||||
|
||||
## 1.7.2
|
||||
* Add a fallback for RenderScript
|
||||
* Add italian translation
|
||||
|
||||
## 1.7.1
|
||||
* Fix support for 64 bit architectures
|
||||
|
||||
## 1.7.0
|
||||
* Pick list separator on sharing history listing
|
||||
* Update Hungarian translation
|
||||
|
||||
## 1.6.0
|
||||
* Share history listing as plain text
|
||||
* Add separator line between history items
|
||||
* Add content description for floating action buttons
|
||||
|
||||
## 1.5.1
|
||||
* Fix generating hex dump
|
||||
|
||||
## 1.5.0
|
||||
* Show a hex dump of decoded contents
|
||||
* Fix processing text from share menu
|
||||
* Add French translation
|
||||
|
||||
## 1.4.3
|
||||
* Fix broken history listing
|
||||
|
||||
## 1.4.2
|
||||
* Show barcode type and content length after decoding
|
||||
|
||||
## 1.4.1
|
||||
* Toggle availability of torch mode button according to camera features
|
||||
|
||||
## 1.4.0
|
||||
* Save scanned codes and show a history listing
|
||||
* Share generated barcodes
|
||||
* Add scanning from a shared image
|
||||
* Add menu item to switch between camera front/back camera
|
||||
* Add dutch and hungarian translation
|
||||
|
||||
## 1.3.8
|
||||
* Qualify for F-Droid
|
||||
|
||||
## 1.3.7
|
||||
* Normalize URL schemes
|
||||
|
||||
## 1.3.6
|
||||
* Fix handling of camera scene modes
|
||||
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
# Contribution Guidelines
|
||||
|
||||
Please try to keep things in good shape and comply to what's there.
|
||||
|
||||
This project follows Android [best practices][android_best_practices]
|
||||
so please have a look if you've never heard of them.
|
||||
|
||||
The code is formatted according to Android Studio's standard, with the
|
||||
exception of indent being tabs instead of spaces.
|
||||
|
||||
Use the feature branch workflow to add new features and make sure
|
||||
to squash when merging into master:
|
||||
|
||||
$ git merge cool_feature --squash
|
||||
|
||||
Then write a [good commit message][commit_messages] to keep the history
|
||||
meaningful and useful. One feature, one commit.
|
||||
|
||||
[android_best_practices]: https://developer.android.com/distribute/best-practices/develop/
|
||||
[commit_messages]: https://juffalow.com/other/write-good-git-commit-message
|
||||
@@ -1,5 +1,4 @@
|
||||
PACKAGE = de.markusfisch.android.binaryeye
|
||||
APK = app/build/outputs/apk/debug/app-debug.apk
|
||||
|
||||
all: debug install start
|
||||
|
||||
@@ -7,26 +6,25 @@ debug:
|
||||
./gradlew assembleDebug
|
||||
|
||||
release: lint
|
||||
@./gradlew assembleRelease \
|
||||
-Pandroid.injected.signing.store.file=$(ANDROID_KEYFILE) \
|
||||
-Pandroid.injected.signing.store.password=$(ANDROID_STORE_PASSWORD) \
|
||||
-Pandroid.injected.signing.key.alias=$(ANDROID_KEY_ALIAS) \
|
||||
-Pandroid.injected.signing.key.password=$(ANDROID_KEY_PASSWORD)
|
||||
./gradlew assembleRelease
|
||||
|
||||
bundle: lint
|
||||
./gradlew bundleRelease
|
||||
|
||||
lint:
|
||||
./gradlew lintDebug
|
||||
|
||||
sonarqube:
|
||||
./gradlew sonarqube
|
||||
|
||||
infer: clean
|
||||
infer -- ./gradlew assembleDebug
|
||||
|
||||
test:
|
||||
./gradlew cAT
|
||||
./gradlew test
|
||||
|
||||
cat:
|
||||
./gradlew test cAT
|
||||
|
||||
install:
|
||||
adb $(TARGET) install -r $(APK)
|
||||
adb $(TARGET) install -r app/build/outputs/apk/debug/app-debug.apk
|
||||
|
||||
start:
|
||||
adb $(TARGET) shell 'am start -n \
|
||||
@@ -35,8 +33,14 @@ start:
|
||||
uninstall:
|
||||
adb $(TARGET) uninstall $(PACKAGE).debug
|
||||
|
||||
meminfo:
|
||||
adb shell dumpsys meminfo $(PACKAGE).debug
|
||||
|
||||
images:
|
||||
svg/update.sh
|
||||
|
||||
avocado:
|
||||
avocado $(shell fgrep -rl '<vector' app/src/main/res)
|
||||
|
||||
clean:
|
||||
./gradlew clean
|
||||
|
||||
@@ -2,4 +2,52 @@
|
||||
|
||||
Binary Eye does not collect any personal information.
|
||||
|
||||
If you have any questions or suggestions about my Privacy Policy, do not hesitate to contact me.
|
||||
If you have any questions or suggestions about my Privacy Policy, do not
|
||||
hesitate to contact me.
|
||||
|
||||
## Permissions
|
||||
|
||||
### [ACCESS_FINE_LOCATION](https://developer.android.com/reference/android/Manifest.permission#ACCESS_FINE_LOCATION)
|
||||
|
||||
Required to add a WiFi network from a barcode (usually a QR Code).
|
||||
|
||||
Before Android Q, this feature requires using
|
||||
[WifiManager.getConfiguredNetworks][configuredNetworks]
|
||||
which requires `ACCESS_FINE_LOCATION`.
|
||||
|
||||
On Android Q an better, this permission is not requested nor required.
|
||||
|
||||
### [CAMERA](https://developer.android.com/reference/android/Manifest.permission#CAMERA)
|
||||
|
||||
Required to read a barcode from the camera image.
|
||||
|
||||
### [INTERNET](https://developer.android.com/reference/android/Manifest.permission#INTERNET)
|
||||
|
||||
Required to send a scanned code to a custom URL you can specify in settings.
|
||||
|
||||
### [VIBRATE](https://developer.android.com/reference/android/Manifest.permission#VIBRATE)
|
||||
|
||||
Required to vibrate on detection.
|
||||
|
||||
### [WRITE_EXTERNAL_STORAGE](https://developer.android.com/reference/android/Manifest.permission#WRITE_EXTERNAL_STORAGE)
|
||||
|
||||
Required to save generated barcodes as files before Android Q.
|
||||
|
||||
### [CHANGE_WIFI_STATE](https://developer.android.com/reference/android/Manifest.permission#CHANGE_WIFI_STATE)
|
||||
|
||||
Required to add a WiFi network from a barcode (usually a QR Code).
|
||||
|
||||
On Android Q and better, this feature requires using
|
||||
[WifiManager.addNetworkSuggestions][addNetworkSuggestions]
|
||||
which requires `CHANGE_WIFI_STATE`.
|
||||
|
||||
### [ACCESS_WIFI_STATE](https://developer.android.com/reference/android/Manifest.permission#ACCESS_FINE_LOCATION)
|
||||
|
||||
Required to add a WiFi network from a barcode (usually a QR Code).
|
||||
|
||||
Before Android Q, this feature requires using
|
||||
[WifiManager.getConfiguredNetworks][configuredNetworks]
|
||||
which requires `ACCESS_WIFI_STATE`.
|
||||
|
||||
[configuredNetworks]: https://developer.android.com/reference/android/net/wifi/WifiManager#getConfiguredNetworks()
|
||||
[addNetworkSuggestions]: https://developer.android.com/reference/android/net/wifi/WifiManager#addNetworkSuggestions(java.util.List%3Candroid.net.wifi.WifiNetworkSuggestion%3E)
|
||||
|
||||
@@ -2,18 +2,115 @@
|
||||
|
||||
Yet another barcode scanner for Android. As if there weren't [enough][play].
|
||||
|
||||
This one is totally free, open source and does use the [ZXing][zxing] barcode
|
||||
scanning library.
|
||||
This one is free, without any ads and open source.
|
||||
|
||||
Unlike many others it works in portrait and landscape orientation, can read
|
||||
inverted codes, uses Material Design and doesn't do anything else but decoding
|
||||
a barcode.
|
||||
Works in portrait and landscape orientation, can read inverted codes,
|
||||
comes in Material Design and can also generate barcodes.
|
||||
|
||||
<a href="https://play.google.com/store/apps/details?id=de.markusfisch.android.binaryeye"><img alt="Get it on Google Play" src="https://play.google.com/intl/en_us/badges/images/apps/en-play-badge.png" height="45px"/></a>
|
||||
Binary Eye uses the [ZXing][zxing] ("Zebra Crossing") barcode scanning
|
||||
library.
|
||||
|
||||
And it's written in [Kotlin][kotlin] because I've wanted to explore that a
|
||||
bit more.
|
||||
## Screenshots
|
||||
|
||||
<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/screencap-scanning.png"
|
||||
alt="Screenshot Gallery" width="160"/>
|
||||
<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/screencap-scanning-cropped.png"
|
||||
alt="Screenshot Gallery" width="160"/>
|
||||
<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/screencap-decoded.png"
|
||||
alt="Screenshot Theme" width="160"/>
|
||||
<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/screencap-compose-barcode.png"
|
||||
alt="Screenshot Editor" width="160"/>
|
||||
|
||||
## Download
|
||||
|
||||
<a href="https://f-droid.org/en/packages/de.markusfisch.android.binaryeye/"><img alt="Get it on F-Droid" src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png" height="80"/></a> <a href="https://play.google.com/store/apps/details?id=de.markusfisch.android.binaryeye"><img alt="Get it on Google Play" src="https://play.google.com/intl/en_us/badges/images/generic/en_badge_web_generic.png" height="80"/></a>
|
||||
|
||||
## Supported Barcode Formats
|
||||
|
||||
### Read
|
||||
|
||||
ZXing can read the following barcode formats:
|
||||
* [AZTEC][aztec]
|
||||
* [CODABAR][codabar]
|
||||
* [CODE 39][code_39]
|
||||
* [CODE 93][code_93]
|
||||
* [CODE 128][code_128]
|
||||
* [DATA MATRIX][data_matrix]
|
||||
* [EAN 8][ean_8]
|
||||
* [EAN 13][ean_13]
|
||||
* [ITF][itf]
|
||||
* [MAXICODE][maxicode] (only when unrotated and unskewed, see [77][77],
|
||||
because of which Binary Eye *cannot* read this barcode)
|
||||
* [PDF417][pdf417]
|
||||
* [QR CODE][qr_code]
|
||||
* [RSS 14][rss]
|
||||
* [RSS EXPANDED][rss]
|
||||
* [UPC A][upc_a]
|
||||
* [UPC E][upc_e]
|
||||
* [UPC EAN EXTENSION][upc_ean]
|
||||
|
||||
### Generate
|
||||
|
||||
ZXing can generate the following barcode formats:
|
||||
* [AZTEC][aztec]
|
||||
* [CODABAR][codabar]
|
||||
* [CODE 39][code_39]
|
||||
* [CODE 128][code_128]
|
||||
* [DATA MATRIX][data_matrix]
|
||||
* [EAN 8][ean_8]
|
||||
* [EAN 13][ean_13]
|
||||
* [ITF][itf]
|
||||
* [PDF 417][pdf417]
|
||||
* [QR CODE][qr_code]
|
||||
* [UPC A][upc_a]
|
||||
|
||||
## RenderScript
|
||||
|
||||
This app uses [RenderScript][rs] to resize and rotate the camera image.
|
||||
Unfortunately, RenderScript has some nasty gotchas.
|
||||
|
||||
### RenderScript.forceCompat()
|
||||
|
||||
It's necessary to call `RenderScript.forceCompat()` on some devices/roms.
|
||||
|
||||
`RenderScript.forceCompat()` needs to be run before any other RenderScript
|
||||
function and unfortunately there is no way to know if invoking `forceCompat()`
|
||||
is necessary or not.
|
||||
|
||||
If `RenderScript.forceCompat()` is necessary, a `RSRuntimeException` will
|
||||
be thrown and the only option is to restart the app, this time with calling
|
||||
`forceCompat()` first.
|
||||
|
||||
Calling `RenderScript.forceCompat()` means the processing is done in
|
||||
software so you probably don't want to enable it by default.
|
||||
|
||||
### 2D barcodes
|
||||
|
||||
If you want to fork this and are only interested in reading 2D barcodes
|
||||
(like QR or Aztec), you may want to remove the custom rotation kernel
|
||||
altogether as ZXing can read 2D barcodes in any orientation.
|
||||
|
||||
This will make your app a bit simpler and saves you from compiling a
|
||||
custom RenderScript kernel for each architecture you want to support.
|
||||
|
||||
[play]: https://play.google.com/store/search?q=barcode%20scanner&c=apps
|
||||
[zxing]: https://github.com/zxing/zxing
|
||||
[kotlin]: http://kotlinlang.org/
|
||||
[aztec]: https://en.wikipedia.org/wiki/Aztec_Code
|
||||
[codabar]: https://en.wikipedia.org/wiki/Codabar
|
||||
[code_39]: https://en.wikipedia.org/wiki/Code_39
|
||||
[code_93]: https://en.wikipedia.org/wiki/Code_93
|
||||
[code_128]: https://en.wikipedia.org/wiki/Code_128
|
||||
[data_matrix]: https://en.wikipedia.org/wiki/Data_Matrix
|
||||
[ean_8]: https://en.wikipedia.org/wiki/EAN-8
|
||||
[ean_13]: https://en.wikipedia.org/wiki/International_Article_Number
|
||||
[itf]: https://en.wikipedia.org/wiki/Interleaved_2_of_5
|
||||
[maxicode]: https://en.wikipedia.org/wiki/MaxiCode
|
||||
[pdf417]: https://en.wikipedia.org/wiki/PDF417
|
||||
[qr_code]: https://en.wikipedia.org/wiki/QR_code
|
||||
[rss]: https://en.wikipedia.org/wiki/GS1_DataBar
|
||||
[upc_a]: https://en.wikipedia.org/wiki/Universal_Product_Code
|
||||
[upc_e]: https://en.wikipedia.org/wiki/Universal_Product_Code#UPC-E
|
||||
[upc_ean]: https://en.wikipedia.org/wiki/Universal_Product_Code#EAN-13
|
||||
[77]: https://github.com/markusfisch/BinaryEye/issues/77
|
||||
[rs]: https://developer.android.com/guide/topics/renderscript/compute
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'net.ltgt.errorprone'
|
||||
|
||||
configurations.errorprone {
|
||||
resolutionStrategy.force 'com.google.errorprone:error_prone_core:2.0.19'
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdkVersion sdk_version
|
||||
@@ -14,19 +9,32 @@ android {
|
||||
minSdkVersion 9
|
||||
targetSdkVersion sdk_version
|
||||
|
||||
versionCode 12
|
||||
versionName '1.3.6'
|
||||
versionCode 76
|
||||
versionName '1.37.0'
|
||||
|
||||
// it's recommended to set this value to the lowest API level
|
||||
// able to provide all the functionality
|
||||
renderscriptTargetApi 18
|
||||
renderscriptTargetApi 24
|
||||
renderscriptSupportModeEnabled true
|
||||
renderscriptSupportModeBlasEnabled true
|
||||
|
||||
testInstrumentationRunner 'android.support.test.runner.AndroidJUnitRunner'
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
config {
|
||||
keyAlias System.getenv('ANDROID_KEY_ALIAS')
|
||||
keyPassword System.getenv('ANDROID_KEY_PASSWORD')
|
||||
storePassword System.getenv('ANDROID_STORE_PASSWORD')
|
||||
def filePath = System.getenv('ANDROID_KEYFILE')
|
||||
storeFile filePath ? file(filePath) : null
|
||||
}
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
main.java.srcDirs += 'src/main/kotlin'
|
||||
androidTest.java.srcDirs += 'src/androidTest/kotlin'
|
||||
test.java.srcDirs += 'src/test/kotlin'
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
@@ -35,11 +43,10 @@ android {
|
||||
}
|
||||
|
||||
release {
|
||||
proguardFiles \
|
||||
getDefaultProguardFile('proguard-android.txt'), \
|
||||
'proguard-rules.pro'
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||
minifyEnabled true
|
||||
shrinkResources true
|
||||
signingConfig signingConfigs.config
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,16 +59,23 @@ android {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
androidTestImplementation ('com.android.support.test:runner:1.0.2') {
|
||||
androidTestImplementation('com.android.support.test:runner:1.0.2') {
|
||||
exclude module: 'support-annotations'
|
||||
}
|
||||
androidTestImplementation ('com.android.support.test:rules:1.0.2') {
|
||||
androidTestImplementation('com.android.support.test:rules:1.0.2') {
|
||||
exclude module: 'support-annotations'
|
||||
}
|
||||
testImplementation 'junit:junit:4.12'
|
||||
|
||||
implementation fileTree(dir: 'libs', include: '*')
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.2"
|
||||
implementation "com.android.support:appcompat-v7:$support_version"
|
||||
implementation "com.android.support:design:$support_version"
|
||||
implementation "com.android.support:preference-v7:$support_version"
|
||||
implementation "com.android.support:preference-v14:$support_version"
|
||||
// Do NOT upgrade ZXing because newer versions won't work on API < 24
|
||||
// https://github.com/zxing/zxing/issues/1170
|
||||
implementation 'com.google.zxing:core:3.3.3'
|
||||
implementation 'com.github.markusfisch:CameraView:1.9.1'
|
||||
implementation 'com.github.markusfisch:ScalingImageView:1.3.0'
|
||||
}
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
<!-- Lint can't understand Kotlin sources and reports all resources
|
||||
as unused -->
|
||||
<issue id="UnusedResources">
|
||||
<ignore path="src/main/res/"/>
|
||||
<ignore path="src/main/res/" />
|
||||
</issue>
|
||||
<!-- Ignore RenderScript output -->
|
||||
<issue id="UnusedResources">
|
||||
<ignore path="*/res/rs/debug/raw/bc*"/>
|
||||
<ignore path="*/res/rs/debug/raw/bc*" />
|
||||
</issue>
|
||||
</lint>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
-dontwarn com.google.**
|
||||
-dontwarn android.support.v7.**
|
||||
-dontwarn android.support.v8.**
|
||||
|
||||
-keep class android.support.v7.widget.SearchView { *; }
|
||||
-keep class android.support.v8.renderscript.** { *; }
|
||||
|
||||
|
After Width: | Height: | Size: 97 KiB |
|
After Width: | Height: | Size: 79 KiB |
|
After Width: | Height: | Size: 88 KiB |
|
After Width: | Height: | Size: 77 KiB |
|
After Width: | Height: | Size: 52 KiB |
|
After Width: | Height: | Size: 47 KiB |
@@ -1,14 +1,15 @@
|
||||
package de.markusfisch.android.binaryeye
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import android.support.test.InstrumentationRegistry
|
||||
import android.support.test.runner.AndroidJUnit4
|
||||
|
||||
import android.support.v8.renderscript.RenderScript
|
||||
import de.markusfisch.android.binaryeye.rs.Preprocessor
|
||||
import de.markusfisch.android.binaryeye.zxing.Zxing
|
||||
|
||||
import org.junit.Assert.fail;
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
import java.util.regex.Pattern
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
@@ -19,34 +20,89 @@ class PreprocessorTest {
|
||||
val assets = InstrumentationRegistry.getInstrumentation()
|
||||
.context.assets
|
||||
val pattern = Pattern.compile(
|
||||
"[0-9]+-([0-9]+)x([0-9]+)-([0-9]+)deg.yuv"
|
||||
"[0-9]+-([0-9]+)deg.jpg"
|
||||
)
|
||||
|
||||
for (file in assets.list("yuv")) {
|
||||
val m = pattern.matcher(file)
|
||||
if (!m.find() || m.groupCount() < 3) {
|
||||
continue
|
||||
val samples = assets.list("samples")
|
||||
checkNotNull(samples) { "no samples found" }
|
||||
for (sample in samples) {
|
||||
val m = pattern.matcher(sample)
|
||||
if (!m.find() || m.groupCount() < 1) {
|
||||
fail("invalid sample: $sample")
|
||||
}
|
||||
|
||||
val frameWidth = m.group(1).toInt()
|
||||
val frameHeight = m.group(2).toInt()
|
||||
val frameOrientation = m.group(3).toInt()
|
||||
val frameData = assets.open("yuv/$file").readBytes()
|
||||
val preprocessor = Preprocessor(
|
||||
val bitmap = BitmapFactory.decodeStream(
|
||||
assets.open("samples/$sample")
|
||||
)
|
||||
val frameData = getNV21(bitmap)
|
||||
val frameWidth = bitmap.width
|
||||
val frameHeight = bitmap.height
|
||||
val frameOrientation = m.group(1) ?: return
|
||||
val rs = RenderScript.create(
|
||||
InstrumentationRegistry.getTargetContext(),
|
||||
)
|
||||
val preprocessor = Preprocessor(
|
||||
rs,
|
||||
frameWidth,
|
||||
frameHeight,
|
||||
frameOrientation
|
||||
)
|
||||
preprocessor.process(frameData)
|
||||
val result = zxing.decode(
|
||||
frameData,
|
||||
preprocessor.outWidth,
|
||||
preprocessor.outHeight
|
||||
null
|
||||
)
|
||||
val outWidth: Int
|
||||
val outHeight: Int
|
||||
val orientation = frameOrientation.toInt()
|
||||
if (orientation == 90 || orientation == 270) {
|
||||
preprocessor.resizeAndRotate(frameData)
|
||||
outWidth = preprocessor.outHeight
|
||||
outHeight = preprocessor.outWidth
|
||||
} else {
|
||||
preprocessor.resizeOnly(frameData)
|
||||
outWidth = preprocessor.outWidth
|
||||
outHeight = preprocessor.outHeight
|
||||
}
|
||||
val result = zxing.decode(frameData, outWidth, outHeight)
|
||||
preprocessor.destroy()
|
||||
rs.destroy()
|
||||
|
||||
checkNotNull(result) { "no barcode found in $file" }
|
||||
checkNotNull(result) { "no barcode found in $sample" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getNV21(bitmap: Bitmap): ByteArray {
|
||||
val width = bitmap.width
|
||||
val height = bitmap.height
|
||||
val argb = IntArray(width * height)
|
||||
bitmap.getPixels(argb, 0, width, 0, 0, width, height)
|
||||
return encodeYUV420SP(argb, width, height)
|
||||
}
|
||||
|
||||
private fun encodeYUV420SP(argb: IntArray, width: Int, height: Int): ByteArray {
|
||||
val yuv = ByteArray(ceilIfUneven(width) * ceilIfUneven(height) * 3 / 2)
|
||||
var yIndex = 0
|
||||
var uvIndex = width * height
|
||||
var r: Int
|
||||
var g: Int
|
||||
var b: Int
|
||||
var Y: Int
|
||||
var u: Int
|
||||
var v: Int
|
||||
var index = 0
|
||||
for (y in 0 until height) {
|
||||
for (x in 0 until width) {
|
||||
val pixel = argb[index]
|
||||
r = pixel and 0xff0000 shr 16
|
||||
g = pixel and 0xff00 shr 8
|
||||
b = pixel and 0xff
|
||||
Y = (66 * r + 129 * g + 25 * b + 128 shr 8) + 16
|
||||
u = (-38 * r - 74 * g + 112 * b + 128 shr 8) + 128
|
||||
v = (112 * r - 94 * g - 18 * b + 128 shr 8) + 128
|
||||
yuv[yIndex++] = (if (Y < 0) 0 else if (Y > 255) 255 else Y).toByte()
|
||||
if (y % 2 == 0 && index % 2 == 0) {
|
||||
yuv[uvIndex++] = (if (v < 0) 0 else if (v > 255) 255 else v).toByte()
|
||||
yuv[uvIndex++] = (if (u < 0) 0 else if (u > 255) 255 else u).toByte()
|
||||
}
|
||||
++index
|
||||
}
|
||||
}
|
||||
return yuv
|
||||
}
|
||||
|
||||
private fun ceilIfUneven(n: Int) = if (n and 1 == 1) n + 1 else n
|
||||
|
||||
@@ -5,15 +5,34 @@
|
||||
<supports-screens
|
||||
android:largeScreens="true"
|
||||
android:xlargeScreens="true"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
|
||||
<uses-permission android:name="android.permission.CAMERA"/>
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-permission android:name="android.permission.VIBRATE"/>
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
||||
<uses-permission android:name="android.permission.CHANGE_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.autofocus"/>
|
||||
<uses-sdk tools:overrideLibrary="android.support.v14.preference"/>
|
||||
<queries>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
<data android:scheme="https"/>
|
||||
</intent>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
<data android:scheme="http"/>
|
||||
</intent>
|
||||
</queries>
|
||||
<application
|
||||
tools:ignore="UnusedAttribute"
|
||||
android:name=".app.BinaryEyeApp"
|
||||
android:usesCleartextTraffic="true"
|
||||
android:allowBackup="true"
|
||||
android:fullBackupContent="true"
|
||||
android:fullBackupContent="@xml/backup_rules"
|
||||
android:supportsRtl="true"
|
||||
android:resizeableActivity="true"
|
||||
android:label="@string/app_name"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:theme="@style/AppTheme">
|
||||
@@ -25,6 +44,9 @@
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
</intent-filter>
|
||||
<meta-data
|
||||
android:name="android.app.shortcuts"
|
||||
android:resource="@xml/shortcuts"/>
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".activity.CameraActivity"
|
||||
@@ -48,8 +70,36 @@
|
||||
android:scheme="http"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".activity.PickActivity"
|
||||
android:label="@string/pick_code_to_scan"
|
||||
tools:ignore="AppLinkUrlError">
|
||||
<intent-filter android:label="@string/decode_barcode">
|
||||
<action android:name="android.intent.action.SEND"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<data android:mimeType="image/*"/>
|
||||
</intent-filter>
|
||||
<intent-filter android:label="@string/decode_barcode">
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<data android:mimeType="image/*"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".activity.MainActivity"
|
||||
android:label="@string/app_name"/>
|
||||
android:label="@string/app_name"
|
||||
android:exported="true"/>
|
||||
<provider
|
||||
android:name="android.support.v4.content.FileProvider"
|
||||
android:authorities="${applicationId}.provider"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true">
|
||||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/file_paths"/>
|
||||
</provider>
|
||||
<meta-data
|
||||
android:name="android.app.searchable"
|
||||
android:resource="@xml/searchable"/>
|
||||
</application>
|
||||
</manifest>
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
package de.markusfisch.android.binaryeye.actions
|
||||
|
||||
import de.markusfisch.android.binaryeye.actions.mail.MailAction
|
||||
import de.markusfisch.android.binaryeye.actions.otpauth.OtpauthAction
|
||||
import de.markusfisch.android.binaryeye.actions.search.OpenOrSearchAction
|
||||
import de.markusfisch.android.binaryeye.actions.sms.SmsAction
|
||||
import de.markusfisch.android.binaryeye.actions.tel.TelAction
|
||||
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
|
||||
|
||||
object ActionRegistry {
|
||||
val DEFAULT_ACTION: IAction = OpenOrSearchAction
|
||||
|
||||
private val REGISTRY: Set<IAction> = setOf(
|
||||
MailAction,
|
||||
OtpauthAction,
|
||||
SmsAction,
|
||||
TelAction,
|
||||
VCardAction,
|
||||
VEventAction,
|
||||
WebAction,
|
||||
WifiAction
|
||||
)
|
||||
|
||||
fun getAction(data: ByteArray): IAction = REGISTRY.find {
|
||||
it.canExecuteOn(data)
|
||||
} ?: DEFAULT_ACTION
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package de.markusfisch.android.binaryeye.actions
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import de.markusfisch.android.binaryeye.app.execShareIntent
|
||||
import de.markusfisch.android.binaryeye.app.parseAndNormalizeUri
|
||||
import de.markusfisch.android.binaryeye.widget.toast
|
||||
|
||||
interface IAction {
|
||||
val iconResId: Int
|
||||
val titleResId: Int
|
||||
|
||||
fun canExecuteOn(data: ByteArray): Boolean
|
||||
suspend fun execute(context: Context, data: ByteArray)
|
||||
}
|
||||
|
||||
abstract class IntentAction : IAction {
|
||||
abstract val errorMsg: Int
|
||||
|
||||
final override suspend fun execute(context: Context, data: ByteArray) {
|
||||
val intent = createIntent(context, data) ?: return context.toast(
|
||||
errorMsg
|
||||
)
|
||||
execShareIntent(context, intent)
|
||||
}
|
||||
|
||||
abstract suspend fun createIntent(context: Context, data: ByteArray): Intent?
|
||||
}
|
||||
|
||||
abstract class SchemeAction : IAction {
|
||||
abstract val scheme: String
|
||||
open val intentAction: String = Intent.ACTION_VIEW
|
||||
open val buildRegex: Boolean = false
|
||||
|
||||
final override fun canExecuteOn(data: ByteArray): Boolean {
|
||||
val content = String(data)
|
||||
return if (buildRegex) {
|
||||
content.matches("""^$scheme://[\w\W]+$""".toRegex(RegexOption.IGNORE_CASE))
|
||||
} else {
|
||||
content.startsWith("$scheme://", ignoreCase = true)
|
||||
}
|
||||
}
|
||||
|
||||
final override suspend fun execute(context: Context, data: ByteArray) {
|
||||
val uri = parseAndNormalizeUri(String(data))
|
||||
execShareIntent(context, Intent(intentAction, uri))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package de.markusfisch.android.binaryeye.actions.mail
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.actions.IntentAction
|
||||
|
||||
object MailAction : IntentAction() {
|
||||
private val mailRegex =
|
||||
"""^mail(?:to)?:([\w.%+-]+@[A-Za-z\d.-]+\.[A-Za-z]{2,6}(?:[?&](?:subject|body)=[\S\s]*?){0,2})$""".toRegex()
|
||||
|
||||
override val iconResId: Int = R.drawable.ic_action_mail
|
||||
override val titleResId: Int = R.string.mail_send
|
||||
override val errorMsg: Int = R.string.mail_error
|
||||
|
||||
override fun canExecuteOn(data: ByteArray): Boolean {
|
||||
return String(data).matches(mailRegex)
|
||||
}
|
||||
|
||||
override suspend fun createIntent(context: Context, data: ByteArray): Intent? {
|
||||
val mailWithMessage = mailRegex.matchEntire(
|
||||
String(data)
|
||||
)?.groupValues?.get(1) ?: return null
|
||||
return Intent(
|
||||
Intent.ACTION_SENDTO,
|
||||
Uri.parse("mailto:$mailWithMessage")
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package de.markusfisch.android.binaryeye.actions.otpauth
|
||||
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.actions.SchemeAction
|
||||
|
||||
object OtpauthAction : SchemeAction() {
|
||||
override val iconResId: Int = R.drawable.ic_action_otpauth
|
||||
override val titleResId: Int = R.string.otpauth_add
|
||||
override val scheme: String = "otpauth"
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package de.markusfisch.android.binaryeye.actions.search
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.actions.IAction
|
||||
import de.markusfisch.android.binaryeye.app.alertDialog
|
||||
import de.markusfisch.android.binaryeye.app.execShareIntent
|
||||
import de.markusfisch.android.binaryeye.app.parseAndNormalizeUri
|
||||
import de.markusfisch.android.binaryeye.app.prefs
|
||||
import de.markusfisch.android.binaryeye.widget.toast
|
||||
import java.net.URLEncoder
|
||||
|
||||
object OpenOrSearchAction : IAction {
|
||||
override val iconResId: Int = R.drawable.ic_action_search
|
||||
override val titleResId: Int = R.string.search_web
|
||||
|
||||
override fun canExecuteOn(data: ByteArray): Boolean = false
|
||||
|
||||
override suspend fun execute(context: Context, data: ByteArray) {
|
||||
val intent = openUri(context, String(data)) ?: return
|
||||
execShareIntent(context, intent)
|
||||
}
|
||||
|
||||
private suspend fun openUri(context: Context, data: String, search: Boolean = true): Intent? {
|
||||
val uri = parseAndNormalizeUri(data)
|
||||
val intent = Intent(Intent.ACTION_VIEW, uri)
|
||||
when {
|
||||
intent.resolveActivity(context.packageManager) != null -> return intent
|
||||
search -> return getSearchIntent(context, data)
|
||||
else -> context.toast(R.string.cannot_resolve_action)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private suspend fun getSearchIntent(context: Context, query: String): Intent? {
|
||||
val names = context.resources.getStringArray(
|
||||
R.array.search_engines_names
|
||||
).toMutableList()
|
||||
val urls = context.resources.getStringArray(
|
||||
R.array.search_engines_values
|
||||
).toMutableList()
|
||||
if (prefs.openWithUrl.isNotEmpty()) {
|
||||
names.add(prefs.openWithUrl)
|
||||
urls.add(prefs.openWithUrl)
|
||||
}
|
||||
val queryUri = alertDialog<String>(context) { resume ->
|
||||
setTitle(R.string.pick_search_engine)
|
||||
setItems(names.toTypedArray()) { _, which ->
|
||||
resume(urls[which] + URLEncoder.encode(query, "utf-8"))
|
||||
}
|
||||
} ?: return null
|
||||
return openUri(context, queryUri, false)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package de.markusfisch.android.binaryeye.actions.sms
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.actions.IntentAction
|
||||
|
||||
object SmsAction : IntentAction() {
|
||||
private val smsRegex =
|
||||
"""^sms(?:to)?:(\+?[0-9]+)(?::([\S\s]*))?$""".toRegex(RegexOption.IGNORE_CASE)
|
||||
|
||||
override val iconResId: Int = R.drawable.ic_action_sms
|
||||
override val titleResId: Int = R.string.sms_send
|
||||
override val errorMsg: Int = R.string.sms_error
|
||||
|
||||
override fun canExecuteOn(data: ByteArray): Boolean {
|
||||
return String(data).matches(smsRegex)
|
||||
}
|
||||
|
||||
override suspend fun createIntent(context: Context, data: ByteArray): Intent? {
|
||||
val (number: String, message: String) = smsRegex.matchEntire(
|
||||
String(data)
|
||||
)?.let {
|
||||
it.groupValues[1] to it.groupValues[2]
|
||||
} ?: return null
|
||||
return Intent(
|
||||
Intent.ACTION_SENDTO,
|
||||
Uri.parse("smsto:$number")
|
||||
).apply {
|
||||
if (message.isNotEmpty()) putExtra("sms_body", message)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package de.markusfisch.android.binaryeye.actions.tel
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.actions.IntentAction
|
||||
import de.markusfisch.android.binaryeye.app.parseAndNormalizeUri
|
||||
|
||||
object TelAction : IntentAction() {
|
||||
private val telRegex = """^tel:(\+?[0-9]+)$""".toRegex(RegexOption.IGNORE_CASE)
|
||||
|
||||
override val iconResId: Int = R.drawable.ic_action_tel
|
||||
override val titleResId: Int = R.string.tel_dial
|
||||
override val errorMsg: Int = R.string.tel_error
|
||||
|
||||
override fun canExecuteOn(data: ByteArray): Boolean {
|
||||
return String(data).matches(telRegex)
|
||||
}
|
||||
|
||||
override suspend fun createIntent(context: Context, data: ByteArray): Intent {
|
||||
return Intent(Intent.ACTION_DIAL, parseAndNormalizeUri(String(data)))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package de.markusfisch.android.binaryeye.actions.vtype
|
||||
|
||||
import java.util.*
|
||||
|
||||
object VTypeParser {
|
||||
private const val BACKSLASH_R_LEGACY =
|
||||
"(?:\u000D\u000A|[\u000A\u000B\u000C\u000D\u0085\u2028\u2029])"
|
||||
private val vTypeRegex =
|
||||
"""^BEGIN:(V.+)(?:$BACKSLASH_R_LEGACY.+?:[\s\S]+?)+?${BACKSLASH_R_LEGACY}END:\1$BACKSLASH_R_LEGACY?$""".toRegex(
|
||||
RegexOption.IGNORE_CASE
|
||||
)
|
||||
private val propertyRegex = """^(.+?):([\s\S]*?)$""".toRegex(
|
||||
RegexOption.MULTILINE
|
||||
)
|
||||
|
||||
fun parseVType(data: String): String? = vTypeRegex.matchEntire(
|
||||
data
|
||||
)?.groupValues?.get(1)?.toUpperCase(Locale.US)
|
||||
|
||||
fun parseMap(
|
||||
data: String
|
||||
): Map<String, List<VTypeProperty>> = propertyRegex.findAll(data).map {
|
||||
it.groupValues[1].split(';').let { typeAndInfo ->
|
||||
typeAndInfo[0] to VTypeProperty(
|
||||
typeAndInfo.drop(1),
|
||||
it.groupValues[2]
|
||||
)
|
||||
}
|
||||
}.groupBy({ it.first.toUpperCase(Locale.US) }) { it.second }
|
||||
}
|
||||
|
||||
data class VTypeProperty(
|
||||
val info: List<String>,
|
||||
val value: String
|
||||
) {
|
||||
val firstTypeOrFirstInfo: String?
|
||||
get() = this.info.find { it.startsWith("TYPE=", true) }?.let {
|
||||
typeRegex.matchEntire(it)?.groupValues?.get(1)
|
||||
} ?: this.info.firstOrNull()
|
||||
|
||||
companion object {
|
||||
private val typeRegex = """^(?:TYPE=)(.+?)[:;]?.*$""".toRegex(RegexOption.IGNORE_CASE)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
package de.markusfisch.android.binaryeye.actions.vtype.vcard
|
||||
|
||||
import android.content.ContentValues
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.provider.ContactsContract
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.actions.IntentAction
|
||||
import de.markusfisch.android.binaryeye.actions.vtype.VTypeParser
|
||||
import java.util.*
|
||||
import kotlin.collections.ArrayList
|
||||
|
||||
object VCardAction : IntentAction() {
|
||||
override val iconResId: Int
|
||||
get() = R.drawable.ic_action_vcard
|
||||
override val titleResId: Int
|
||||
get() = R.string.vcard_add
|
||||
override val errorMsg: Int
|
||||
get() = R.string.vcard_failed
|
||||
|
||||
override fun canExecuteOn(data: ByteArray): Boolean {
|
||||
return VTypeParser.parseVType(String(data)) == "VCARD"
|
||||
}
|
||||
|
||||
override suspend fun createIntent(context: Context, data: ByteArray): Intent? {
|
||||
val info = VTypeParser.parseMap(String(data))
|
||||
|
||||
return Intent(Intent.ACTION_INSERT_OR_EDIT).apply {
|
||||
type = ContactsContract.Contacts.CONTENT_ITEM_TYPE
|
||||
info["FN"]?.singleOrNull()?.also { name ->
|
||||
putExtra(ContactsContract.Intents.Insert.NAME, name.value)
|
||||
} ?: info["N"]?.singleOrNull()?.also { name ->
|
||||
putExtra(ContactsContract.Intents.Insert.NAME, name.value.nameFormat)
|
||||
}
|
||||
info["TEL"]?.filter { it.value.isNotEmpty() }?.forEachIndexed { index, phoneProperty ->
|
||||
val (extraPhoneType, extraPhone) = when (index) {
|
||||
0 -> ContactsContract.Intents.Insert.PHONE_TYPE to
|
||||
ContactsContract.Intents.Insert.PHONE
|
||||
1 -> ContactsContract.Intents.Insert.SECONDARY_PHONE_TYPE to
|
||||
ContactsContract.Intents.Insert.SECONDARY_PHONE
|
||||
2 -> ContactsContract.Intents.Insert.TERTIARY_PHONE_TYPE to
|
||||
ContactsContract.Intents.Insert.TERTIARY_PHONE_TYPE
|
||||
else -> return@forEachIndexed
|
||||
}
|
||||
phoneProperty.firstTypeOrFirstInfo?.also {
|
||||
it.phoneType?.also { phoneType ->
|
||||
putExtra(extraPhoneType, phoneType)
|
||||
} ?: putExtra(extraPhoneType, it)
|
||||
}
|
||||
putExtra(
|
||||
extraPhone,
|
||||
phoneProperty.value.toLowerCase(Locale.US).removePrefix("tel:")
|
||||
)
|
||||
}
|
||||
info["EMAIL"]?.forEachIndexed { index, mailProperty ->
|
||||
val (extraMailType, extraMail) = when (index) {
|
||||
0 -> ContactsContract.Intents.Insert.EMAIL_TYPE to
|
||||
ContactsContract.Intents.Insert.EMAIL
|
||||
1 -> ContactsContract.Intents.Insert.SECONDARY_EMAIL_TYPE to
|
||||
ContactsContract.Intents.Insert.SECONDARY_EMAIL
|
||||
2 -> ContactsContract.Intents.Insert.TERTIARY_EMAIL_TYPE to
|
||||
ContactsContract.Intents.Insert.TERTIARY_EMAIL
|
||||
else -> return@forEachIndexed
|
||||
}
|
||||
mailProperty.firstTypeOrFirstInfo?.also {
|
||||
it.mailType?.also { mailType ->
|
||||
putExtra(extraMailType, mailType)
|
||||
} ?: putExtra(extraMailType, it)
|
||||
}
|
||||
putExtra(extraMail, mailProperty.value)
|
||||
}
|
||||
info["ORG"]?.singleOrNull()?.also { organization ->
|
||||
putExtra(ContactsContract.Intents.Insert.COMPANY, organization.value)
|
||||
}
|
||||
info["TITLE"]?.singleOrNull()?.also { jobTitle ->
|
||||
putExtra(ContactsContract.Intents.Insert.JOB_TITLE, jobTitle.value)
|
||||
}
|
||||
info["ADR"]?.forEach { addr ->
|
||||
addr.firstTypeOrFirstInfo?.also {
|
||||
it.addressType?.also { addressType ->
|
||||
putExtra(ContactsContract.Intents.Insert.POSTAL_TYPE, addressType)
|
||||
} ?: putExtra(ContactsContract.Intents.Insert.POSTAL_TYPE, it)
|
||||
}
|
||||
putExtra(ContactsContract.Intents.Insert.POSTAL, addr.value.locationFormat)
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
|
||||
val urls = info["URL"]?.mapNotNull {
|
||||
it.value.takeIf { url -> url.isNotBlank() }?.let { url ->
|
||||
ContentValues(2).apply {
|
||||
put(
|
||||
ContactsContract.Data.MIMETYPE,
|
||||
ContactsContract.CommonDataKinds.Website.CONTENT_ITEM_TYPE
|
||||
)
|
||||
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())
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val String.locationFormat: String
|
||||
get() = """^([^;]*?);([^;]*?);([^;]*?);([^;]*?);([^;]*?);([^;]*?);([^;]*?)$""".toRegex()
|
||||
.matchEntire(
|
||||
this
|
||||
)?.groupValues?.let {
|
||||
listOf(
|
||||
it[1],
|
||||
it[2],
|
||||
it[3],
|
||||
"${it[4]} ${it[6]}",
|
||||
"${it[5]} ${it[7]}"
|
||||
).filter { line -> line.isNotBlank() }
|
||||
.joinToString("\n").takeIf { location -> location.isNotBlank() }
|
||||
} ?: this
|
||||
|
||||
private val String.nameFormat: String
|
||||
get() = """^([^;]*?);([^;]*?);([^;]*?);([^;]*?);([^;]*?)$""".toRegex()
|
||||
.matchEntire(this)?.groupValues?.let {
|
||||
listOf(it[4], it[2], it[3], it[1], it[5]).joinToString(" ")
|
||||
.takeIf { name -> name.isNotBlank() }
|
||||
} ?: this
|
||||
|
||||
private val String.mailType: Int?
|
||||
get() = when (this.toUpperCase(Locale.US)) {
|
||||
"HOME" -> ContactsContract.CommonDataKinds.Email.TYPE_HOME
|
||||
"WORK" -> ContactsContract.CommonDataKinds.Email.TYPE_WORK
|
||||
"OTHER" -> ContactsContract.CommonDataKinds.Email.TYPE_OTHER
|
||||
"MOBILE" -> ContactsContract.CommonDataKinds.Email.TYPE_MOBILE
|
||||
else -> null
|
||||
}
|
||||
|
||||
private val String.phoneType: Int?
|
||||
get() = when (this.toUpperCase(Locale.US)) {
|
||||
"HOME" -> ContactsContract.CommonDataKinds.Phone.TYPE_HOME
|
||||
"MOBILE", "CELL" -> ContactsContract.CommonDataKinds.Phone.TYPE_MOBILE
|
||||
"WORK" -> ContactsContract.CommonDataKinds.Phone.TYPE_WORK
|
||||
"FAX_WORK" -> ContactsContract.CommonDataKinds.Phone.TYPE_FAX_WORK
|
||||
"FAX_HOME" -> ContactsContract.CommonDataKinds.Phone.TYPE_FAX_HOME
|
||||
"PAGER" -> ContactsContract.CommonDataKinds.Phone.TYPE_PAGER
|
||||
"OTHER" -> ContactsContract.CommonDataKinds.Phone.TYPE_OTHER
|
||||
"CALLBACK" -> ContactsContract.CommonDataKinds.Phone.TYPE_CALLBACK
|
||||
"CAR" -> ContactsContract.CommonDataKinds.Phone.TYPE_CAR
|
||||
"COMPANY_MAIN" -> ContactsContract.CommonDataKinds.Phone.TYPE_COMPANY_MAIN
|
||||
"ISDN" -> ContactsContract.CommonDataKinds.Phone.TYPE_ISDN
|
||||
"MAIN" -> ContactsContract.CommonDataKinds.Phone.TYPE_MAIN
|
||||
"OTHER_FAX" -> ContactsContract.CommonDataKinds.Phone.TYPE_OTHER_FAX
|
||||
"RADIO" -> ContactsContract.CommonDataKinds.Phone.TYPE_RADIO
|
||||
"TELEX" -> ContactsContract.CommonDataKinds.Phone.TYPE_TELEX
|
||||
"TTY_TDD" -> ContactsContract.CommonDataKinds.Phone.TYPE_TTY_TDD
|
||||
"WORK_MOBILE" -> ContactsContract.CommonDataKinds.Phone.TYPE_WORK_MOBILE
|
||||
"WORK_PAGER" -> ContactsContract.CommonDataKinds.Phone.TYPE_WORK_PAGER
|
||||
"ASSISTANT" -> ContactsContract.CommonDataKinds.Phone.TYPE_ASSISTANT
|
||||
"MMS" -> ContactsContract.CommonDataKinds.Phone.TYPE_MMS
|
||||
else -> null
|
||||
}
|
||||
|
||||
private val String.addressType: Int?
|
||||
get() = when (this.toUpperCase(Locale.US)) {
|
||||
"HOME" -> ContactsContract.CommonDataKinds.StructuredPostal.TYPE_HOME
|
||||
"WORK" -> ContactsContract.CommonDataKinds.StructuredPostal.TYPE_WORK
|
||||
"OTHER" -> ContactsContract.CommonDataKinds.StructuredPostal.TYPE_OTHER
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package de.markusfisch.android.binaryeye.actions.vtype.vevent
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.provider.CalendarContract
|
||||
import android.support.annotation.RequiresApi
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.actions.IntentAction
|
||||
import de.markusfisch.android.binaryeye.actions.vtype.VTypeParser
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
|
||||
object VEventAction : IntentAction() {
|
||||
override val iconResId: Int
|
||||
get() = R.drawable.ic_action_vevent
|
||||
override val titleResId: Int
|
||||
get() = R.string.vevent_add
|
||||
override val errorMsg: Int
|
||||
get() = R.string.vevent_failed
|
||||
|
||||
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))
|
||||
return type == "VEVENT" || type == "VCALENDAR"
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
|
||||
override suspend fun createIntent(context: Context, data: ByteArray): Intent? {
|
||||
val info = VTypeParser.parseMap(String(data))
|
||||
|
||||
return Intent(Intent.ACTION_EDIT).apply {
|
||||
type = "vnd.android.cursor.item/event"
|
||||
info["SUMMARY"]?.singleOrNull()?.also { title ->
|
||||
putExtra(CalendarContract.Events.TITLE, title.value)
|
||||
}
|
||||
info["DESCRIPTION"]?.singleOrNull()?.also { description ->
|
||||
putExtra(CalendarContract.Events.DESCRIPTION, description.value)
|
||||
}
|
||||
info["LOCATION"]?.singleOrNull()?.also { location ->
|
||||
putExtra(CalendarContract.Events.EVENT_LOCATION, location.value)
|
||||
}
|
||||
info["DTSTART"]?.singleOrNull()?.also { eventStart ->
|
||||
dateFormats.simpleFindParse(eventStart.value)?.also {
|
||||
putExtra(CalendarContract.EXTRA_EVENT_BEGIN_TIME, it.time)
|
||||
}
|
||||
}
|
||||
info["DTEND"]?.singleOrNull()?.also { eventEnd ->
|
||||
dateFormats.simpleFindParse(eventEnd.value)?.also {
|
||||
putExtra(CalendarContract.EXTRA_EVENT_END_TIME, it.time)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("SimpleDateFormat") // we definitely don't wan't the local format
|
||||
private val dateFormats = listOf(
|
||||
SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"),
|
||||
SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss"),
|
||||
SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'"),
|
||||
SimpleDateFormat("yyyyMMdd'T'HHmmss"),
|
||||
SimpleDateFormat("yyyy-MM-dd"),
|
||||
SimpleDateFormat("yyyyMMdd")
|
||||
)
|
||||
|
||||
private fun List<SimpleDateFormat>.simpleFindParse(data: String): Date? {
|
||||
for (simpleDataFormat in this) {
|
||||
return simpleDataFormat.simpleParse(data) ?: continue
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun SimpleDateFormat.simpleParse(data: String): Date? = try {
|
||||
this.parse(data)
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package de.markusfisch.android.binaryeye.actions.web
|
||||
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.actions.SchemeAction
|
||||
|
||||
object WebAction : SchemeAction() {
|
||||
override val iconResId: Int = R.drawable.ic_action_open
|
||||
override val titleResId: Int = R.string.open_url
|
||||
override val scheme: String = "https?"
|
||||
override val buildRegex: Boolean = true
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package de.markusfisch.android.binaryeye.actions.wifi
|
||||
|
||||
import android.content.Context
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.actions.IAction
|
||||
import de.markusfisch.android.binaryeye.widget.toast
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
object WifiAction : IAction {
|
||||
override val iconResId = R.drawable.ic_action_wifi
|
||||
override val titleResId = R.string.connect_to_wifi
|
||||
|
||||
var password: String? = null
|
||||
|
||||
override fun canExecuteOn(data: ByteArray): Boolean =
|
||||
WifiConnector.parse(String(data)) {
|
||||
password = it
|
||||
} != null
|
||||
|
||||
override suspend fun execute(
|
||||
context: Context,
|
||||
data: ByteArray
|
||||
) = withContext(Dispatchers.IO) {
|
||||
val wifiConfig = WifiConnector.parse(
|
||||
String(data)
|
||||
) ?: return@withContext
|
||||
val message = WifiConnector.addNetwork(context, wifiConfig)
|
||||
withContext(Dispatchers.Main) {
|
||||
context.toast(message)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,370 @@
|
||||
package de.markusfisch.android.binaryeye.actions.wifi
|
||||
|
||||
import android.content.Context
|
||||
import android.net.wifi.WifiConfiguration
|
||||
import android.net.wifi.WifiEnterpriseConfig
|
||||
import android.net.wifi.WifiManager
|
||||
import android.net.wifi.WifiNetworkSuggestion
|
||||
import android.os.Build
|
||||
import android.support.annotation.RequiresApi
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Normal:
|
||||
* WIFI:S:[network SSID];T:<WPA|WEP|nopass|>;P:[network password];H:<true|false|>;;
|
||||
*
|
||||
* WPA2 enterprise (EAP):
|
||||
* WIFI:S:[network SSID];T:WPA2-EAP;H:<true|false|nopass|>;E:[EAP method];PH2:[Phase 2 method];AI:[anonymous identity];I:[identity];P:[password];;
|
||||
*
|
||||
* EPA methods:
|
||||
* "AKA", "AKA_PRIME",
|
||||
* "NONE", "PEAP",
|
||||
* "PWD", "SIM",
|
||||
* "TLS", "TTLS",
|
||||
* "UNAUTH_TLS"
|
||||
* https://developer.android.com/reference/android/net/wifi/WifiEnterpriseConfig.Eap.html
|
||||
*
|
||||
* Phase 2 methods:
|
||||
* "AKA", "AKA_PRIME",
|
||||
* "GTC", "MSCHAP",
|
||||
* "MSCHAPV2", "NONE",
|
||||
* "PAP", "SIM"
|
||||
* https://developer.android.com/reference/android/net/wifi/WifiEnterpriseConfig.Phase2.html
|
||||
*
|
||||
* The fields can appear in any order. Only "S:" is required.
|
||||
*/
|
||||
object WifiConnector {
|
||||
fun parse(
|
||||
input: String,
|
||||
passwordBlock: ((password: String?) -> Unit)? = null
|
||||
): Any? {
|
||||
val inputMap = parseMap(input) ?: return null
|
||||
val parsedData = SimpleDataAccessor.of(inputMap) ?: return null
|
||||
passwordBlock?.apply {
|
||||
invoke(parsedData.password)
|
||||
}
|
||||
return if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
||||
// WifiConfiguration is deprecated in Android Q
|
||||
@Suppress("DEPRECATION")
|
||||
WifiConfiguration().apply(parsedData)
|
||||
} else {
|
||||
WifiNetworkSuggestion.Builder().apply(parsedData)
|
||||
}
|
||||
}
|
||||
|
||||
fun addNetwork(context: Context, config: Any): Int {
|
||||
val wifiManager = context.applicationContext.getSystemService(
|
||||
Context.WIFI_SERVICE
|
||||
) as WifiManager
|
||||
return if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
||||
// WifiConfiguration is deprecated in Android Q
|
||||
@Suppress("DEPRECATION")
|
||||
val wifiConfig = config as WifiConfiguration
|
||||
if (wifiManager.enableWifi() &&
|
||||
wifiManager.removeOldNetwork(wifiConfig) &&
|
||||
wifiManager.enableNewNetwork(wifiConfig)
|
||||
) {
|
||||
R.string.wifi_added
|
||||
} else {
|
||||
R.string.wifi_config_failed
|
||||
}
|
||||
} else {
|
||||
val suggestion = (config as WifiNetworkSuggestion.Builder).build()
|
||||
val suggestions = listOf(suggestion)
|
||||
// remove previous conflicting network suggestion
|
||||
wifiManager.removeNetworkSuggestions(suggestions)
|
||||
val result = wifiManager.addNetworkSuggestions(suggestions)
|
||||
if (result == WifiManager.STATUS_NETWORK_SUGGESTIONS_SUCCESS) {
|
||||
R.string.wifi_added
|
||||
} else {
|
||||
R.string.wifi_config_failed
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal fun parseMap(string: String): Map<String, String>? {
|
||||
// normally those codes should have the last semicolon, but many
|
||||
// generators don't add it
|
||||
val wifiRegex = """^WIFI:((?:.+?:(?:[^\\;]|\\.)*;)+);?$""".toRegex()
|
||||
// should be: ^(.+):((?:[^\\;,":]|\\.)*);$ but allows unescaped , "
|
||||
// and : because many QR Code creators don't escape properly
|
||||
val pairRegex = """(.+?):((?:[^\\;]|\\.)*);""".toRegex()
|
||||
return wifiRegex.matchEntire(
|
||||
string
|
||||
)?.groupValues?.get(1)?.let { pairs ->
|
||||
pairRegex.findAll(pairs).map { pair ->
|
||||
pair.groupValues[1].toUpperCase(Locale.US) to pair.groupValues[2]
|
||||
}.toMap()
|
||||
}
|
||||
}
|
||||
|
||||
internal class SimpleDataAccessor private constructor(
|
||||
private val inputMap: Map<String, String>
|
||||
) {
|
||||
// this should be private but because of testing it isn't possible
|
||||
internal val ssid: String
|
||||
get() = inputMap.getValue("S").unescape
|
||||
internal val securityType: String
|
||||
get() = inputMap["T"]?.unescape ?: ""
|
||||
internal val password: String?
|
||||
get() = inputMap["P"]?.unescape
|
||||
internal val hidden: Boolean
|
||||
get() = inputMap["H"]?.unescape == "true"
|
||||
internal val anonymousIdentity: String
|
||||
get() = inputMap["AI"]?.unescape ?: ""
|
||||
internal val identity: String
|
||||
get() = inputMap["I"]?.unescape ?: ""
|
||||
internal val eapMethod: Int?
|
||||
get() = if (inputMap["E"].isNullOrEmpty()) {
|
||||
requireSdk(Build.VERSION_CODES.JELLY_BEAN_MR2) {
|
||||
WifiEnterpriseConfig.Eap.NONE
|
||||
}
|
||||
} else when (inputMap["E"]) {
|
||||
"AKA" -> requireSdk(Build.VERSION_CODES.LOLLIPOP) { WifiEnterpriseConfig.Eap.AKA }
|
||||
"AKA_PRIME" -> requireSdk(Build.VERSION_CODES.M) { WifiEnterpriseConfig.Eap.AKA_PRIME }
|
||||
"NONE" -> requireSdk(Build.VERSION_CODES.JELLY_BEAN_MR2) { WifiEnterpriseConfig.Eap.NONE }
|
||||
"PEAP" -> requireSdk(Build.VERSION_CODES.JELLY_BEAN_MR2) { WifiEnterpriseConfig.Eap.PEAP }
|
||||
"PWD" -> requireSdk(Build.VERSION_CODES.JELLY_BEAN_MR2) { WifiEnterpriseConfig.Eap.PWD }
|
||||
"SIM" -> requireSdk(Build.VERSION_CODES.LOLLIPOP) { WifiEnterpriseConfig.Eap.SIM }
|
||||
"TLS" -> requireSdk(Build.VERSION_CODES.JELLY_BEAN_MR2) { WifiEnterpriseConfig.Eap.TLS }
|
||||
"TTLS" -> requireSdk(Build.VERSION_CODES.JELLY_BEAN_MR2) { WifiEnterpriseConfig.Eap.TTLS }
|
||||
"UNAUTH_TLS" -> requireSdk(Build.VERSION_CODES.N) { WifiEnterpriseConfig.Eap.UNAUTH_TLS }
|
||||
else -> null
|
||||
}
|
||||
internal val phase2Method: Int?
|
||||
get() = if (inputMap["PH2"].isNullOrEmpty()) {
|
||||
requireSdk(Build.VERSION_CODES.JELLY_BEAN_MR2) {
|
||||
WifiEnterpriseConfig.Phase2.NONE
|
||||
}
|
||||
} else when (inputMap["PH2"]) {
|
||||
"AKA" -> requireSdk(Build.VERSION_CODES.O) { WifiEnterpriseConfig.Phase2.AKA }
|
||||
"AKA_PRIME" -> requireSdk(Build.VERSION_CODES.O) { WifiEnterpriseConfig.Phase2.AKA_PRIME }
|
||||
"GTC" -> requireSdk(Build.VERSION_CODES.JELLY_BEAN_MR2) { WifiEnterpriseConfig.Phase2.GTC }
|
||||
"MSCHAP" -> requireSdk(Build.VERSION_CODES.JELLY_BEAN_MR2) { WifiEnterpriseConfig.Phase2.MSCHAP }
|
||||
"MSCHAPV2" -> requireSdk(Build.VERSION_CODES.JELLY_BEAN_MR2) { WifiEnterpriseConfig.Phase2.MSCHAPV2 }
|
||||
"NONE" -> requireSdk(Build.VERSION_CODES.JELLY_BEAN_MR2) { WifiEnterpriseConfig.Phase2.NONE }
|
||||
"PAP" -> requireSdk(Build.VERSION_CODES.JELLY_BEAN_MR2) { WifiEnterpriseConfig.Phase2.PAP }
|
||||
"SIM" -> requireSdk(Build.VERSION_CODES.O) { WifiEnterpriseConfig.Phase2.SIM }
|
||||
else -> null
|
||||
}
|
||||
|
||||
internal companion object {
|
||||
internal fun of(inputMap: Map<String, String>): SimpleDataAccessor? {
|
||||
return SimpleDataAccessor(inputMap).takeUnless {
|
||||
inputMap["S"].isNullOrEmpty()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.Q)
|
||||
private fun WifiNetworkSuggestion.Builder.apply(
|
||||
data: SimpleDataAccessor
|
||||
): WifiNetworkSuggestion.Builder? {
|
||||
fun WifiNetworkSuggestion.Builder.applyCommon(
|
||||
data: SimpleDataAccessor
|
||||
): WifiNetworkSuggestion.Builder? {
|
||||
setSsid(data.ssid)
|
||||
return this
|
||||
}
|
||||
|
||||
fun WifiNetworkSuggestion.Builder.applySecurity(
|
||||
data: SimpleDataAccessor
|
||||
): WifiNetworkSuggestion.Builder? {
|
||||
when (data.securityType) {
|
||||
"WPA", "WPA2" -> {
|
||||
data.password?.let {
|
||||
setWpa2Passphrase(it)
|
||||
}
|
||||
}
|
||||
"WPA2-EAP" -> {
|
||||
data.password?.let {
|
||||
setWpa2Passphrase(it)
|
||||
}
|
||||
setWpa2EnterpriseConfig(WifiEnterpriseConfig().apply {
|
||||
identity = data.identity
|
||||
anonymousIdentity = data.anonymousIdentity
|
||||
password = data.password
|
||||
data.eapMethod?.let { eapMethod = it }
|
||||
data.phase2Method?.let { phase2Method = it }
|
||||
})
|
||||
}
|
||||
"WPA3" -> {
|
||||
data.password?.let {
|
||||
setWpa3Passphrase(it)
|
||||
}
|
||||
}
|
||||
"WPA3-EAP" -> {
|
||||
data.password?.let { setWpa3Passphrase(it) }
|
||||
setWpa3EnterpriseConfig(WifiEnterpriseConfig().apply {
|
||||
identity = data.identity
|
||||
anonymousIdentity = data.anonymousIdentity
|
||||
password = data.password
|
||||
data.eapMethod?.let { eapMethod = it }
|
||||
data.phase2Method?.let { phase2Method = it }
|
||||
})
|
||||
}
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
return this.applyCommon(data)?.applySecurity(data)
|
||||
}
|
||||
|
||||
// WifiConfiguration is deprecated in Android Q
|
||||
@Suppress("DEPRECATION")
|
||||
private fun WifiConfiguration.apply(
|
||||
data: SimpleDataAccessor
|
||||
): WifiConfiguration? {
|
||||
val ssidWithQuotes = data.ssid.quote
|
||||
val passwordWithQuotes = data.password?.quoteUnlessHex
|
||||
|
||||
fun WifiConfiguration.applyCommon(
|
||||
data: SimpleDataAccessor
|
||||
): WifiConfiguration? {
|
||||
allowedAuthAlgorithms.clear()
|
||||
allowedGroupCiphers.clear()
|
||||
allowedKeyManagement.clear()
|
||||
allowedPairwiseCiphers.clear()
|
||||
allowedProtocols.clear()
|
||||
|
||||
SSID = ssidWithQuotes
|
||||
hiddenSSID = data.hidden
|
||||
return this
|
||||
}
|
||||
|
||||
fun WifiConfiguration.applySecurity(
|
||||
data: SimpleDataAccessor
|
||||
): WifiConfiguration? {
|
||||
when (data.securityType) {
|
||||
"", "nopass" -> allowedKeyManagement.set(WifiConfiguration.KeyMgmt.NONE)
|
||||
"WEP" -> @Suppress("DEPRECATION") /* WEP as insecure */ {
|
||||
passwordWithQuotes?.let {
|
||||
wepKeys[0] = it
|
||||
} ?: return null
|
||||
wepTxKeyIndex = 0
|
||||
allowedAuthAlgorithms.set(WifiConfiguration.AuthAlgorithm.SHARED)
|
||||
allowedKeyManagement.set(WifiConfiguration.KeyMgmt.NONE)
|
||||
allowedGroupCiphers.set(WifiConfiguration.GroupCipher.TKIP)
|
||||
allowedGroupCiphers.set(WifiConfiguration.GroupCipher.CCMP)
|
||||
allowedGroupCiphers.set(WifiConfiguration.GroupCipher.WEP40)
|
||||
allowedGroupCiphers.set(WifiConfiguration.GroupCipher.WEP104)
|
||||
}
|
||||
"WPA" -> {
|
||||
passwordWithQuotes?.let {
|
||||
preSharedKey = it
|
||||
} ?: return null
|
||||
allowedAuthAlgorithms.set(WifiConfiguration.AuthAlgorithm.OPEN)
|
||||
@Suppress("DEPRECATION") // WPA 1 is insecure and has bad performance
|
||||
allowedProtocols.set(WifiConfiguration.Protocol.WPA) // WPA
|
||||
allowedProtocols.set(WifiConfiguration.Protocol.RSN) // WPA2
|
||||
allowedKeyManagement.set(WifiConfiguration.KeyMgmt.WPA_PSK)
|
||||
allowedKeyManagement.set(WifiConfiguration.KeyMgmt.WPA_EAP)
|
||||
@Suppress("DEPRECATION") // TKIP is insecure and has bad performance
|
||||
allowedPairwiseCiphers.set(WifiConfiguration.PairwiseCipher.TKIP)
|
||||
allowedPairwiseCiphers.set(WifiConfiguration.PairwiseCipher.CCMP)
|
||||
allowedGroupCiphers.set(WifiConfiguration.GroupCipher.TKIP)
|
||||
allowedGroupCiphers.set(WifiConfiguration.GroupCipher.CCMP)
|
||||
}
|
||||
"WPA2-EAP" -> requireSdk(Build.VERSION_CODES.JELLY_BEAN_MR2) {
|
||||
passwordWithQuotes?.let {
|
||||
preSharedKey = it
|
||||
} ?: return null
|
||||
allowedAuthAlgorithms.set(WifiConfiguration.AuthAlgorithm.OPEN)
|
||||
allowedProtocols.set(WifiConfiguration.Protocol.RSN) // WPA2
|
||||
allowedKeyManagement.set(WifiConfiguration.KeyMgmt.WPA_EAP)
|
||||
@Suppress("DEPRECATION") // TKIP is insecure and has bad performance
|
||||
allowedPairwiseCiphers.set(WifiConfiguration.PairwiseCipher.TKIP)
|
||||
allowedPairwiseCiphers.set(WifiConfiguration.PairwiseCipher.CCMP)
|
||||
allowedGroupCiphers.set(WifiConfiguration.GroupCipher.TKIP)
|
||||
allowedGroupCiphers.set(WifiConfiguration.GroupCipher.CCMP)
|
||||
|
||||
enterpriseConfig.identity = data.identity
|
||||
enterpriseConfig.anonymousIdentity = data.anonymousIdentity
|
||||
enterpriseConfig.password = data.password
|
||||
enterpriseConfig.eapMethod =
|
||||
data.eapMethod ?: return null // non valid eapMethod
|
||||
enterpriseConfig.phase2Method =
|
||||
data.phase2Method ?: return null // non valid phase2Method
|
||||
} ?: return null // api isn't high enough
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
return this.applyCommon(data)?.applySecurity(data)
|
||||
}
|
||||
|
||||
private inline fun <T> requireSdk(version: Int, block: () -> T): T? {
|
||||
return if (Build.VERSION.SDK_INT >= version) return block() else null
|
||||
}
|
||||
}
|
||||
|
||||
// keep possibility of wrongly unescaped \ by explicitly searching
|
||||
// for special chars
|
||||
private val escapedRegex = """\\([\\;,":])""".toRegex()
|
||||
private val String.unescape: String
|
||||
get() = this.replace(escapedRegex) { escaped ->
|
||||
escaped.groupValues[1]
|
||||
}
|
||||
|
||||
private val String.quote: String
|
||||
get() = if (startsWith("\"") && endsWith("\"")) this else "\"$this\""
|
||||
|
||||
private val String.quoteUnlessHex: String
|
||||
get() = if (isHex()) this else this.quote
|
||||
|
||||
// Unfortunately, we can never know whether a string consisting only
|
||||
// of 0-f is a hex string. So we just make this one assumption that
|
||||
// a string of 0-f that is exactly 64 characters long, is _probably_
|
||||
// a hex string for raw PSK. Of course, this may fail for any string
|
||||
// that just happens to be 64 characters long and contains just 0-f.
|
||||
private val hexRegex = """^[0-9a-f]+$""".toRegex(
|
||||
RegexOption.IGNORE_CASE
|
||||
)
|
||||
|
||||
private fun String.isHex() = length == 64 && matches(hexRegex)
|
||||
|
||||
private fun WifiManager.enableWifi(): Boolean {
|
||||
// setWifiEnabled() will always return false for Android Q
|
||||
// because Q doesn't allow apps to enable/disable Wi-Fi anymore
|
||||
@Suppress("DEPRECATION")
|
||||
return isWifiEnabled || setWifiEnabled(true)
|
||||
}
|
||||
|
||||
// WifiConfiguration is deprecated in Android Q
|
||||
@Suppress("DEPRECATION")
|
||||
private fun WifiManager.removeOldNetwork(
|
||||
wifiConfig: WifiConfiguration
|
||||
): Boolean {
|
||||
try {
|
||||
configuredNetworks?.firstOrNull {
|
||||
it.SSID == wifiConfig.SSID &&
|
||||
it.allowedKeyManagement == wifiConfig.allowedKeyManagement
|
||||
}?.networkId?.also {
|
||||
removeNetwork(it)
|
||||
}
|
||||
} catch (e: SecurityException) {
|
||||
// the user didn't allow ACCESS_FINE_LOCATION which is
|
||||
// required to access configuredNetworks and that's fine
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// WifiConfiguration is deprecated in Android Q
|
||||
@Suppress("DEPRECATION")
|
||||
private fun WifiManager.enableNewNetwork(
|
||||
wifiConfig: WifiConfiguration
|
||||
): Boolean {
|
||||
val id = addNetwork(wifiConfig)
|
||||
if (id == -1) {
|
||||
return false
|
||||
}
|
||||
disconnect()
|
||||
return if (enableNetwork(id, true)) {
|
||||
reconnect()
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
@@ -1,62 +1,77 @@
|
||||
package de.markusfisch.android.binaryeye.activity
|
||||
|
||||
import com.google.zxing.Result
|
||||
|
||||
import de.markusfisch.android.cameraview.widget.CameraView
|
||||
|
||||
import de.markusfisch.android.binaryeye.app.initSystemBars
|
||||
import de.markusfisch.android.binaryeye.rs.Preprocessor
|
||||
import de.markusfisch.android.binaryeye.zxing.Zxing
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.SharedPreferences
|
||||
import android.graphics.Rect
|
||||
import android.graphics.RectF
|
||||
import android.hardware.Camera
|
||||
import android.media.AudioManager
|
||||
import android.media.ToneGenerator
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.os.Vibrator
|
||||
import android.preference.PreferenceManager
|
||||
import android.support.v4.app.ActivityCompat
|
||||
import android.support.v4.content.ContextCompat
|
||||
import android.support.design.widget.FloatingActionButton
|
||||
import android.support.v7.app.AppCompatActivity
|
||||
import android.support.v7.widget.Toolbar
|
||||
import android.support.v8.renderscript.RSRuntimeException
|
||||
import android.support.v8.renderscript.RenderScript
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.widget.SeekBar
|
||||
import android.widget.Toast
|
||||
import com.google.zxing.Result
|
||||
import com.google.zxing.ResultMetadataType
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.app.*
|
||||
import de.markusfisch.android.binaryeye.database.Scan
|
||||
import de.markusfisch.android.binaryeye.graphics.Mapping
|
||||
import de.markusfisch.android.binaryeye.graphics.frameToView
|
||||
import de.markusfisch.android.binaryeye.graphics.isPortrait
|
||||
import de.markusfisch.android.binaryeye.net.sendAsync
|
||||
import de.markusfisch.android.binaryeye.rs.Preprocessor
|
||||
import de.markusfisch.android.binaryeye.view.setPaddingFromWindowInsets
|
||||
import de.markusfisch.android.binaryeye.widget.DetectorView
|
||||
import de.markusfisch.android.binaryeye.widget.toast
|
||||
import de.markusfisch.android.binaryeye.zxing.Zxing
|
||||
import de.markusfisch.android.cameraview.widget.CameraView
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
class CameraActivity : AppCompatActivity() {
|
||||
private val zxing = Zxing()
|
||||
private val decodingRunnable = Runnable {
|
||||
while (decoding) {
|
||||
val result = decodeFrame()
|
||||
if (result != null) {
|
||||
cameraView.post { found(result) }
|
||||
decoding = false
|
||||
break
|
||||
private val zxing = Zxing { point ->
|
||||
point?.let {
|
||||
getMapping()?.map(it)?.let {
|
||||
detectorView.post {
|
||||
detectorView.mark(listOf(it))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private lateinit var preferences: SharedPreferences
|
||||
private lateinit var rs: RenderScript
|
||||
private lateinit var vibrator: Vibrator
|
||||
private lateinit var cameraView: CameraView
|
||||
private lateinit var detectorView: DetectorView
|
||||
private lateinit var zoomBar: SeekBar
|
||||
private lateinit var flashFab: FloatingActionButton
|
||||
|
||||
private var decoding = false
|
||||
private var decodingThread: Thread? = null
|
||||
private var preprocessor: Preprocessor? = null
|
||||
private var frameData: ByteArray? = null
|
||||
private var frameWidth: Int = 0
|
||||
private var frameHeight: Int = 0
|
||||
private var frameOrientation: Int = 0
|
||||
private var nativeMapping: Mapping? = null
|
||||
private var rotatedMapping: Mapping? = null
|
||||
private var recreatePreprocessor = false
|
||||
private var rotate = false
|
||||
private var invert = false
|
||||
private var flash = false
|
||||
private var decoding = true
|
||||
private var returnResult = false
|
||||
private var frontFacing = false
|
||||
private var bulkMode = false
|
||||
private var ignoreNext: String? = null
|
||||
private var fallbackBuffer: IntArray? = null
|
||||
|
||||
override fun onRequestPermissionsResult(
|
||||
requestCode: Int,
|
||||
@@ -64,92 +79,178 @@ class CameraActivity : AppCompatActivity() {
|
||||
grantResults: IntArray
|
||||
) {
|
||||
when (requestCode) {
|
||||
REQUEST_CAMERA -> if (grantResults.size > 0 &&
|
||||
grantResults[0] != PackageManager.PERMISSION_GRANTED) {
|
||||
Toast.makeText(
|
||||
this,
|
||||
R.string.no_camera_no_fun,
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
PERMISSION_CAMERA -> if (grantResults.isNotEmpty() &&
|
||||
grantResults[0] != PackageManager.PERMISSION_GRANTED
|
||||
) {
|
||||
toast(R.string.no_camera_no_fun)
|
||||
finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onActivityResult(
|
||||
requestCode: Int,
|
||||
resultCode: Int,
|
||||
resultData: Intent?
|
||||
) {
|
||||
when (requestCode) {
|
||||
PICK_FILE_RESULT_CODE -> {
|
||||
if (resultCode == Activity.RESULT_OK && resultData != null) {
|
||||
val pick = Intent(this, PickActivity::class.java)
|
||||
pick.action = Intent.ACTION_VIEW
|
||||
pick.setDataAndType(resultData.data, "image/*")
|
||||
startActivity(pick)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun attachBaseContext(base: Context?) {
|
||||
base?.applyLocale(prefs.customLocale)
|
||||
super.attachBaseContext(base)
|
||||
}
|
||||
|
||||
override fun onCreate(state: Bundle?) {
|
||||
super.onCreate(state)
|
||||
setContentView(R.layout.activity_camera)
|
||||
|
||||
// necessary to get the right translation after setting a custom locale
|
||||
setTitle(R.string.scan_code)
|
||||
|
||||
rs = RenderScript.create(this)
|
||||
vibrator = getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
|
||||
|
||||
initSystemBars(this)
|
||||
setSupportActionBar(findViewById(R.id.toolbar) as Toolbar)
|
||||
|
||||
preferences = PreferenceManager.getDefaultSharedPreferences(this)
|
||||
vibrator = getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
|
||||
|
||||
cameraView = findViewById(R.id.camera_view) as CameraView
|
||||
detectorView = findViewById(R.id.detector_view) as DetectorView
|
||||
zoomBar = findViewById(R.id.zoom) as SeekBar
|
||||
flashFab = findViewById(R.id.flash) as FloatingActionButton
|
||||
|
||||
initCameraView()
|
||||
setTapToFocus()
|
||||
initZoomBar()
|
||||
restoreZoom()
|
||||
initFlashFab(findViewById(R.id.flash))
|
||||
initDetectorView()
|
||||
|
||||
if (intent?.action == Intent.ACTION_SEND &&
|
||||
intent.type == "text/plain"
|
||||
) {
|
||||
handleSendText(intent)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
preprocessor?.destroy()
|
||||
resetPreProcessor()
|
||||
fallbackBuffer = null
|
||||
saveZoom()
|
||||
}
|
||||
|
||||
override fun onNewIntent(intent: Intent) {
|
||||
super.onNewIntent(intent)
|
||||
handleSendText(intent)
|
||||
saveCropHandlePos()
|
||||
rs.destroy()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
System.gc()
|
||||
returnResult = "com.google.zxing.client.android.SCAN".equals(
|
||||
intent.action
|
||||
)
|
||||
handleSendText(intent)
|
||||
if (hasCameraPermission()) {
|
||||
cameraView.openAsync(
|
||||
CameraView.findCameraId(
|
||||
Camera.CameraInfo.CAMERA_FACING_BACK
|
||||
)
|
||||
)
|
||||
zxing.updateHints(prefs.tryHarder)
|
||||
returnResult = "com.google.zxing.client.android.SCAN" == intent.action
|
||||
if (hasCameraPermission(this)) {
|
||||
openCamera()
|
||||
}
|
||||
}
|
||||
|
||||
private fun resetPreProcessor() {
|
||||
preprocessor?.destroy()
|
||||
preprocessor = null
|
||||
}
|
||||
|
||||
private fun openCamera() {
|
||||
cameraView.openAsync(
|
||||
CameraView.findCameraId(
|
||||
@Suppress("DEPRECATION")
|
||||
if (frontFacing) {
|
||||
Camera.CameraInfo.CAMERA_FACING_FRONT
|
||||
} else {
|
||||
Camera.CameraInfo.CAMERA_FACING_BACK
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
cancelDecoding()
|
||||
closeCamera()
|
||||
}
|
||||
|
||||
private fun closeCamera() {
|
||||
cameraView.close()
|
||||
// closing the camera will also shut off the flash
|
||||
flash = false
|
||||
}
|
||||
|
||||
override fun onRestoreInstanceState(savedState: Bundle) {
|
||||
super.onRestoreInstanceState(savedState)
|
||||
zoomBar.max = savedState.getInt(ZOOM_MAX)
|
||||
zoomBar.progress = savedState.getInt(ZOOM_LEVEL)
|
||||
frontFacing = savedState.getBoolean(FRONT_FACING)
|
||||
bulkMode = savedState.getBoolean(BULK_MODE)
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
outState.putInt(ZOOM_MAX, zoomBar.max)
|
||||
outState.putInt(ZOOM_LEVEL, zoomBar.progress)
|
||||
outState.putBoolean(FRONT_FACING, frontFacing)
|
||||
outState.putBoolean(BULK_MODE, bulkMode)
|
||||
super.onSaveInstanceState(outState)
|
||||
}
|
||||
|
||||
override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
|
||||
// always give crop handle precedence over other controls
|
||||
// because it can easily overlap and would then be inaccessible
|
||||
if (detectorView.onTouchEvent(ev)) {
|
||||
return true
|
||||
}
|
||||
return super.dispatchTouchEvent(ev)
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||
menuInflater.inflate(R.menu.activity_camera, menu)
|
||||
menu.findItem(R.id.bulk_mode).isChecked = bulkMode
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
return when (item.itemId) {
|
||||
R.id.create -> {
|
||||
startActivity(MainActivity.getEncodeIntent(this))
|
||||
createBarcode()
|
||||
true
|
||||
}
|
||||
R.id.history -> {
|
||||
startActivity(MainActivity.getHistoryIntent(this))
|
||||
true
|
||||
}
|
||||
R.id.pick_file -> {
|
||||
val chooseFile = Intent(Intent.ACTION_GET_CONTENT)
|
||||
chooseFile.type = "image/*"
|
||||
startActivityForResult(
|
||||
Intent.createChooser(
|
||||
chooseFile,
|
||||
getString(R.string.pick_file)
|
||||
),
|
||||
PICK_FILE_RESULT_CODE
|
||||
)
|
||||
true
|
||||
}
|
||||
R.id.switch_camera -> {
|
||||
switchCamera()
|
||||
true
|
||||
}
|
||||
R.id.bulk_mode -> {
|
||||
bulkMode = bulkMode xor true
|
||||
item.isChecked = bulkMode
|
||||
true
|
||||
}
|
||||
R.id.preferences -> {
|
||||
startActivity(MainActivity.getPreferencesIntent(this))
|
||||
true
|
||||
}
|
||||
R.id.info -> {
|
||||
@@ -160,130 +261,151 @@ class CameraActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun createBarcode() {
|
||||
startActivity(MainActivity.getEncodeIntent(this))
|
||||
}
|
||||
|
||||
private fun switchCamera() {
|
||||
closeCamera()
|
||||
frontFacing = frontFacing xor true
|
||||
openCamera()
|
||||
}
|
||||
|
||||
private fun openReadme() {
|
||||
startActivity(
|
||||
Intent(
|
||||
Intent.ACTION_VIEW, Uri.parse(
|
||||
"https://github.com/markusfisch/BinaryEye/blob/master/README.md"
|
||||
)
|
||||
)
|
||||
val intent = Intent(
|
||||
Intent.ACTION_VIEW,
|
||||
Uri.parse(getString(R.string.project_url))
|
||||
)
|
||||
if (intent.resolveActivity(packageManager) != null) {
|
||||
startActivity(intent)
|
||||
} else {
|
||||
toast(R.string.project_url)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleSendText(intent: Intent) {
|
||||
if (!Intent.ACTION_SEND.equals(intent.getAction()) ||
|
||||
!"text/plain".equals(intent.getType())) {
|
||||
return
|
||||
val text = intent.getStringExtra(Intent.EXTRA_TEXT)
|
||||
if (text?.isEmpty() == false) {
|
||||
startActivity(MainActivity.getEncodeIntent(this, text, true))
|
||||
finish()
|
||||
}
|
||||
|
||||
var text = intent.getStringExtra(Intent.EXTRA_TEXT)
|
||||
if (text == null || text.isEmpty()) {
|
||||
return
|
||||
}
|
||||
|
||||
// consume this intent
|
||||
intent.setAction(null)
|
||||
|
||||
startActivity(MainActivity.getEncodeIntent(this, text, true))
|
||||
finish()
|
||||
}
|
||||
|
||||
private fun hasCameraPermission(): Boolean {
|
||||
val permission = android.Manifest.permission.CAMERA
|
||||
|
||||
if (ContextCompat.checkSelfPermission(this, permission) !=
|
||||
PackageManager.PERMISSION_GRANTED) {
|
||||
ActivityCompat.requestPermissions(
|
||||
this, arrayOf(permission),
|
||||
REQUEST_CAMERA
|
||||
)
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
private fun initCameraView() {
|
||||
cameraView.setUseOrientationListener(true)
|
||||
@Suppress("ClickableViewAccessibility")
|
||||
cameraView.setOnTouchListener(object : View.OnTouchListener {
|
||||
var focus = true
|
||||
var offset = -1f
|
||||
var progress = 0
|
||||
|
||||
override fun onTouch(v: View?, event: MotionEvent?): Boolean {
|
||||
event ?: return false
|
||||
val pos = event.y
|
||||
when (event.actionMasked) {
|
||||
MotionEvent.ACTION_DOWN -> {
|
||||
offset = pos
|
||||
progress = zoomBar.progress
|
||||
return true
|
||||
}
|
||||
MotionEvent.ACTION_MOVE -> {
|
||||
if (prefs.zoomBySwiping) {
|
||||
v ?: return false
|
||||
val dist = offset - pos
|
||||
val maxValue = zoomBar.max
|
||||
val change = maxValue / v.height.toFloat() * 2f * dist
|
||||
zoomBar.progress = min(
|
||||
maxValue,
|
||||
max(progress + change.roundToInt(), 0)
|
||||
)
|
||||
return true
|
||||
}
|
||||
}
|
||||
MotionEvent.ACTION_UP -> {
|
||||
// stop calling focusTo() as soon as it returns false
|
||||
// to avoid throwing and catching future exceptions
|
||||
if (focus) {
|
||||
focus = cameraView.focusTo(v, event.x, event.y)
|
||||
if (focus) {
|
||||
v?.performClick()
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
})
|
||||
@Suppress("DEPRECATION")
|
||||
cameraView.setOnCameraListener(object : CameraView.OnCameraListener {
|
||||
override fun onConfigureParameters(
|
||||
parameters: Camera.Parameters
|
||||
) {
|
||||
if (parameters.isZoomSupported()) {
|
||||
val max = parameters.getMaxZoom()
|
||||
if (parameters.isZoomSupported) {
|
||||
val max = parameters.maxZoom
|
||||
if (zoomBar.max != max) {
|
||||
zoomBar.max = max
|
||||
zoomBar.progress = max / 10
|
||||
saveZoom()
|
||||
}
|
||||
parameters.setZoom(zoomBar.progress)
|
||||
parameters.zoom = zoomBar.progress
|
||||
} else {
|
||||
zoomBar.visibility = View.GONE
|
||||
}
|
||||
val sceneModes = parameters.supportedSceneModes
|
||||
sceneModes?.let {
|
||||
for (mode in sceneModes) {
|
||||
if (mode.equals(Camera.Parameters.SCENE_MODE_BARCODE)) {
|
||||
if (mode == Camera.Parameters.SCENE_MODE_BARCODE) {
|
||||
parameters.sceneMode = mode
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
CameraView.setAutoFocus(parameters)
|
||||
updateFlashFab(parameters.flashMode == null)
|
||||
}
|
||||
|
||||
override fun onCameraError() {
|
||||
Toast.makeText(
|
||||
this@CameraActivity,
|
||||
R.string.camera_error,
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
this@CameraActivity.toast(R.string.camera_error)
|
||||
finish()
|
||||
}
|
||||
|
||||
override fun onCameraReady(camera: Camera) {
|
||||
frameWidth = cameraView.frameWidth
|
||||
frameHeight = cameraView.frameHeight
|
||||
frameOrientation = cameraView.frameOrientation
|
||||
camera.setPreviewCallback { data, _ -> frameData = data }
|
||||
// reset preprocessor to make sure it always fits the current
|
||||
// frame orientation; important for landscape to landscape
|
||||
// orientation changes
|
||||
resetPreProcessor()
|
||||
val frameWidth = cameraView.frameWidth
|
||||
val frameHeight = cameraView.frameHeight
|
||||
val frameOrientation = cameraView.frameOrientation
|
||||
ignoreNext = null
|
||||
decoding = true
|
||||
camera.setPreviewCallback { frameData, _ ->
|
||||
if (decoding) {
|
||||
decodeFrame(
|
||||
frameData,
|
||||
frameWidth,
|
||||
frameHeight,
|
||||
frameOrientation
|
||||
)?.let { result ->
|
||||
if (result.text != ignoreNext) {
|
||||
postResult(result)
|
||||
decoding = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPreviewStarted(camera: Camera) {
|
||||
startDecoding()
|
||||
}
|
||||
|
||||
override fun onCameraStopping(camera: Camera) {
|
||||
cancelDecoding()
|
||||
camera.setPreviewCallback(null)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun setTapToFocus() {
|
||||
val runnable = Runnable {
|
||||
cameraView.setFocusArea(null)
|
||||
}
|
||||
cameraView.setOnTouchListener({ v: View, event: MotionEvent ->
|
||||
if (event.getActionMasked() == MotionEvent.ACTION_UP) {
|
||||
val camera = cameraView.camera
|
||||
camera?.cancelAutoFocus()
|
||||
cameraView.setFocusArea(
|
||||
cameraView.calculateFocusRect(
|
||||
event.x,
|
||||
event.y,
|
||||
100
|
||||
)
|
||||
)
|
||||
camera?.autoFocus({ _: Boolean, _: Camera ->
|
||||
cameraView.removeCallbacks(runnable)
|
||||
cameraView.postDelayed(runnable, 3000)
|
||||
})
|
||||
v.performClick()
|
||||
}
|
||||
true
|
||||
})
|
||||
}
|
||||
|
||||
private fun initZoomBar() {
|
||||
zoomBar.setOnSeekBarChangeListener(object :
|
||||
SeekBar.OnSeekBarChangeListener {
|
||||
@@ -299,15 +421,17 @@ class CameraActivity : AppCompatActivity() {
|
||||
|
||||
override fun onStopTrackingTouch(seekBar: SeekBar) {}
|
||||
})
|
||||
restoreZoom()
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
private fun setZoom(zoom: Int) {
|
||||
val camera: Camera? = cameraView.getCamera()
|
||||
val camera: Camera? = cameraView.camera
|
||||
camera?.let {
|
||||
try {
|
||||
val params = camera.getParameters()
|
||||
params.setZoom(zoom)
|
||||
camera.setParameters(params)
|
||||
val params = camera.parameters
|
||||
params.zoom = zoom
|
||||
camera.parameters = params
|
||||
} catch (e: RuntimeException) {
|
||||
// ignore; there's nothing we can do
|
||||
}
|
||||
@@ -315,113 +439,336 @@ class CameraActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
private fun saveZoom() {
|
||||
val editor = preferences.edit()
|
||||
val editor = prefs.preferences.edit()
|
||||
editor.putInt(ZOOM_MAX, zoomBar.max)
|
||||
editor.putInt(ZOOM_LEVEL, zoomBar.progress)
|
||||
editor.apply()
|
||||
}
|
||||
|
||||
private fun restoreZoom() {
|
||||
zoomBar.max = preferences.getInt(ZOOM_MAX, zoomBar.max)
|
||||
zoomBar.progress = preferences.getInt(ZOOM_LEVEL, zoomBar.progress)
|
||||
}
|
||||
|
||||
private fun initFlashFab(fab: View) {
|
||||
if (!packageManager.hasSystemFeature(
|
||||
PackageManager.FEATURE_CAMERA_FLASH
|
||||
)) {
|
||||
fab.visibility = View.GONE
|
||||
} else {
|
||||
fab.setOnClickListener { _ ->
|
||||
toggleTorchMode()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun toggleTorchMode() {
|
||||
val camera = cameraView.getCamera()
|
||||
val parameters = camera?.getParameters()
|
||||
parameters?.setFlashMode(
|
||||
if (flash) {
|
||||
Camera.Parameters.FLASH_MODE_OFF
|
||||
} else {
|
||||
Camera.Parameters.FLASH_MODE_TORCH
|
||||
}
|
||||
zoomBar.max = prefs.preferences.getInt(ZOOM_MAX, zoomBar.max)
|
||||
zoomBar.progress = prefs.preferences.getInt(
|
||||
ZOOM_LEVEL,
|
||||
zoomBar.progress
|
||||
)
|
||||
flash = flash xor true
|
||||
parameters?.let { camera.setParameters(parameters) }
|
||||
}
|
||||
|
||||
private fun startDecoding() {
|
||||
frameData = null
|
||||
decoding = true
|
||||
decodingThread = Thread(decodingRunnable)
|
||||
decodingThread?.start()
|
||||
private fun initDetectorView() {
|
||||
detectorView.onRoiChange = {
|
||||
decoding = false
|
||||
}
|
||||
detectorView.onRoiChanged = {
|
||||
decoding = true
|
||||
recreatePreprocessor = true
|
||||
}
|
||||
detectorView.setPaddingFromWindowInsets()
|
||||
restoreCropHandlePos()
|
||||
}
|
||||
|
||||
private fun cancelDecoding() {
|
||||
decoding = false
|
||||
if (decodingThread != null) {
|
||||
var retry = 100
|
||||
while (retry-- > 0) {
|
||||
try {
|
||||
decodingThread?.join()
|
||||
break
|
||||
} catch (e: InterruptedException) {
|
||||
decodingThread?.interrupt()
|
||||
}
|
||||
}
|
||||
decodingThread = null
|
||||
private fun saveCropHandlePos() {
|
||||
val pos = detectorView.getCropHandlePos()
|
||||
prefs.cropHandleX = pos.x
|
||||
prefs.cropHandleY = pos.y
|
||||
prefs.cropHandleOrientation = detectorView.currentOrientation
|
||||
}
|
||||
|
||||
private fun restoreCropHandlePos() {
|
||||
detectorView.setCropHandlePos(
|
||||
prefs.cropHandleX,
|
||||
prefs.cropHandleY,
|
||||
prefs.cropHandleOrientation
|
||||
)
|
||||
}
|
||||
|
||||
private fun updateFlashFab(unavailable: Boolean) {
|
||||
if (unavailable) {
|
||||
flashFab.setImageResource(R.drawable.ic_action_create)
|
||||
flashFab.setOnClickListener { createBarcode() }
|
||||
} else {
|
||||
flashFab.setImageResource(R.drawable.ic_action_flash)
|
||||
flashFab.setOnClickListener { toggleTorchMode() }
|
||||
}
|
||||
}
|
||||
|
||||
private fun decodeFrame(): Result? {
|
||||
var fd = frameData
|
||||
fd ?: return null
|
||||
if (preprocessor == null) {
|
||||
preprocessor = Preprocessor(
|
||||
this,
|
||||
@Suppress("DEPRECATION")
|
||||
private fun toggleTorchMode() {
|
||||
val camera = cameraView.camera
|
||||
val parameters = camera?.parameters
|
||||
parameters?.flashMode = if (flash) {
|
||||
Camera.Parameters.FLASH_MODE_OFF
|
||||
} else {
|
||||
Camera.Parameters.FLASH_MODE_TORCH
|
||||
}
|
||||
parameters?.let {
|
||||
try {
|
||||
camera.parameters = parameters
|
||||
flash = flash xor true
|
||||
} catch (e: RuntimeException) {
|
||||
// ignore; there's nothing we can do
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun decodeFrame(
|
||||
frameData: ByteArray?,
|
||||
frameWidth: Int,
|
||||
frameHeight: Int,
|
||||
frameOrientation: Int
|
||||
): Result? {
|
||||
frameData ?: return null
|
||||
rotate = if (prefs.autoRotate) {
|
||||
rotate xor true
|
||||
} else {
|
||||
invert = invert xor true
|
||||
isPortrait(frameOrientation)
|
||||
}
|
||||
return try {
|
||||
if (recreatePreprocessor) {
|
||||
resetPreProcessor()
|
||||
recreatePreprocessor = false
|
||||
}
|
||||
val pp = preprocessor ?: createPreprocessorAndMapping(
|
||||
frameWidth,
|
||||
frameHeight,
|
||||
frameOrientation
|
||||
)
|
||||
val w: Int
|
||||
val h: Int
|
||||
if (rotate) {
|
||||
pp.resizeAndRotate(frameData)
|
||||
w = pp.outHeight
|
||||
h = pp.outWidth
|
||||
} else {
|
||||
invert = invert xor true
|
||||
pp.resizeOnly(frameData)
|
||||
w = pp.outWidth
|
||||
h = pp.outHeight
|
||||
}
|
||||
preprocessor = pp
|
||||
zxing.decode(frameData, w, h, invert)
|
||||
} catch (e: RSRuntimeException) {
|
||||
prefs.forceCompat = prefs.forceCompat xor true
|
||||
// now the only option is to restart the app because
|
||||
// RenderScript.forceCompat() needs to be called before
|
||||
// RenderScript is initialized
|
||||
restartApp(this)
|
||||
null
|
||||
}
|
||||
var pp = preprocessor
|
||||
pp ?: return null
|
||||
pp.process(fd)
|
||||
invert = invert xor true
|
||||
return zxing.decode(
|
||||
fd,
|
||||
}
|
||||
|
||||
private fun createPreprocessorAndMapping(
|
||||
frameWidth: Int,
|
||||
frameHeight: Int,
|
||||
frameOrientation: Int
|
||||
): Preprocessor {
|
||||
val frameRoi = calculateFrameRect(
|
||||
frameWidth,
|
||||
frameHeight,
|
||||
frameOrientation
|
||||
)
|
||||
val pp = Preprocessor(
|
||||
rs,
|
||||
frameWidth,
|
||||
frameHeight,
|
||||
frameRoi
|
||||
)
|
||||
val viewRect = if (frameRoi != null) {
|
||||
detectorView.roi
|
||||
} else {
|
||||
cameraView.previewRect
|
||||
}
|
||||
nativeMapping = frameToView(
|
||||
pp.outWidth,
|
||||
pp.outHeight,
|
||||
invert
|
||||
frameOrientation,
|
||||
viewRect
|
||||
)
|
||||
rotatedMapping = frameToView(
|
||||
pp.outHeight,
|
||||
pp.outWidth,
|
||||
(frameOrientation - 90 + 360) % 360,
|
||||
viewRect
|
||||
)
|
||||
return pp
|
||||
}
|
||||
|
||||
private fun found(result: Result) {
|
||||
cancelDecoding()
|
||||
vibrator.vibrate(100)
|
||||
|
||||
if (returnResult) {
|
||||
val resultIntent = Intent()
|
||||
resultIntent.putExtra("SCAN_RESULT", result.text)
|
||||
setResult(RESULT_OK, resultIntent)
|
||||
finish()
|
||||
return
|
||||
}
|
||||
|
||||
startActivity(
|
||||
MainActivity.getDecodeIntent(
|
||||
this,
|
||||
result.text,
|
||||
result.barcodeFormat
|
||||
private fun calculateFrameRect(
|
||||
frameWidth: Int,
|
||||
frameHeight: Int,
|
||||
frameOrientation: Int
|
||||
): Rect? {
|
||||
val viewRoi = detectorView.roi
|
||||
return if (
|
||||
viewRoi.width() == 0 ||
|
||||
viewRoi.height() == 0
|
||||
) {
|
||||
null
|
||||
} else {
|
||||
// map ROI in detectorView to cameraView
|
||||
val previewRect = cameraView.previewRect
|
||||
// previewRect may be larger than the screen (and thus as the
|
||||
// detectorView) in which case its left and/or top coordinate
|
||||
// will be negative which must then be clamped to the
|
||||
// screen/DetectorView
|
||||
val previewLeft = max(0, previewRect.left)
|
||||
val previewTop = max(0, previewRect.top)
|
||||
val previewRoi = Rect(
|
||||
previewLeft + viewRoi.left,
|
||||
previewTop + viewRoi.top,
|
||||
previewLeft + viewRoi.right,
|
||||
previewTop + viewRoi.bottom
|
||||
)
|
||||
)
|
||||
val previewRectWidth = previewRect.width().toFloat()
|
||||
val previewRectHeight = previewRect.height().toFloat()
|
||||
val normalizedRoi = RectF(
|
||||
previewRoi.left.toFloat() / previewRectWidth,
|
||||
previewRoi.top.toFloat() / previewRectHeight,
|
||||
previewRoi.right.toFloat() / previewRectWidth,
|
||||
previewRoi.bottom.toFloat() / previewRectHeight
|
||||
)
|
||||
// since the ROI is always centered and symmetric, we don't
|
||||
// need to distinguish between 0 and 180 or 90 and 270 degree
|
||||
if (isPortrait(frameOrientation)) {
|
||||
Rect(
|
||||
(normalizedRoi.top * frameWidth.toFloat()).roundToInt(),
|
||||
(normalizedRoi.left * frameHeight.toFloat()).roundToInt(),
|
||||
(normalizedRoi.bottom * frameWidth.toFloat()).roundToInt(),
|
||||
(normalizedRoi.right * frameHeight.toFloat()).roundToInt()
|
||||
)
|
||||
} else {
|
||||
Rect(
|
||||
(normalizedRoi.left * frameWidth.toFloat()).roundToInt(),
|
||||
(normalizedRoi.top * frameHeight.toFloat()).roundToInt(),
|
||||
(normalizedRoi.right * frameWidth.toFloat()).roundToInt(),
|
||||
(normalizedRoi.bottom * frameHeight.toFloat()).roundToInt()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun postResult(result: Result) {
|
||||
// get mapping for the current rotate value
|
||||
val mapping = getMapping()
|
||||
cameraView.post {
|
||||
val rp = result.resultPoints
|
||||
if (mapping != null && rp != null && rp.isNotEmpty()) {
|
||||
detectorView.mark(mapping.map(rp))
|
||||
}
|
||||
vibrator.vibrate()
|
||||
showResult(
|
||||
this@CameraActivity,
|
||||
result,
|
||||
vibrator,
|
||||
returnResult,
|
||||
bulkMode
|
||||
)
|
||||
if (bulkMode) {
|
||||
ignoreNext = result.text
|
||||
toast(result.text)
|
||||
detectorView.postDelayed({
|
||||
decoding = true
|
||||
}, 500)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getMapping() = if (rotate) rotatedMapping else nativeMapping
|
||||
|
||||
companion object {
|
||||
private const val REQUEST_CAMERA = 1
|
||||
private const val PICK_FILE_RESULT_CODE = 1
|
||||
private const val ZOOM_MAX = "zoom_max"
|
||||
private const val ZOOM_LEVEL = "zoom_level"
|
||||
private const val FRONT_FACING = "front_facing"
|
||||
private const val BULK_MODE = "bulk_mode"
|
||||
}
|
||||
}
|
||||
|
||||
fun showResult(
|
||||
activity: Activity,
|
||||
result: Result,
|
||||
vibrator: Vibrator,
|
||||
isResult: Boolean = false,
|
||||
bulkMode: Boolean = false,
|
||||
) {
|
||||
if (isResult) {
|
||||
activity.setResult(Activity.RESULT_OK, getReturnIntent(result))
|
||||
activity.finish()
|
||||
return
|
||||
}
|
||||
if (prefs.copyImmediately) {
|
||||
activity.copyToClipboard(result.text)
|
||||
}
|
||||
val scan = Scan(result)
|
||||
if (prefs.sendScanUrl.isNotEmpty()) {
|
||||
scan.sendAsync(
|
||||
prefs.sendScanUrl,
|
||||
prefs.sendScanType
|
||||
) { code, body ->
|
||||
if (code == null || code < 200 || code > 299) {
|
||||
vibrator.error()
|
||||
beepBeepBeep()
|
||||
}
|
||||
if (body != null && body.isNotEmpty()) {
|
||||
activity.toast(body)
|
||||
} else if (code == null || code > 299) {
|
||||
activity.toast(R.string.background_request_failed)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (prefs.useHistory) {
|
||||
scan.id = db.insertScan(scan)
|
||||
}
|
||||
if (!bulkMode) {
|
||||
activity.startActivity(
|
||||
MainActivity.getDecodeIntent(activity, scan)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun getReturnIntent(result: Result): Intent {
|
||||
val intent = Intent()
|
||||
intent.putExtra("SCAN_RESULT", result.text)
|
||||
intent.putExtra(
|
||||
"SCAN_RESULT_FORMAT",
|
||||
result.barcodeFormat.toString()
|
||||
)
|
||||
if (result.rawBytes?.isNotEmpty() == true) {
|
||||
intent.putExtra("SCAN_RESULT_BYTES", result.rawBytes)
|
||||
}
|
||||
result.resultMetadata?.let { metadata ->
|
||||
metadata[ResultMetadataType.UPC_EAN_EXTENSION]?.let {
|
||||
intent.putExtra(
|
||||
"SCAN_RESULT_ORIENTATION",
|
||||
it.toString()
|
||||
)
|
||||
}
|
||||
metadata[ResultMetadataType.ERROR_CORRECTION_LEVEL]?.let {
|
||||
intent.putExtra(
|
||||
"SCAN_RESULT_ERROR_CORRECTION_LEVEL",
|
||||
it.toString()
|
||||
)
|
||||
}
|
||||
metadata[ResultMetadataType.UPC_EAN_EXTENSION]?.let {
|
||||
intent.putExtra(
|
||||
"SCAN_RESULT_UPC_EAN_EXTENSION",
|
||||
it.toString()
|
||||
)
|
||||
}
|
||||
metadata[ResultMetadataType.BYTE_SEGMENTS]?.let {
|
||||
var i = 0
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
for (seg in it as Iterable<ByteArray>) {
|
||||
intent.putExtra("SCAN_RESULT_BYTE_SEGMENTS_$i", seg)
|
||||
++i
|
||||
}
|
||||
}
|
||||
}
|
||||
return intent
|
||||
}
|
||||
|
||||
private fun beepBeepBeep() {
|
||||
ToneGenerator(AudioManager.STREAM_ALARM, 100).startTone(
|
||||
ToneGenerator.TONE_CDMA_CONFIRM,
|
||||
1000
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,21 +1,38 @@
|
||||
package de.markusfisch.android.binaryeye.activity
|
||||
|
||||
import com.google.zxing.BarcodeFormat
|
||||
|
||||
import de.markusfisch.android.binaryeye.app.initSystemBars
|
||||
import de.markusfisch.android.binaryeye.app.setFragment
|
||||
import de.markusfisch.android.binaryeye.fragment.DecodeFragment
|
||||
import de.markusfisch.android.binaryeye.fragment.EncodeFragment
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
|
||||
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.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) {
|
||||
@@ -26,38 +43,62 @@ class MainActivity : AppCompatActivity() {
|
||||
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)
|
||||
|
||||
setSupportActionBar(findViewById(R.id.toolbar) as Toolbar)
|
||||
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) {
|
||||
var fragment: Fragment
|
||||
if (intent?.hasExtra(ENCODE) == true) {
|
||||
fragment = EncodeFragment.newInstance(
|
||||
intent.getStringExtra(ENCODE)
|
||||
)
|
||||
} else if (intent?.hasExtra(DECODE) == true) {
|
||||
fragment = DecodeFragment.newInstance(
|
||||
intent.getStringExtra(DECODE),
|
||||
intent.getSerializableExtra(
|
||||
DECODE_FORMAT
|
||||
) as BarcodeFormat
|
||||
)
|
||||
} else {
|
||||
fragment = DecodeFragment()
|
||||
}
|
||||
setFragment(supportFragmentManager, fragment)
|
||||
setFragment(supportFragmentManager, getFragmentForIntent(intent))
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val PREFERENCES = "preferences"
|
||||
private const val HISTORY = "history"
|
||||
private const val ENCODE = "encode"
|
||||
private const val DECODE = "decode"
|
||||
private const val DECODE_FORMAT = "decode_format"
|
||||
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,
|
||||
@@ -67,23 +108,22 @@ class MainActivity : AppCompatActivity() {
|
||||
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(
|
||||
android.content.Intent.FLAG_ACTIVITY_NO_HISTORY or
|
||||
android.content.Intent.FLAG_ACTIVITY_CLEAR_TASK or
|
||||
android.content.Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
Intent.FLAG_ACTIVITY_NO_HISTORY or
|
||||
flagActivityClearTask or
|
||||
Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
)
|
||||
}
|
||||
return intent
|
||||
}
|
||||
|
||||
fun getDecodeIntent(
|
||||
context: Context,
|
||||
text: String,
|
||||
format: BarcodeFormat
|
||||
): Intent {
|
||||
fun getDecodeIntent(context: Context, scan: Scan): Intent {
|
||||
val intent = Intent(context, MainActivity::class.java)
|
||||
intent.putExtra(DECODE, text)
|
||||
intent.putExtra(DECODE_FORMAT, format)
|
||||
intent.putExtra(DECODED, scan)
|
||||
return intent
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,206 @@
|
||||
package de.markusfisch.android.binaryeye.activity
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Rect
|
||||
import android.graphics.RectF
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.os.Parcelable
|
||||
import android.os.Vibrator
|
||||
import android.support.v7.app.AppCompatActivity
|
||||
import android.support.v7.widget.Toolbar
|
||||
import android.support.v8.renderscript.RenderScript
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import com.google.zxing.Result
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.app.*
|
||||
import de.markusfisch.android.binaryeye.graphics.crop
|
||||
import de.markusfisch.android.binaryeye.graphics.loadImageUri
|
||||
import de.markusfisch.android.binaryeye.graphics.mapResult
|
||||
import de.markusfisch.android.binaryeye.rs.fixTransparency
|
||||
import de.markusfisch.android.binaryeye.view.recordToolbarHeight
|
||||
import de.markusfisch.android.binaryeye.view.setPaddingFromWindowInsets
|
||||
import de.markusfisch.android.binaryeye.widget.CropImageView
|
||||
import de.markusfisch.android.binaryeye.widget.DetectorView
|
||||
import de.markusfisch.android.binaryeye.widget.toast
|
||||
import de.markusfisch.android.binaryeye.zxing.Zxing
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class PickActivity : AppCompatActivity() {
|
||||
private val zxing = Zxing()
|
||||
|
||||
private lateinit var rs: RenderScript
|
||||
private lateinit var vibrator: Vibrator
|
||||
private lateinit var cropImageView: CropImageView
|
||||
private lateinit var detectorView: DetectorView
|
||||
|
||||
private var result: Result? = null
|
||||
|
||||
override fun attachBaseContext(base: Context?) {
|
||||
base?.applyLocale(prefs.customLocale)
|
||||
super.attachBaseContext(base)
|
||||
}
|
||||
|
||||
override fun onCreate(state: Bundle?) {
|
||||
super.onCreate(state)
|
||||
setContentView(R.layout.activity_pick)
|
||||
|
||||
// necessary to get the right translation after setting a custom locale
|
||||
setTitle(R.string.pick_code_to_scan)
|
||||
|
||||
zxing.updateHints(true)
|
||||
rs = RenderScript.create(this)
|
||||
vibrator = getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
|
||||
|
||||
initSystemBars(this)
|
||||
val toolbar = findViewById(R.id.toolbar) as Toolbar
|
||||
recordToolbarHeight(toolbar)
|
||||
setSupportActionBar(toolbar)
|
||||
|
||||
supportFragmentManager.addOnBackStackChangedListener {
|
||||
colorSystemAndToolBars(this@PickActivity)
|
||||
}
|
||||
|
||||
val bitmap = fixTransparency(
|
||||
rs,
|
||||
getBitmapFromIntent()
|
||||
)
|
||||
|
||||
if (bitmap == null) {
|
||||
applicationContext.toast(R.string.error_no_content)
|
||||
finish()
|
||||
return
|
||||
}
|
||||
|
||||
cropImageView = findViewById(R.id.image) as CropImageView
|
||||
cropImageView.restrictTranslation = false
|
||||
cropImageView.setImageBitmap(bitmap)
|
||||
cropImageView.onScan = {
|
||||
scanWithinBounds(bitmap)
|
||||
}
|
||||
|
||||
detectorView = findViewById(R.id.detector_view) as DetectorView
|
||||
detectorView.onRoiChanged = {
|
||||
scanWithinBounds(bitmap)
|
||||
}
|
||||
detectorView.setPaddingFromWindowInsets()
|
||||
|
||||
findViewById(R.id.scan).setOnClickListener {
|
||||
showResult()
|
||||
}
|
||||
}
|
||||
|
||||
private fun getBitmapFromIntent(): Bitmap? = if (
|
||||
intent?.action == Intent.ACTION_SEND &&
|
||||
intent.type?.startsWith("image/") == true
|
||||
) {
|
||||
loadSentImage(intent)
|
||||
} else if (
|
||||
intent?.action == Intent.ACTION_VIEW &&
|
||||
intent.type?.startsWith("image/") == true
|
||||
) {
|
||||
loadImageToOpen(intent)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
private fun scanWithinBounds(bitmap: Bitmap) {
|
||||
val roi = detectorView.roi
|
||||
val rectInView = if (roi.width() < 1) {
|
||||
cropImageView.getBoundsRect()
|
||||
} else {
|
||||
roi
|
||||
}
|
||||
val imageRect = cropImageView.mappedRect
|
||||
val rectInImage = normalizeRoi(imageRect, rectInView)
|
||||
val cropped = crop(
|
||||
bitmap,
|
||||
rectInImage,
|
||||
cropImageView.imageRotation
|
||||
) ?: return
|
||||
CoroutineScope(Dispatchers.IO).launch(Dispatchers.IO) {
|
||||
result = zxing.decodePositiveNegative(cropped)
|
||||
result?.let {
|
||||
withContext(Dispatchers.Main) {
|
||||
if (!isFinishing) {
|
||||
vibrator.vibrate()
|
||||
}
|
||||
val rc = Rect()
|
||||
imageRect.round(rc)
|
||||
rectInView.intersect(rc)
|
||||
detectorView.mark(
|
||||
mapResult(
|
||||
cropped.width,
|
||||
cropped.height,
|
||||
0,
|
||||
rectInView,
|
||||
it
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
rs.destroy()
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||
menuInflater.inflate(R.menu.activity_pick, menu)
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
return when (item.itemId) {
|
||||
R.id.rotate -> {
|
||||
rotateClockwise()
|
||||
true
|
||||
}
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
}
|
||||
|
||||
private fun rotateClockwise() {
|
||||
cropImageView.imageRotation = cropImageView.imageRotation + 90 % 360
|
||||
}
|
||||
|
||||
private fun loadSentImage(intent: Intent): Bitmap? {
|
||||
val uri = intent.getParcelableExtra<Parcelable>(
|
||||
Intent.EXTRA_STREAM
|
||||
) as? Uri ?: return null
|
||||
return loadImageUri(contentResolver, uri)
|
||||
}
|
||||
|
||||
private fun loadImageToOpen(intent: Intent): Bitmap? {
|
||||
val uri = intent.data ?: return null
|
||||
return loadImageUri(contentResolver, uri)
|
||||
}
|
||||
|
||||
private fun showResult() {
|
||||
result?.let {
|
||||
showResult(this, it, vibrator)
|
||||
finish()
|
||||
return
|
||||
}
|
||||
applicationContext.toast(R.string.no_barcode_found)
|
||||
}
|
||||
}
|
||||
|
||||
private fun normalizeRoi(imageRect: RectF, roi: Rect): RectF {
|
||||
val w = imageRect.width()
|
||||
val h = imageRect.height()
|
||||
return RectF(
|
||||
(roi.left - imageRect.left) / w,
|
||||
(roi.top - imageRect.top) / h,
|
||||
(roi.right - imageRect.left) / w,
|
||||
(roi.bottom - imageRect.top) / h
|
||||
)
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package de.markusfisch.android.binaryeye.activity
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.support.v7.app.AppCompatActivity
|
||||
import de.markusfisch.android.binaryeye.app.setRestartCount
|
||||
|
||||
class SplashActivity : AppCompatActivity() {
|
||||
override fun onCreate(state: Bundle?) {
|
||||
@@ -12,6 +13,7 @@ class SplashActivity : AppCompatActivity() {
|
||||
// because that would happen after the app is fully
|
||||
// initialized what is too late
|
||||
|
||||
setRestartCount(intent)
|
||||
startActivity(Intent(applicationContext, CameraActivity::class.java))
|
||||
finish()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
package de.markusfisch.android.binaryeye.adapter
|
||||
|
||||
import android.content.Context
|
||||
import android.database.Cursor
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.CursorAdapter
|
||||
import android.widget.TextView
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.database.Database
|
||||
import java.text.DateFormat
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
|
||||
class ScansAdapter(context: Context, cursor: Cursor) :
|
||||
CursorAdapter(context, cursor, false) {
|
||||
var selectedScanId = 0L
|
||||
var selectedScanPosition = -1
|
||||
|
||||
private val idIndex = cursor.getColumnIndex(Database.SCANS_ID)
|
||||
private val timeIndex = cursor.getColumnIndex(Database.SCANS_DATETIME)
|
||||
private val nameIndex = cursor.getColumnIndex(Database.SCANS_NAME)
|
||||
private val contentIndex = cursor.getColumnIndex(Database.SCANS_CONTENT)
|
||||
private val formatIndex = cursor.getColumnIndex(Database.SCANS_FORMAT)
|
||||
|
||||
fun select(id: Long, position: Int) {
|
||||
selectedScanId = id
|
||||
selectedScanPosition = position
|
||||
}
|
||||
|
||||
fun clearSelection() {
|
||||
selectedScanId = 0L
|
||||
selectedScanPosition = -1
|
||||
}
|
||||
|
||||
fun getSelectedContent() = if (selectedScanPosition < 0) {
|
||||
null
|
||||
} else {
|
||||
val cursor = getItem(selectedScanPosition)
|
||||
if (cursor is Cursor) {
|
||||
cursor.getString(contentIndex)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
override fun newView(
|
||||
context: Context,
|
||||
cursor: Cursor,
|
||||
parent: ViewGroup
|
||||
): View? = LayoutInflater.from(parent.context).inflate(
|
||||
R.layout.row_scan, parent, false
|
||||
)
|
||||
|
||||
override fun bindView(
|
||||
view: View,
|
||||
context: Context,
|
||||
cursor: Cursor
|
||||
) {
|
||||
val holder = getViewHolder(view)
|
||||
holder.timeView.text = formatDateTime(cursor.getString(timeIndex))
|
||||
val name = cursor.getString(nameIndex)
|
||||
val content = cursor.getString(contentIndex)
|
||||
var icon = 0
|
||||
holder.contentView.text = when {
|
||||
name?.isNotEmpty() == true -> {
|
||||
icon = R.drawable.ic_label
|
||||
name
|
||||
}
|
||||
content?.isEmpty() == true -> context.getString(R.string.binary_data)
|
||||
else -> content
|
||||
}
|
||||
holder.contentView.setCompoundDrawablesWithIntrinsicBounds(
|
||||
icon, 0, 0, 0
|
||||
)
|
||||
holder.formatView.text = prettifyFormatName(cursor.getString(formatIndex))
|
||||
// view.isSelected needs to be put on the queue to work
|
||||
val selected = cursor.getLong(idIndex) == selectedScanId
|
||||
view.post {
|
||||
view.isSelected = selected
|
||||
}
|
||||
}
|
||||
|
||||
private fun getViewHolder(view: View): ViewHolder {
|
||||
return view.tag as ViewHolder? ?: ViewHolder(
|
||||
view.findViewById(R.id.time),
|
||||
view.findViewById(R.id.content),
|
||||
view.findViewById(R.id.format)
|
||||
).also {
|
||||
view.tag = it
|
||||
}
|
||||
}
|
||||
|
||||
private data class ViewHolder(
|
||||
val timeView: TextView,
|
||||
val contentView: TextView,
|
||||
val formatView: TextView
|
||||
)
|
||||
}
|
||||
|
||||
fun prettifyFormatName(format: String) = format.replace("_", " ")
|
||||
|
||||
private fun formatDateTime(rfc: String): String {
|
||||
SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.US).parse(rfc)?.let {
|
||||
return DateFormat.getDateTimeInstance(
|
||||
DateFormat.LONG,
|
||||
DateFormat.SHORT
|
||||
).format(it)
|
||||
}
|
||||
return rfc
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package de.markusfisch.android.binaryeye.app
|
||||
|
||||
import android.content.Context
|
||||
import android.support.v7.app.AlertDialog
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
|
||||
suspend inline fun <T : Any> alertDialog(
|
||||
context: Context,
|
||||
crossinline build: AlertDialog.Builder.(resume: (T?) -> Unit) -> Unit
|
||||
): T? = withContext(Dispatchers.Main) {
|
||||
suspendCoroutine<T?> { continuation ->
|
||||
AlertDialog.Builder(context).apply {
|
||||
setOnCancelListener {
|
||||
continuation.resume(null)
|
||||
}
|
||||
build(continuation::resume)
|
||||
show()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package de.markusfisch.android.binaryeye.app
|
||||
|
||||
import android.app.Application
|
||||
import android.support.v8.renderscript.RenderScript
|
||||
import de.markusfisch.android.binaryeye.database.Database
|
||||
import de.markusfisch.android.binaryeye.preference.Preferences
|
||||
|
||||
val db = Database()
|
||||
val prefs = Preferences()
|
||||
|
||||
class BinaryEyeApp : Application() {
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
db.open(this)
|
||||
prefs.init(this)
|
||||
|
||||
if (prefs.forceCompat) {
|
||||
RenderScript.forceCompat()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package de.markusfisch.android.binaryeye.app
|
||||
|
||||
import android.content.ClipData
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
|
||||
fun Context.copyToClipboard(text: String) {
|
||||
val service = getSystemService(Context.CLIPBOARD_SERVICE)
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) {
|
||||
@Suppress("DEPRECATION")
|
||||
(service as android.text.ClipboardManager).text = text
|
||||
} else {
|
||||
(service as android.content.ClipboardManager).setPrimaryClip(
|
||||
ClipData.newPlainText("plain text", text)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,9 @@
|
||||
package de.markusfisch.android.binaryeye.app
|
||||
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.support.v4.app.Fragment
|
||||
import android.support.v4.app.FragmentManager
|
||||
import android.support.v4.app.FragmentTransaction
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
|
||||
fun setFragment(fm: FragmentManager?, fragment: Fragment) {
|
||||
fm?.let { getTransaction(fm, fragment).commit() }
|
||||
@@ -14,9 +13,8 @@ fun addFragment(fm: FragmentManager?, fragment: Fragment) {
|
||||
fm?.let { getTransaction(fm, fragment).addToBackStack(null).commit() }
|
||||
}
|
||||
|
||||
@SuppressLint("CommitTransaction")
|
||||
private fun getTransaction(
|
||||
fm: FragmentManager,
|
||||
fragment: Fragment
|
||||
): FragmentTransaction {
|
||||
return fm.beginTransaction().replace(R.id.content_frame, fragment)
|
||||
}
|
||||
) = fm.beginTransaction().replace(R.id.content_frame, fragment)
|
||||
@@ -0,0 +1,14 @@
|
||||
package de.markusfisch.android.binaryeye.app
|
||||
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
|
||||
private val nonPrintable = "[\\x00-\\x08\\x0e-\\x1f]".toRegex()
|
||||
|
||||
fun hasNonPrintableCharacters(s: String) = nonPrintable.containsMatchIn(s)
|
||||
|
||||
fun parseAndNormalizeUri(input: String): Uri = Uri.parse(input).let {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) it.normalizeScheme() else it
|
||||
}
|
||||
|
||||
fun ByteArray.toHexString() = joinToString("") { "%02X".format(it) }
|
||||
@@ -0,0 +1,86 @@
|
||||
package de.markusfisch.android.binaryeye.app
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.ContentValues
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.os.Environment
|
||||
import android.provider.MediaStore
|
||||
import android.support.annotation.MainThread
|
||||
import android.widget.EditText
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
import java.io.OutputStream
|
||||
|
||||
fun addSuffixIfNotGiven(name: String, suffix: String): String {
|
||||
val trimmed = name.trim()
|
||||
return if (suffix.isNotEmpty() && !trimmed.endsWith(suffix)) {
|
||||
"$trimmed$suffix"
|
||||
} else {
|
||||
trimmed
|
||||
}
|
||||
}
|
||||
|
||||
// Dialogs doesn't have any root layout, need to be inflated with null root
|
||||
@Suppress("InflateParams")
|
||||
@MainThread
|
||||
suspend fun Activity.askForFileName(suffix: String = ""): String? {
|
||||
val view = layoutInflater.inflate(R.layout.dialog_save_file, null)
|
||||
val editText = view.findViewById<EditText>(R.id.file_name)
|
||||
return alertDialog<String>(this as Context) { resume ->
|
||||
setView(view)
|
||||
setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
resume(editText.text.toString())
|
||||
}
|
||||
}?.let { name ->
|
||||
addSuffixIfNotGiven(name, suffix)
|
||||
}
|
||||
}
|
||||
|
||||
fun Boolean.toSaveResult() = if (this) {
|
||||
R.string.saved_in_downloads
|
||||
} else {
|
||||
R.string.error_saving_file
|
||||
}
|
||||
|
||||
fun writeExternalFile(
|
||||
context: Context,
|
||||
fileName: String,
|
||||
mimeType: String,
|
||||
write: (outputStream: OutputStream) -> Unit
|
||||
): Boolean = try {
|
||||
openExternalOutputStream(context, fileName, mimeType).use { write(it) }
|
||||
true
|
||||
} catch (e: IOException) {
|
||||
false
|
||||
}
|
||||
|
||||
private fun openExternalOutputStream(
|
||||
context: Context,
|
||||
fileName: String,
|
||||
mimeType: String
|
||||
): OutputStream = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
||||
@Suppress("DEPRECATION")
|
||||
val file = File(
|
||||
Environment.getExternalStoragePublicDirectory(
|
||||
Environment.DIRECTORY_DOWNLOADS
|
||||
),
|
||||
fileName
|
||||
)
|
||||
if (file.exists()) {
|
||||
throw IOException()
|
||||
}
|
||||
FileOutputStream(file)
|
||||
} else {
|
||||
val resolver = context.contentResolver
|
||||
val uri = resolver.insert(
|
||||
MediaStore.Downloads.EXTERNAL_CONTENT_URI,
|
||||
ContentValues().apply {
|
||||
put(MediaStore.Downloads.DISPLAY_NAME, fileName)
|
||||
put(MediaStore.Downloads.MIME_TYPE, mimeType)
|
||||
}
|
||||
) ?: throw IOException()
|
||||
resolver.openOutputStream(uri) ?: throw IOException()
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package de.markusfisch.android.binaryeye.app
|
||||
|
||||
import android.content.Context
|
||||
import android.view.View
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
|
||||
fun Context.hideSoftKeyboard(view: View) {
|
||||
(getSystemService(
|
||||
Context.INPUT_METHOD_SERVICE
|
||||
) as InputMethodManager).hideSoftInputFromWindow(
|
||||
view.windowToken, 0
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package de.markusfisch.android.binaryeye.app
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import java.util.*
|
||||
|
||||
fun Context.applyLocale(localeName: String) {
|
||||
if (localeName.isNotEmpty()) {
|
||||
val locale = Locale(localeName)
|
||||
Locale.setDefault(locale)
|
||||
val conf = resources.configuration
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
|
||||
@Suppress("DEPRECATION")
|
||||
conf.locale = locale
|
||||
} else {
|
||||
conf.setLocale(locale)
|
||||
}
|
||||
resources.updateConfiguration(conf, resources.displayMetrics)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package de.markusfisch.android.binaryeye.app
|
||||
|
||||
import android.Manifest
|
||||
import android.app.Activity
|
||||
import android.content.pm.PackageManager
|
||||
import android.support.v4.app.ActivityCompat
|
||||
import android.support.v4.content.ContextCompat
|
||||
|
||||
var permissionGrantedCallback: (() -> Any)? = null
|
||||
|
||||
const val PERMISSION_CAMERA = 1
|
||||
fun hasCameraPermission(activity: Activity) = hasPermission(
|
||||
activity,
|
||||
Manifest.permission.CAMERA,
|
||||
PERMISSION_CAMERA
|
||||
)
|
||||
|
||||
const val PERMISSION_WRITE = 2
|
||||
fun hasWritePermission(activity: Activity, callback: () -> Any): Boolean {
|
||||
permissionGrantedCallback = callback
|
||||
return hasPermission(
|
||||
activity,
|
||||
Manifest.permission.WRITE_EXTERNAL_STORAGE,
|
||||
PERMISSION_WRITE
|
||||
)
|
||||
}
|
||||
|
||||
const val PERMISSION_LOCATION = 3
|
||||
fun hasLocationPermission(activity: Activity, callback: () -> Any): Boolean {
|
||||
permissionGrantedCallback = callback
|
||||
return hasPermission(
|
||||
activity,
|
||||
Manifest.permission.ACCESS_FINE_LOCATION,
|
||||
PERMISSION_LOCATION
|
||||
)
|
||||
}
|
||||
|
||||
private fun hasPermission(
|
||||
activity: Activity,
|
||||
permission: String,
|
||||
requestCode: Int
|
||||
) = if (ContextCompat.checkSelfPermission(
|
||||
activity,
|
||||
permission
|
||||
) != PackageManager.PERMISSION_GRANTED
|
||||
) {
|
||||
ActivityCompat.requestPermissions(
|
||||
activity,
|
||||
arrayOf(permission),
|
||||
requestCode
|
||||
)
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package de.markusfisch.android.binaryeye.app
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import de.markusfisch.android.binaryeye.activity.SplashActivity
|
||||
|
||||
private const val RESTART_COUNT = "restart_count"
|
||||
|
||||
private var restartCount = 0
|
||||
|
||||
fun setRestartCount(intent: Intent?) {
|
||||
intent?.let {
|
||||
restartCount = intent.getIntExtra(RESTART_COUNT, restartCount)
|
||||
}
|
||||
}
|
||||
|
||||
fun restartApp(activity: Activity? = null) {
|
||||
if (activity != null && restartCount < 3) {
|
||||
val intent = Intent(activity, SplashActivity::class.java)
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
intent.putExtra(RESTART_COUNT, ++restartCount)
|
||||
activity.startActivity(intent)
|
||||
activity.finish()
|
||||
}
|
||||
Runtime.getRuntime().exit(0)
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package de.markusfisch.android.binaryeye.app
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.support.v4.content.FileProvider
|
||||
import de.markusfisch.android.binaryeye.BuildConfig
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.widget.toast
|
||||
import java.io.File
|
||||
|
||||
fun shareText(context: Context, text: String, type: String = "text/plain") {
|
||||
val intent = Intent(Intent.ACTION_SEND)
|
||||
intent.putExtra(Intent.EXTRA_TEXT, text)
|
||||
intent.type = type
|
||||
execShareIntent(context, intent)
|
||||
}
|
||||
|
||||
fun shareUri(context: Context, uri: Uri, type: String) {
|
||||
val intent = Intent(Intent.ACTION_SEND)
|
||||
intent.putExtra(Intent.EXTRA_STREAM, uri)
|
||||
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
intent.type = type
|
||||
execShareIntent(context, intent)
|
||||
}
|
||||
|
||||
fun execShareIntent(context: Context, intent: Intent) {
|
||||
if (intent.resolveActivity(context.packageManager) != null) {
|
||||
context.startActivity(intent)
|
||||
} else {
|
||||
context.toast(R.string.cannot_resolve_action)
|
||||
}
|
||||
}
|
||||
|
||||
fun shareFile(context: Context, file: File, type: String) {
|
||||
getUriForFile(context, file)?.let {
|
||||
shareUri(context, it, type)
|
||||
}
|
||||
}
|
||||
|
||||
fun getUriForFile(context: Context, file: File): Uri? {
|
||||
return if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
|
||||
Uri.fromFile(file)
|
||||
} else {
|
||||
FileProvider.getUriForFile(
|
||||
context,
|
||||
BuildConfig.APPLICATION_ID + ".provider",
|
||||
file
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,98 +1,131 @@
|
||||
package de.markusfisch.android.binaryeye.app
|
||||
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
|
||||
import android.content.Context
|
||||
import android.content.res.Configuration
|
||||
import android.content.res.Resources
|
||||
import android.graphics.Point
|
||||
import android.content.ContextWrapper
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
import android.os.Build
|
||||
import android.support.v4.content.ContextCompat
|
||||
import android.support.v7.app.AppCompatActivity
|
||||
import android.util.TypedValue
|
||||
import android.support.v7.widget.LinearLayoutManager
|
||||
import android.support.v7.widget.RecyclerView
|
||||
import android.view.View
|
||||
import android.view.Window
|
||||
import android.view.WindowManager
|
||||
import android.widget.AbsListView
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
|
||||
fun initSystemBars(activity: AppCompatActivity?) {
|
||||
val view = activity?.findViewById(R.id.main_layout)
|
||||
if (view != null && setSystemBarColor(
|
||||
activity.window,
|
||||
ContextCompat.getColor(
|
||||
activity,
|
||||
R.color.primary_dark_translucent
|
||||
)
|
||||
)) {
|
||||
view.setPadding(0, getStatusBarHeight(activity.resources), 0, 0)
|
||||
}
|
||||
}
|
||||
|
||||
fun setSystemBarColor(window: Window, color: Int): Boolean {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
|
||||
return false
|
||||
}
|
||||
|
||||
window.statusBarColor = color
|
||||
window.navigationBarColor = color
|
||||
window.decorView.systemUiVisibility =
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_STABLE or
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
fun getStatusAndToolBarHeight(context: Context): Int {
|
||||
return getStatusBarHeight(context.resources) + getToolBarHeight(context)
|
||||
}
|
||||
|
||||
fun getStatusBarHeight(res: Resources): Int {
|
||||
return getIdentifierDimen(res, "status_bar_height")
|
||||
}
|
||||
|
||||
fun getToolBarHeight(context: Context): Int {
|
||||
val tv = TypedValue()
|
||||
return if (context.theme.resolveAttribute(
|
||||
android.R.attr.actionBarSize,
|
||||
tv,
|
||||
true
|
||||
))
|
||||
TypedValue.complexToDimensionPixelSize(
|
||||
tv.data,
|
||||
context.resources.displayMetrics
|
||||
)
|
||||
else
|
||||
0
|
||||
}
|
||||
|
||||
fun getNavigationBarSize(res: Resources): Point {
|
||||
val size = Point(0, 0)
|
||||
if (!getIdentifierBoolean(res, "config_showNavigationBar")) {
|
||||
return size
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR2) {
|
||||
val conf = res.getConfiguration()
|
||||
if (conf.orientation == Configuration.ORIENTATION_LANDSCAPE &&
|
||||
// according to https://developer.android.com/training/multiscreen/screensizes.html#TaskUseSWQuali
|
||||
// only a screen < 600 dp is considered to be a phone
|
||||
// and can move its navigation bar to the side
|
||||
conf.smallestScreenWidthDp < 600) {
|
||||
size.x = getIdentifierDimen(
|
||||
res,
|
||||
"navigation_bar_height_landscape"
|
||||
)
|
||||
return size
|
||||
val systemBarListViewScrollListener = object : AbsListView.OnScrollListener {
|
||||
override fun onScroll(
|
||||
view: AbsListView,
|
||||
firstVisibleItem: Int,
|
||||
visibleItemCount: Int,
|
||||
totalItemCount: Int
|
||||
) {
|
||||
// give Android some time to settle down before running this,
|
||||
// not putting it on the queue makes it only work sometimes
|
||||
view.post {
|
||||
val scrolled = firstVisibleItem > 0 ||
|
||||
(totalItemCount > 0 && firstChildScrolled(view))
|
||||
val scrollable = scrolled || totalItemCount > 0 && lastChildOutOfView(view)
|
||||
colorSystemAndToolBars(view.context, scrolled, scrollable)
|
||||
}
|
||||
}
|
||||
size.y = getIdentifierDimen(res, "navigation_bar_height")
|
||||
return size
|
||||
|
||||
override fun onScrollStateChanged(view: AbsListView, scrollState: Int) {
|
||||
}
|
||||
}
|
||||
|
||||
private fun getIdentifierBoolean(res: Resources, name: String): Boolean {
|
||||
val id = res.getIdentifier(name, "bool", "android")
|
||||
return id > 0 && res.getBoolean(id)
|
||||
val systemBarRecyclerViewScrollListener = object : RecyclerView.OnScrollListener() {
|
||||
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
||||
val layoutManager = recyclerView.layoutManager as LinearLayoutManager
|
||||
val scrolled = layoutManager.findFirstCompletelyVisibleItemPosition() != 0
|
||||
val scrollable = scrolled || layoutManager.findLastVisibleItemPosition() <
|
||||
recyclerView.adapter.itemCount - 1
|
||||
colorSystemAndToolBars(recyclerView.context, scrolled, scrollable)
|
||||
}
|
||||
|
||||
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
|
||||
}
|
||||
}
|
||||
|
||||
private fun getIdentifierDimen(res: Resources, name: String): Int {
|
||||
val id = res.getIdentifier(name, "dimen", "android")
|
||||
return if (id > 0) res.getDimensionPixelSize(id) else 0
|
||||
private fun firstChildScrolled(listView: AbsListView): Boolean {
|
||||
val child = listView.getChildAt(0)
|
||||
return child != null && child.top < listView.paddingTop
|
||||
}
|
||||
|
||||
private fun lastChildOutOfView(listView: AbsListView): Boolean {
|
||||
val child = listView.getChildAt(listView.lastVisiblePosition)
|
||||
return child != null && child.bottom >= listView.height
|
||||
}
|
||||
|
||||
fun initSystemBars(activity: AppCompatActivity) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
// keeps the soft keyboard from repositioning the layout
|
||||
val window = activity.window
|
||||
window.setSoftInputMode(
|
||||
WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE
|
||||
)
|
||||
window.decorView.systemUiVisibility =
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_STABLE or
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
|
||||
}
|
||||
colorSystemAndToolBars(activity)
|
||||
}
|
||||
|
||||
private var statusBarColorLocked = false
|
||||
fun lockStatusBarColor() {
|
||||
statusBarColorLocked = true
|
||||
}
|
||||
|
||||
fun unlockStatusBarColor() {
|
||||
statusBarColorLocked = false
|
||||
}
|
||||
|
||||
private var translucentPrimaryColor = 0
|
||||
private val actionBarBackground = ColorDrawable()
|
||||
fun colorSystemAndToolBars(
|
||||
context: Context,
|
||||
scrolled: Boolean = false,
|
||||
scrollable: Boolean = false
|
||||
) {
|
||||
if (translucentPrimaryColor == 0) {
|
||||
translucentPrimaryColor = ContextCompat.getColor(
|
||||
context,
|
||||
R.color.primary_translucent
|
||||
)
|
||||
}
|
||||
val topColor = if (scrolled) translucentPrimaryColor else 0
|
||||
val activity = getAppCompatActivity(context) ?: return
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
val window = activity.window
|
||||
if (!statusBarColorLocked) {
|
||||
window.statusBarColor = topColor
|
||||
}
|
||||
window.navigationBarColor = if (scrolled || scrollable) {
|
||||
translucentPrimaryColor
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
activity.supportActionBar?.setBackgroundDrawable(
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
|
||||
// avoid allocation on Honeycomb and better
|
||||
actionBarBackground.color = topColor
|
||||
actionBarBackground
|
||||
} else {
|
||||
// ColorDrawable.setColor() doesn't exist pre Honeycomb
|
||||
ColorDrawable(topColor)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun getAppCompatActivity(context: Context): AppCompatActivity? {
|
||||
var ctx = context
|
||||
while (ctx is ContextWrapper) {
|
||||
if (ctx is AppCompatActivity) {
|
||||
return ctx
|
||||
}
|
||||
ctx = ctx.baseContext ?: break
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
package de.markusfisch.android.binaryeye.app
|
||||
|
||||
import android.os.Build
|
||||
import android.os.VibrationEffect
|
||||
import android.os.Vibrator
|
||||
|
||||
private const val MILLISECONDS = 100L
|
||||
private val errorPatternTimings = longArrayOf(
|
||||
50L, 100L, 50L, 100L, 50L
|
||||
)
|
||||
private val errorPatternAmplitudes = intArrayOf(
|
||||
255, 0, 255, 0, 255
|
||||
)
|
||||
|
||||
fun Vibrator.vibrate() {
|
||||
if (!prefs.vibrate) {
|
||||
return
|
||||
}
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
||||
@Suppress("DEPRECATION")
|
||||
vibrate(MILLISECONDS)
|
||||
} else {
|
||||
vibrate(
|
||||
VibrationEffect.createOneShot(
|
||||
MILLISECONDS,
|
||||
VibrationEffect.DEFAULT_AMPLITUDE
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun Vibrator.error() {
|
||||
if (!prefs.vibrate) {
|
||||
return
|
||||
}
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
||||
@Suppress("DEPRECATION")
|
||||
vibrate(errorPatternTimings, -1)
|
||||
} else {
|
||||
vibrate(
|
||||
VibrationEffect.createWaveform(
|
||||
errorPatternTimings,
|
||||
errorPatternAmplitudes,
|
||||
-1
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
package de.markusfisch.android.binaryeye.database
|
||||
|
||||
import android.content.Context
|
||||
import android.database.Cursor
|
||||
import de.markusfisch.android.binaryeye.app.toHexString
|
||||
import de.markusfisch.android.binaryeye.app.writeExternalFile
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.OutputStream
|
||||
|
||||
fun exportCsv(
|
||||
context: Context,
|
||||
name: String,
|
||||
cursor: Cursor,
|
||||
delimiter: String
|
||||
) = writeExternalFile(context, name, "text/csv") { outputStream ->
|
||||
exportCsv(outputStream, cursor, delimiter)
|
||||
}
|
||||
|
||||
fun exportCsv(cursor: Cursor, delimiter: String): String? {
|
||||
val outputStream = ByteArrayOutputStream()
|
||||
exportCsv(outputStream, cursor, delimiter)
|
||||
return outputStream.toString()
|
||||
}
|
||||
|
||||
private fun exportCsv(
|
||||
outputStream: OutputStream,
|
||||
cursor: Cursor,
|
||||
delimiter: String
|
||||
) {
|
||||
if (!cursor.moveToFirst()) {
|
||||
return
|
||||
}
|
||||
val columns = arrayOf(
|
||||
Database.SCANS_DATETIME,
|
||||
Database.SCANS_FORMAT,
|
||||
Database.SCANS_CONTENT,
|
||||
Database.SCANS_ERROR_CORRECTION_LEVEL,
|
||||
Database.SCANS_ISSUE_NUMBER,
|
||||
Database.SCANS_ORIENTATION,
|
||||
Database.SCANS_OTHER_META_DATA,
|
||||
Database.SCANS_PDF417_EXTRA_METADATA,
|
||||
Database.SCANS_POSSIBLE_COUNTRY,
|
||||
Database.SCANS_SUGGESTED_PRICE,
|
||||
Database.SCANS_UPC_EAN_EXTENSION
|
||||
)
|
||||
val indices = columns.map {
|
||||
cursor.getColumnIndex(it)
|
||||
}
|
||||
val contentIndex = cursor.getColumnIndex(Database.SCANS_CONTENT)
|
||||
val rawIndex = cursor.getColumnIndex(Database.SCANS_RAW)
|
||||
outputStream.write(
|
||||
columns.joinToString(
|
||||
delimiter,
|
||||
postfix = "\n"
|
||||
).toByteArray()
|
||||
)
|
||||
do {
|
||||
var deviation: Pair<Int, String>? = null
|
||||
if (cursor.getString(contentIndex)?.isEmpty() == true) {
|
||||
deviation = Pair(
|
||||
contentIndex,
|
||||
cursor.getBlob(rawIndex).toHexString()
|
||||
)
|
||||
}
|
||||
outputStream.write(
|
||||
cursor.toCsvRecord(indices, delimiter, deviation)
|
||||
)
|
||||
} while (cursor.moveToNext())
|
||||
}
|
||||
|
||||
private fun Cursor.toCsvRecord(
|
||||
indices: List<Int>,
|
||||
delimiter: String,
|
||||
deviation: Pair<Int, String>?
|
||||
): ByteArray {
|
||||
val sb = StringBuilder()
|
||||
indices.forEach {
|
||||
val value = if (deviation?.first == it) {
|
||||
deviation.second
|
||||
} else {
|
||||
this.getString(it)
|
||||
}
|
||||
sb.append(value?.quoteAndEscape() ?: "")
|
||||
sb.append(delimiter)
|
||||
}
|
||||
sb.append("\n")
|
||||
return sb.toString().toByteArray()
|
||||
}
|
||||
|
||||
private fun String.quoteAndEscape() = "\"${
|
||||
this
|
||||
.replace("\n", " ")
|
||||
.replace("\"", "\"\"")
|
||||
}\""
|
||||
@@ -0,0 +1,285 @@
|
||||
package de.markusfisch.android.binaryeye.database
|
||||
|
||||
import android.content.ContentValues
|
||||
import android.content.Context
|
||||
import android.database.Cursor
|
||||
import android.database.sqlite.SQLiteDatabase
|
||||
import android.database.sqlite.SQLiteOpenHelper
|
||||
import de.markusfisch.android.binaryeye.app.hasNonPrintableCharacters
|
||||
import de.markusfisch.android.binaryeye.app.prefs
|
||||
|
||||
class Database {
|
||||
private lateinit var db: SQLiteDatabase
|
||||
|
||||
fun open(context: Context) {
|
||||
db = OpenHelper(context).writableDatabase
|
||||
}
|
||||
|
||||
fun getScans(query: String? = null): Cursor? = db.rawQuery(
|
||||
"""SELECT
|
||||
$SCANS_ID,
|
||||
$SCANS_DATETIME,
|
||||
$SCANS_NAME,
|
||||
$SCANS_CONTENT,
|
||||
$SCANS_FORMAT
|
||||
FROM $SCANS
|
||||
${getWhereClause(query)}
|
||||
ORDER BY $SCANS_DATETIME DESC
|
||||
""", getWhereArguments(query)
|
||||
)
|
||||
|
||||
fun getScansDetailed(query: String? = null): Cursor? = db.rawQuery(
|
||||
"""SELECT
|
||||
$SCANS_ID,
|
||||
$SCANS_DATETIME,
|
||||
$SCANS_NAME,
|
||||
$SCANS_CONTENT,
|
||||
$SCANS_RAW,
|
||||
$SCANS_FORMAT,
|
||||
$SCANS_ERROR_CORRECTION_LEVEL,
|
||||
$SCANS_ISSUE_NUMBER,
|
||||
$SCANS_ORIENTATION,
|
||||
$SCANS_OTHER_META_DATA,
|
||||
$SCANS_PDF417_EXTRA_METADATA,
|
||||
$SCANS_POSSIBLE_COUNTRY,
|
||||
$SCANS_SUGGESTED_PRICE,
|
||||
$SCANS_UPC_EAN_EXTENSION
|
||||
FROM $SCANS
|
||||
${getWhereClause(query)}
|
||||
ORDER BY $SCANS_DATETIME DESC
|
||||
""", getWhereArguments(query)
|
||||
)
|
||||
|
||||
private fun getWhereClause(
|
||||
query: String?,
|
||||
prefix: String = "WHERE"
|
||||
) = if (query?.isNotEmpty() == true) {
|
||||
"""$prefix $SCANS_CONTENT LIKE ?
|
||||
OR $SCANS_NAME LIKE ?"""
|
||||
} else {
|
||||
""
|
||||
}
|
||||
|
||||
private fun getWhereArguments(
|
||||
query: String?
|
||||
) = if (query?.isNotEmpty() == true) {
|
||||
val instr = "%$query%"
|
||||
arrayOf(instr, instr)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
fun getScan(id: Long): Scan? = db.rawQuery(
|
||||
"""SELECT
|
||||
$SCANS_ID,
|
||||
$SCANS_DATETIME,
|
||||
$SCANS_NAME,
|
||||
$SCANS_CONTENT,
|
||||
$SCANS_RAW,
|
||||
$SCANS_FORMAT,
|
||||
$SCANS_ERROR_CORRECTION_LEVEL,
|
||||
$SCANS_ISSUE_NUMBER,
|
||||
$SCANS_ORIENTATION,
|
||||
$SCANS_OTHER_META_DATA,
|
||||
$SCANS_PDF417_EXTRA_METADATA,
|
||||
$SCANS_POSSIBLE_COUNTRY,
|
||||
$SCANS_SUGGESTED_PRICE,
|
||||
$SCANS_UPC_EAN_EXTENSION
|
||||
FROM $SCANS
|
||||
WHERE $SCANS_ID = ?
|
||||
""", arrayOf("$id")
|
||||
)?.use {
|
||||
if (it.moveToFirst()) {
|
||||
Scan(
|
||||
it.getString(SCANS_CONTENT),
|
||||
it.getBlob(SCANS_RAW),
|
||||
it.getString(SCANS_FORMAT),
|
||||
it.getString(SCANS_ERROR_CORRECTION_LEVEL),
|
||||
it.getString(SCANS_ISSUE_NUMBER),
|
||||
it.getString(SCANS_ORIENTATION),
|
||||
it.getString(SCANS_OTHER_META_DATA),
|
||||
it.getString(SCANS_PDF417_EXTRA_METADATA),
|
||||
it.getString(SCANS_POSSIBLE_COUNTRY),
|
||||
it.getString(SCANS_SUGGESTED_PRICE),
|
||||
it.getString(SCANS_UPC_EAN_EXTENSION),
|
||||
it.getString(SCANS_DATETIME),
|
||||
it.getLong(SCANS_ID)
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun insertScan(scan: Scan): Long {
|
||||
val cv = ContentValues()
|
||||
cv.put(SCANS_DATETIME, scan.timestamp)
|
||||
val isRaw = hasNonPrintableCharacters(scan.content)
|
||||
if (isRaw) {
|
||||
cv.put(SCANS_CONTENT, "")
|
||||
cv.put(SCANS_RAW, scan.raw ?: scan.content.toByteArray())
|
||||
} else {
|
||||
cv.put(SCANS_CONTENT, scan.content)
|
||||
}
|
||||
cv.put(SCANS_FORMAT, scan.format)
|
||||
scan.errorCorrectionLevel?.let {
|
||||
cv.put(SCANS_ERROR_CORRECTION_LEVEL, it)
|
||||
}
|
||||
scan.issueNumber?.let { cv.put(SCANS_ISSUE_NUMBER, it) }
|
||||
scan.orientation?.let { cv.put(SCANS_ORIENTATION, it) }
|
||||
scan.otherMetaData?.let { cv.put(SCANS_OTHER_META_DATA, it) }
|
||||
scan.pdf417ExtraMetaData?.let { cv.put(SCANS_PDF417_EXTRA_METADATA, it) }
|
||||
scan.possibleCountry?.let { cv.put(SCANS_POSSIBLE_COUNTRY, it) }
|
||||
scan.suggestedPrice?.let { cv.put(SCANS_SUGGESTED_PRICE, it) }
|
||||
scan.upcEanExtension?.let { cv.put(SCANS_UPC_EAN_EXTENSION, it) }
|
||||
if (prefs.ignoreConsecutiveDuplicates) {
|
||||
val id = getIdOfLastScan(
|
||||
cv.get(SCANS_CONTENT) as String,
|
||||
if (isRaw) cv.get(SCANS_RAW) as ByteArray else null,
|
||||
scan.format
|
||||
)
|
||||
if (id > 0L) {
|
||||
return id
|
||||
}
|
||||
}
|
||||
return db.insert(SCANS, null, cv)
|
||||
}
|
||||
|
||||
private fun getIdOfLastScan(
|
||||
content: String,
|
||||
raw: ByteArray?,
|
||||
format: String
|
||||
): Long {
|
||||
return db.rawQuery(
|
||||
"""SELECT
|
||||
$SCANS_ID,
|
||||
$SCANS_CONTENT,
|
||||
$SCANS_RAW,
|
||||
$SCANS_FORMAT
|
||||
FROM $SCANS
|
||||
ORDER BY $SCANS_ID DESC
|
||||
LIMIT 1
|
||||
""", null
|
||||
)?.use {
|
||||
if (it.count > 0 &&
|
||||
it.moveToFirst() &&
|
||||
it.getString(it.getColumnIndex(SCANS_CONTENT)) == content &&
|
||||
(raw == null || it.getBlob(it.getColumnIndex(SCANS_RAW))
|
||||
?.contentEquals(raw) == true) &&
|
||||
it.getString(it.getColumnIndex(SCANS_FORMAT)) == format
|
||||
) {
|
||||
it.getLong(it.getColumnIndex(SCANS_ID))
|
||||
} else {
|
||||
0L
|
||||
}
|
||||
} ?: 0L
|
||||
}
|
||||
|
||||
fun removeScan(id: Long) {
|
||||
db.delete(SCANS, "$SCANS_ID = ?", arrayOf("$id"))
|
||||
}
|
||||
|
||||
fun removeScans(query: String? = null) {
|
||||
db.delete(SCANS, getWhereClause(query, ""), getWhereArguments(query))
|
||||
}
|
||||
|
||||
fun renameScan(id: Long, name: String) {
|
||||
val cv = ContentValues()
|
||||
cv.put(SCANS_NAME, name)
|
||||
db.update(SCANS, cv, "$SCANS_ID = ?", arrayOf("$id"))
|
||||
}
|
||||
|
||||
private class OpenHelper(context: Context) :
|
||||
SQLiteOpenHelper(context, FILE_NAME, null, 4) {
|
||||
override fun onCreate(db: SQLiteDatabase) {
|
||||
createScans(db)
|
||||
}
|
||||
|
||||
override fun onUpgrade(
|
||||
db: SQLiteDatabase,
|
||||
oldVersion: Int,
|
||||
newVersion: Int
|
||||
) {
|
||||
if (oldVersion < 2) {
|
||||
addRawColumn(db)
|
||||
}
|
||||
if (oldVersion < 3) {
|
||||
addMetaDataColumns(db)
|
||||
}
|
||||
if (oldVersion < 4) {
|
||||
addNameColumn(db)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val FILE_NAME = "history.db"
|
||||
const val SCANS = "scans"
|
||||
const val SCANS_ID = "_id"
|
||||
const val SCANS_DATETIME = "_datetime"
|
||||
const val SCANS_NAME = "name"
|
||||
const val SCANS_CONTENT = "content"
|
||||
const val SCANS_RAW = "raw"
|
||||
const val SCANS_FORMAT = "format"
|
||||
const val SCANS_ERROR_CORRECTION_LEVEL = "error_correction_level"
|
||||
const val SCANS_ISSUE_NUMBER = "issue_number"
|
||||
const val SCANS_ORIENTATION = "orientation"
|
||||
const val SCANS_OTHER_META_DATA = "other_meta_data"
|
||||
const val SCANS_PDF417_EXTRA_METADATA = "pdf417_extra_metadata"
|
||||
const val SCANS_POSSIBLE_COUNTRY = "possible_country"
|
||||
const val SCANS_SUGGESTED_PRICE = "suggested_price"
|
||||
const val SCANS_UPC_EAN_EXTENSION = "upc_ean_extension"
|
||||
|
||||
private fun createScans(db: SQLiteDatabase) {
|
||||
db.execSQL("DROP TABLE IF EXISTS $SCANS")
|
||||
db.execSQL(
|
||||
"""CREATE TABLE $SCANS (
|
||||
$SCANS_ID INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
$SCANS_DATETIME DATETIME NOT NULL,
|
||||
$SCANS_NAME TEXT,
|
||||
$SCANS_CONTENT TEXT NOT NULL,
|
||||
$SCANS_RAW BLOB,
|
||||
$SCANS_FORMAT TEXT NOT NULL,
|
||||
$SCANS_ERROR_CORRECTION_LEVEL TEXT,
|
||||
$SCANS_ISSUE_NUMBER INT,
|
||||
$SCANS_ORIENTATION INT,
|
||||
$SCANS_OTHER_META_DATA TEXT,
|
||||
$SCANS_PDF417_EXTRA_METADATA TEXT,
|
||||
$SCANS_POSSIBLE_COUNTRY TEXT,
|
||||
$SCANS_SUGGESTED_PRICE TEXT,
|
||||
$SCANS_UPC_EAN_EXTENSION TEXT
|
||||
)"""
|
||||
)
|
||||
}
|
||||
|
||||
private fun addRawColumn(db: SQLiteDatabase) {
|
||||
db.execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_RAW BLOB")
|
||||
}
|
||||
|
||||
private fun addMetaDataColumns(db: SQLiteDatabase) {
|
||||
db.execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_ERROR_CORRECTION_LEVEL TEXT")
|
||||
db.execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_ISSUE_NUMBER INT")
|
||||
db.execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_ORIENTATION INT")
|
||||
db.execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_OTHER_META_DATA TEXT")
|
||||
db.execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_PDF417_EXTRA_METADATA TEXT")
|
||||
db.execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_POSSIBLE_COUNTRY TEXT")
|
||||
db.execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_SUGGESTED_PRICE TEXT")
|
||||
db.execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_UPC_EAN_EXTENSION TEXT")
|
||||
}
|
||||
|
||||
private fun addNameColumn(db: SQLiteDatabase) {
|
||||
db.execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_NAME TEXT")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun Cursor.getString(name: String) = this.getString(
|
||||
this.getColumnIndex(name)
|
||||
)
|
||||
|
||||
private fun Cursor.getBlob(name: String) = this.getBlob(
|
||||
this.getColumnIndex(name)
|
||||
)
|
||||
|
||||
private fun Cursor.getLong(name: String) = this.getLong(
|
||||
this.getColumnIndex(name)
|
||||
)
|
||||
@@ -0,0 +1,24 @@
|
||||
package de.markusfisch.android.binaryeye.database
|
||||
|
||||
import android.app.Activity
|
||||
import android.os.Environment
|
||||
import de.markusfisch.android.binaryeye.app.writeExternalFile
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
|
||||
fun exportDatabase(activity: Activity, fileName: String): Boolean {
|
||||
val dbFile = File(
|
||||
Environment.getDataDirectory(),
|
||||
"//data//${activity.packageName}//databases//${Database.FILE_NAME}"
|
||||
)
|
||||
if (!dbFile.exists()) {
|
||||
return false
|
||||
}
|
||||
return writeExternalFile(
|
||||
activity,
|
||||
fileName,
|
||||
"application/vnd.sqlite3"
|
||||
) {
|
||||
FileInputStream(dbFile).copyTo(it)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package de.markusfisch.android.binaryeye.database
|
||||
|
||||
import android.content.Context
|
||||
import android.database.Cursor
|
||||
import de.markusfisch.android.binaryeye.app.toHexString
|
||||
import de.markusfisch.android.binaryeye.app.writeExternalFile
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONException
|
||||
import org.json.JSONObject
|
||||
|
||||
fun exportJson(
|
||||
context: Context,
|
||||
name: String,
|
||||
cursor: Cursor
|
||||
) = writeExternalFile(context, name, "application/json") { outputStream ->
|
||||
exportJson(cursor)?.let {
|
||||
outputStream.write(it.toByteArray())
|
||||
}
|
||||
}
|
||||
|
||||
fun exportJson(cursor: Cursor): String? {
|
||||
if (!cursor.moveToFirst()) {
|
||||
return null
|
||||
}
|
||||
val columns = arrayOf(
|
||||
Database.SCANS_DATETIME,
|
||||
Database.SCANS_FORMAT,
|
||||
Database.SCANS_CONTENT,
|
||||
Database.SCANS_ERROR_CORRECTION_LEVEL,
|
||||
Database.SCANS_ISSUE_NUMBER,
|
||||
Database.SCANS_ORIENTATION,
|
||||
Database.SCANS_OTHER_META_DATA,
|
||||
Database.SCANS_PDF417_EXTRA_METADATA,
|
||||
Database.SCANS_POSSIBLE_COUNTRY,
|
||||
Database.SCANS_SUGGESTED_PRICE,
|
||||
Database.SCANS_UPC_EAN_EXTENSION
|
||||
)
|
||||
val indices = columns.map {
|
||||
Pair(cursor.getColumnIndex(it), it)
|
||||
}
|
||||
val contentIndex = cursor.getColumnIndex(Database.SCANS_CONTENT)
|
||||
val rawIndex = cursor.getColumnIndex(Database.SCANS_RAW)
|
||||
val root = JSONArray()
|
||||
do {
|
||||
var deviation: Pair<Int, String>? = null
|
||||
if (cursor.getString(contentIndex)?.isEmpty() == true) {
|
||||
deviation = Pair(
|
||||
contentIndex,
|
||||
cursor.getBlob(rawIndex).toHexString()
|
||||
)
|
||||
}
|
||||
root.put(cursor.toJsonObject(indices, deviation))
|
||||
} while (cursor.moveToNext())
|
||||
return root.toString()
|
||||
}
|
||||
|
||||
private fun Cursor.toJsonObject(
|
||||
indices: List<Pair<Int, String>>,
|
||||
deviation: Pair<Int, String>?
|
||||
): JSONObject {
|
||||
val obj = JSONObject()
|
||||
return try {
|
||||
indices.forEach {
|
||||
val value = if (deviation?.first == it.first) {
|
||||
deviation.second
|
||||
} else {
|
||||
this.getString(it.first)
|
||||
}
|
||||
obj.put(it.second, value ?: "")
|
||||
}
|
||||
obj
|
||||
} catch (e: JSONException) {
|
||||
obj
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
package de.markusfisch.android.binaryeye.database
|
||||
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
import android.text.format.DateFormat
|
||||
import com.google.zxing.Result
|
||||
import com.google.zxing.ResultMetadataType
|
||||
|
||||
data class Scan(
|
||||
val content: String,
|
||||
val raw: ByteArray?,
|
||||
val format: String,
|
||||
val errorCorrectionLevel: String?,
|
||||
val issueNumber: String?,
|
||||
val orientation: String?,
|
||||
val otherMetaData: String?,
|
||||
val pdf417ExtraMetaData: String?,
|
||||
val possibleCountry: String?,
|
||||
val suggestedPrice: String?,
|
||||
val upcEanExtension: String?,
|
||||
val timestamp: String = getDateTime(),
|
||||
var id: Long = 0L
|
||||
) : Parcelable {
|
||||
constructor(result: Result) : this(
|
||||
result.text,
|
||||
getRawBytes(result),
|
||||
result.barcodeFormat.toString(),
|
||||
result.getMetaString(ResultMetadataType.ERROR_CORRECTION_LEVEL),
|
||||
result.getMetaString(ResultMetadataType.ISSUE_NUMBER),
|
||||
result.getMetaString(ResultMetadataType.ORIENTATION),
|
||||
result.getMetaString(ResultMetadataType.OTHER),
|
||||
result.getMetaString(ResultMetadataType.PDF417_EXTRA_METADATA),
|
||||
result.getMetaString(ResultMetadataType.POSSIBLE_COUNTRY),
|
||||
result.getMetaString(ResultMetadataType.SUGGESTED_PRICE),
|
||||
result.getMetaString(ResultMetadataType.UPC_EAN_EXTENSION)
|
||||
)
|
||||
|
||||
// Needs to be overwritten manually, as ByteArray is an array and
|
||||
// this isn't handled well by Kotlin
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) {
|
||||
return true
|
||||
}
|
||||
if (javaClass != other?.javaClass) {
|
||||
return false
|
||||
}
|
||||
|
||||
other as Scan
|
||||
|
||||
return id == other.id &&
|
||||
timestamp == other.timestamp &&
|
||||
content == other.content &&
|
||||
((raw == null && other.raw == null) ||
|
||||
(raw != null && other.raw != null && raw.contentEquals(other.raw))) &&
|
||||
format == other.format &&
|
||||
errorCorrectionLevel == other.errorCorrectionLevel &&
|
||||
issueNumber == other.issueNumber &&
|
||||
orientation == other.orientation &&
|
||||
otherMetaData == other.otherMetaData &&
|
||||
pdf417ExtraMetaData == other.pdf417ExtraMetaData &&
|
||||
possibleCountry == other.possibleCountry &&
|
||||
suggestedPrice == other.suggestedPrice &&
|
||||
upcEanExtension == other.upcEanExtension
|
||||
}
|
||||
|
||||
// Needs to be overwritten manually, as ByteArray is an array and
|
||||
// this isn't handled well by Kotlin
|
||||
override fun hashCode(): Int {
|
||||
var result = id.hashCode()
|
||||
result = 31 * result + timestamp.hashCode()
|
||||
result = 31 * result + content.hashCode()
|
||||
result = 31 * result + (raw?.contentHashCode() ?: 0)
|
||||
result = 31 * result + format.hashCode()
|
||||
result = 31 * result + (errorCorrectionLevel?.hashCode() ?: 0)
|
||||
result = 31 * result + (issueNumber?.hashCode() ?: 0)
|
||||
result = 31 * result + (orientation?.hashCode() ?: 0)
|
||||
result = 31 * result + (otherMetaData?.hashCode() ?: 0)
|
||||
result = 31 * result + (pdf417ExtraMetaData?.hashCode() ?: 0)
|
||||
result = 31 * result + (possibleCountry?.hashCode() ?: 0)
|
||||
result = 31 * result + (suggestedPrice?.hashCode() ?: 0)
|
||||
result = 31 * result + (upcEanExtension?.hashCode() ?: 0)
|
||||
return result
|
||||
}
|
||||
|
||||
private constructor(parcel: Parcel) : this(
|
||||
content = parcel.readString() ?: "",
|
||||
raw = readByteArray(parcel),
|
||||
format = parcel.readString() ?: "",
|
||||
errorCorrectionLevel = parcel.readString(),
|
||||
issueNumber = parcel.readString(),
|
||||
orientation = parcel.readString(),
|
||||
otherMetaData = parcel.readString(),
|
||||
pdf417ExtraMetaData = parcel.readString(),
|
||||
possibleCountry = parcel.readString(),
|
||||
suggestedPrice = parcel.readString(),
|
||||
upcEanExtension = parcel.readString(),
|
||||
timestamp = parcel.readString() ?: "",
|
||||
id = parcel.readLong()
|
||||
)
|
||||
|
||||
override fun writeToParcel(parcel: Parcel, flags: Int) {
|
||||
parcel.writeString(content)
|
||||
writeByteArray(parcel, raw)
|
||||
parcel.writeString(format)
|
||||
parcel.writeString(errorCorrectionLevel)
|
||||
parcel.writeString(issueNumber)
|
||||
parcel.writeString(orientation)
|
||||
parcel.writeString(otherMetaData)
|
||||
parcel.writeString(pdf417ExtraMetaData)
|
||||
parcel.writeString(possibleCountry)
|
||||
parcel.writeString(suggestedPrice)
|
||||
parcel.writeString(upcEanExtension)
|
||||
parcel.writeString(timestamp)
|
||||
parcel.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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getDateTime(time: Long = System.currentTimeMillis()) = DateFormat.format(
|
||||
"yyyy-MM-dd HH:mm:ss",
|
||||
time
|
||||
).toString()
|
||||
|
||||
private fun getRawBytes(result: Result): ByteArray? {
|
||||
val metadata = result.resultMetadata ?: return null
|
||||
val segments = metadata[ResultMetadataType.BYTE_SEGMENTS] ?: return null
|
||||
var bytes = ByteArray(0)
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
for (seg in segments as Iterable<ByteArray>) {
|
||||
bytes += seg
|
||||
}
|
||||
// byte segments can never be shorter than the text.
|
||||
// Zxing cuts off content prefixes like "WIFI:"
|
||||
return if (bytes.size >= result.text.length) bytes else null
|
||||
}
|
||||
|
||||
private fun Result.getMetaString(
|
||||
key: ResultMetadataType
|
||||
): String? = this.resultMetadata?.let {
|
||||
it[key]?.toString()
|
||||
}
|
||||
|
||||
private fun writeByteArray(parcel: Parcel, array: ByteArray?) {
|
||||
val size = array?.size ?: 0
|
||||
parcel.writeInt(size)
|
||||
if (size > 0) {
|
||||
parcel.writeByteArray(array)
|
||||
}
|
||||
}
|
||||
|
||||
private fun readByteArray(parcel: Parcel): ByteArray? {
|
||||
val size = parcel.readInt()
|
||||
return if (size > 0) {
|
||||
val array = ByteArray(size)
|
||||
parcel.readByteArray(array)
|
||||
array
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
@@ -1,26 +1,57 @@
|
||||
package de.markusfisch.android.binaryeye.fragment
|
||||
|
||||
import com.google.zxing.BarcodeFormat
|
||||
|
||||
import de.markusfisch.android.binaryeye.zxing.Zxing
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
|
||||
import android.os.Bundle
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.AlertDialog
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
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.ImageView
|
||||
import android.widget.Toast
|
||||
import android.view.*
|
||||
import android.widget.EditText
|
||||
import com.google.zxing.BarcodeFormat
|
||||
import com.google.zxing.EncodeHintType
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.app.*
|
||||
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.binaryeye.zxing.encodeAsBitmap
|
||||
import de.markusfisch.android.binaryeye.zxing.encodeAsSvg
|
||||
import de.markusfisch.android.binaryeye.zxing.encodeAsText
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Dispatchers.Main
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
import java.io.OutputStream
|
||||
import java.util.*
|
||||
|
||||
class BarcodeFragment : Fragment() {
|
||||
private enum class FileType {
|
||||
PNG, SVG, TXT
|
||||
}
|
||||
|
||||
private var barcodeBitmap: Bitmap? = null
|
||||
private var barcodeSvg: String? = null
|
||||
private var barcodeTxt: String? = null
|
||||
private var content: String = ""
|
||||
private var format: BarcodeFormat? = 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.view_barcode)
|
||||
val ac = activity ?: return null
|
||||
ac.setTitle(R.string.view_barcode)
|
||||
|
||||
val view = inflater.inflate(
|
||||
R.layout.fragment_barcode,
|
||||
@@ -28,48 +59,212 @@ class BarcodeFragment : Fragment() {
|
||||
false
|
||||
)
|
||||
|
||||
val args = getArguments()
|
||||
args?.let {
|
||||
val size = args.getInt(SIZE)
|
||||
val bitmap: Bitmap?
|
||||
try {
|
||||
bitmap = Zxing.encodeAsBitmap(
|
||||
args.getString(CONTENT),
|
||||
args.getSerializable(FORMAT) as BarcodeFormat,
|
||||
size,
|
||||
size
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Toast.makeText(
|
||||
activity, e.message,
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
fragmentManager.popBackStack()
|
||||
return null
|
||||
val args = arguments ?: return view
|
||||
val content = args.getString(CONTENT) ?: return view
|
||||
val format = args.getSerializable(FORMAT) as BarcodeFormat? ?: return view
|
||||
val hints = args.getSerializable(HINTS) as EnumMap<EncodeHintType, Any>?
|
||||
val size = args.getInt(SIZE)
|
||||
try {
|
||||
barcodeBitmap = encodeAsBitmap(content, format, size, size, hints)
|
||||
barcodeSvg = encodeAsSvg(content, format, hints)
|
||||
barcodeTxt = encodeAsText(content, format, hints)
|
||||
} catch (e: Exception) {
|
||||
var message = e.message
|
||||
if (message == null || message.isEmpty()) {
|
||||
message = getString(R.string.error_encoding_barcode)
|
||||
}
|
||||
view.findViewById<ImageView>(R.id.barcode).setImageBitmap(bitmap)
|
||||
message?.let {
|
||||
ac.toast(message)
|
||||
}
|
||||
fragmentManager.popBackStack()
|
||||
return null
|
||||
}
|
||||
this.content = content
|
||||
this.format = format
|
||||
|
||||
val imageView = view.findViewById<ConfinedScalingImageView>(
|
||||
R.id.barcode
|
||||
)
|
||||
imageView.setImageBitmap(barcodeBitmap)
|
||||
imageView.post {
|
||||
// make sure to invoke this after ScalingImageView.onLayout()
|
||||
imageView.minWidth /= 2f
|
||||
}
|
||||
|
||||
view.findViewById<View>(R.id.share).setOnClickListener {
|
||||
pickFileType(context, R.string.share_as) {
|
||||
shareAs(it)
|
||||
}
|
||||
}
|
||||
|
||||
(view.findViewById(R.id.inset_layout) as View).setPaddingFromWindowInsets()
|
||||
imageView.doOnApplyWindowInsets { v, insets ->
|
||||
(v as ConfinedScalingImageView).insets.set(insets)
|
||||
}
|
||||
|
||||
return view
|
||||
}
|
||||
|
||||
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 -> {
|
||||
barcodeTxt?.let {
|
||||
context.copyToClipboard(it)
|
||||
context.toast(R.string.copied_to_clipboard)
|
||||
}
|
||||
true
|
||||
}
|
||||
R.id.export_to_file -> {
|
||||
pickFileType(context, R.string.export_as) {
|
||||
askForFileNameAndSave(it)
|
||||
}
|
||||
true
|
||||
}
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
}
|
||||
|
||||
private fun pickFileType(
|
||||
context: Context,
|
||||
title: Int,
|
||||
action: (FileType) -> Unit
|
||||
) {
|
||||
val fileTypes = FileType.values()
|
||||
AlertDialog.Builder(context)
|
||||
.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
|
||||
if (!hasWritePermission(ac) { 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("${format.toString()}_$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
|
||||
) {
|
||||
barcodeBitmap?.saveAsPng(it)
|
||||
}
|
||||
FileType.SVG -> saveAs(
|
||||
addSuffixIfNotGiven(fileName, ".svg"),
|
||||
MIME_SVG
|
||||
) { outputStream ->
|
||||
barcodeSvg?.let {
|
||||
outputStream.write(it.toByteArray())
|
||||
}
|
||||
}
|
||||
FileType.TXT -> saveAs(
|
||||
addSuffixIfNotGiven(fileName, ".txt"),
|
||||
MIME_TXT
|
||||
) { outputStream ->
|
||||
barcodeTxt?.let {
|
||||
outputStream.write(it.toByteArray())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||
}
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun saveAs(
|
||||
fileName: String,
|
||||
mimeType: String,
|
||||
write: (outputStream: OutputStream) -> Unit
|
||||
) {
|
||||
val ac = activity ?: return
|
||||
GlobalScope.launch {
|
||||
val message = writeExternalFile(ac, fileName, mimeType, write).toSaveResult()
|
||||
GlobalScope.launch(Main) {
|
||||
ac.toast(message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun shareAs(fileType: FileType) {
|
||||
when (fileType) {
|
||||
FileType.PNG -> barcodeBitmap?.let { share(it) }
|
||||
FileType.SVG -> barcodeSvg?.let { shareText(context, it, MIME_SVG) }
|
||||
FileType.TXT -> barcodeTxt?.let { shareText(context, it) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun share(bitmap: Bitmap) {
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
val file = File(
|
||||
context.externalCacheDir,
|
||||
"shared_barcode.png"
|
||||
)
|
||||
val success = try {
|
||||
FileOutputStream(file).use {
|
||||
bitmap.saveAsPng(it)
|
||||
}
|
||||
true
|
||||
} catch (e: IOException) {
|
||||
false
|
||||
}
|
||||
GlobalScope.launch(Main) {
|
||||
if (success) {
|
||||
shareFile(context, file, "image/png")
|
||||
} else {
|
||||
activity?.toast(R.string.error_saving_file)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val CONTENT = "content"
|
||||
private const val FORMAT = "format"
|
||||
private const val HINTS = "hints"
|
||||
private const val SIZE = "size"
|
||||
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: BarcodeFormat,
|
||||
size: Int
|
||||
size: Int,
|
||||
hints: EnumMap<EncodeHintType, Any>? = null
|
||||
): Fragment {
|
||||
val args = Bundle()
|
||||
args.putString(CONTENT, content)
|
||||
args.putSerializable(FORMAT, format)
|
||||
hints?.let {
|
||||
args.putSerializable(HINTS, it)
|
||||
}
|
||||
args.putInt(SIZE, size)
|
||||
val fragment = BarcodeFragment()
|
||||
fragment.setArguments(args)
|
||||
fragment.arguments = args
|
||||
return fragment
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun Bitmap.saveAsPng(outputStream: OutputStream, quality: Int = 90) {
|
||||
this.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('_').toLowerCase(Locale.getDefault())
|
||||
|
||||
@@ -1,28 +1,47 @@
|
||||
package de.markusfisch.android.binaryeye.fragment
|
||||
|
||||
import com.google.zxing.BarcodeFormat
|
||||
|
||||
import de.markusfisch.android.binaryeye.app.addFragment
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.support.design.widget.FloatingActionButton
|
||||
import android.support.v4.app.Fragment
|
||||
import android.text.ClipboardManager
|
||||
import android.view.LayoutInflater
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.text.Editable
|
||||
import android.text.TextWatcher
|
||||
import android.view.*
|
||||
import android.widget.EditText
|
||||
import android.widget.Toast
|
||||
import android.widget.TableLayout
|
||||
import android.widget.TableRow
|
||||
import android.widget.TextView
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.actions.ActionRegistry
|
||||
import de.markusfisch.android.binaryeye.actions.wifi.WifiAction
|
||||
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.database.Scan
|
||||
import de.markusfisch.android.binaryeye.view.setPaddingFromWindowInsets
|
||||
import de.markusfisch.android.binaryeye.widget.toast
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class DecodeFragment : Fragment() {
|
||||
private lateinit var contentView: EditText
|
||||
private lateinit var format: BarcodeFormat
|
||||
private lateinit var formatView: TextView
|
||||
private lateinit var metaView: TableLayout
|
||||
private lateinit var hexView: TextView
|
||||
private lateinit var format: String
|
||||
private lateinit var fab: FloatingActionButton
|
||||
|
||||
private val parentJob = Job()
|
||||
private val scope: CoroutineScope = CoroutineScope(Dispatchers.Main + parentJob)
|
||||
private val content: String
|
||||
get() = contentView.text.toString()
|
||||
|
||||
private var closeAutomatically = false
|
||||
private var action = ActionRegistry.DEFAULT_ACTION
|
||||
private var isBinary = false
|
||||
private var id = 0L
|
||||
|
||||
override fun onCreate(state: Bundle?) {
|
||||
super.onCreate(state)
|
||||
@@ -34,7 +53,7 @@ class DecodeFragment : Fragment() {
|
||||
container: ViewGroup?,
|
||||
state: Bundle?
|
||||
): View {
|
||||
activity.setTitle(R.string.content)
|
||||
activity?.setTitle(R.string.content)
|
||||
|
||||
val view = inflater.inflate(
|
||||
R.layout.fragment_decode,
|
||||
@@ -42,96 +61,304 @@ class DecodeFragment : Fragment() {
|
||||
false
|
||||
)
|
||||
|
||||
val content = arguments?.getString(CONTENT) ?: ""
|
||||
format = arguments?.getSerializable(FORMAT) as BarcodeFormat? ?: BarcodeFormat.QR_CODE
|
||||
closeAutomatically = prefs.closeAutomatically &&
|
||||
activity?.intent?.hasExtra(MainActivity.DECODED) == true
|
||||
|
||||
contentView = view.findViewById<EditText>(R.id.content)
|
||||
contentView.setText(content)
|
||||
val scan = arguments?.getParcelable(SCAN) as Scan?
|
||||
?: throw IllegalArgumentException("DecodeFragment needs a Scan")
|
||||
id = scan.id
|
||||
|
||||
view.findViewById<View>(R.id.share).setOnClickListener { _ ->
|
||||
share(getContent())
|
||||
val inputContent = scan.content
|
||||
isBinary = hasNonPrintableCharacters(
|
||||
inputContent
|
||||
) or inputContent.isEmpty()
|
||||
val raw = scan.raw ?: inputContent.toByteArray()
|
||||
format = scan.format
|
||||
|
||||
contentView = view.findViewById(R.id.content)
|
||||
fab = view.findViewById(R.id.open)
|
||||
|
||||
if (!isBinary) {
|
||||
contentView.setText(inputContent)
|
||||
contentView.addTextChangedListener(object : TextWatcher {
|
||||
override fun afterTextChanged(s: Editable?) {
|
||||
updateViewsAndAction(content.toByteArray())
|
||||
}
|
||||
|
||||
override fun beforeTextChanged(
|
||||
s: CharSequence?,
|
||||
start: Int,
|
||||
count: Int,
|
||||
after: Int
|
||||
) {
|
||||
}
|
||||
|
||||
override fun onTextChanged(
|
||||
s: CharSequence?,
|
||||
start: Int,
|
||||
before: Int,
|
||||
count: Int
|
||||
) {
|
||||
}
|
||||
})
|
||||
fab.setOnClickListener {
|
||||
executeAction(content.toByteArray())
|
||||
}
|
||||
if (prefs.openImmediately) {
|
||||
executeAction(content.toByteArray())
|
||||
}
|
||||
} else {
|
||||
contentView.setText(R.string.binary_data)
|
||||
contentView.isEnabled = false
|
||||
fab.setImageResource(R.drawable.ic_action_save)
|
||||
fab.setOnClickListener {
|
||||
askForFileNameAndSave(raw)
|
||||
}
|
||||
}
|
||||
|
||||
formatView = view.findViewById(R.id.format)
|
||||
metaView = view.findViewById(R.id.meta)
|
||||
hexView = view.findViewById(R.id.hex)
|
||||
|
||||
updateViewsAndAction(raw)
|
||||
|
||||
if (prefs.showMetaData) {
|
||||
fillMetaView(metaView, scan)
|
||||
}
|
||||
|
||||
(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 updateViewsAndAction(bytes: ByteArray) {
|
||||
val prevAction = action
|
||||
if (!prevAction.canExecuteOn(bytes)) {
|
||||
action = ActionRegistry.getAction(bytes)
|
||||
}
|
||||
formatView.text = resources.getQuantityString(
|
||||
R.plurals.barcode_info,
|
||||
bytes.size,
|
||||
prettifyFormatName(format),
|
||||
bytes.size
|
||||
)
|
||||
hexView.text = if (prefs.showHexDump) hexDump(bytes) else ""
|
||||
if (prevAction !== action) {
|
||||
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 fillMetaView(tableLayout: TableLayout, scan: Scan) {
|
||||
val ctx = tableLayout.context
|
||||
val spaceBetween = (16f * ctx.resources.displayMetrics.density).toInt()
|
||||
var hasMeta = false
|
||||
sortedMapOf(
|
||||
R.string.error_correction_level to scan.errorCorrectionLevel,
|
||||
R.string.issue_number to scan.issueNumber,
|
||||
R.string.orientation to scan.orientation,
|
||||
R.string.other_meta_data to scan.otherMetaData,
|
||||
R.string.pdf417_extra_metadata to scan.pdf417ExtraMetaData,
|
||||
R.string.possible_country to scan.possibleCountry,
|
||||
R.string.suggested_price to scan.suggestedPrice,
|
||||
R.string.upc_ean_extension to scan.upcEanExtension
|
||||
).forEach { item ->
|
||||
item.value?.let {
|
||||
val tr = TableRow(ctx)
|
||||
val keyView = TextView(ctx)
|
||||
keyView.setText(item.key)
|
||||
val valueView = TextView(ctx)
|
||||
valueView.setPadding(spaceBetween, 0, 0, 0)
|
||||
valueView.text = it
|
||||
tr.addView(keyView)
|
||||
tr.addView(valueView)
|
||||
tableLayout.addView(tr)
|
||||
hasMeta = true
|
||||
}
|
||||
}
|
||||
if (hasMeta) {
|
||||
tableLayout.setPadding(0, 0, 0, spaceBetween)
|
||||
}
|
||||
}
|
||||
|
||||
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_to_clipboard -> {
|
||||
copyToClipboard(getContent())
|
||||
R.id.copy_password -> {
|
||||
copyPasswordToClipboard()
|
||||
maybeBackOrFinish()
|
||||
true
|
||||
}
|
||||
R.id.open_url -> {
|
||||
openUrl(getContent())
|
||||
R.id.copy_to_clipboard -> {
|
||||
copyToClipboard(content)
|
||||
maybeBackOrFinish()
|
||||
true
|
||||
}
|
||||
R.id.share -> {
|
||||
context?.also {
|
||||
shareText(it, content)
|
||||
maybeBackOrFinish()
|
||||
}
|
||||
true
|
||||
}
|
||||
R.id.create -> {
|
||||
addFragment(
|
||||
fragmentManager,
|
||||
EncodeFragment.newInstance(getContent(), format)
|
||||
EncodeFragment.newInstance(content, format)
|
||||
)
|
||||
true
|
||||
}
|
||||
R.id.remove -> {
|
||||
db.removeScan(id)
|
||||
backOrFinish()
|
||||
true
|
||||
}
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getContent(): String {
|
||||
return contentView.getText().toString()
|
||||
private fun copyPasswordToClipboard() {
|
||||
val ac = action
|
||||
if (ac is WifiAction) {
|
||||
ac.password?.let { password ->
|
||||
activity?.apply {
|
||||
copyToClipboard(password)
|
||||
toast(R.string.copied_password_to_clipboard)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun copyToClipboard(text: String) {
|
||||
activity ?: return
|
||||
|
||||
val cm = activity.getSystemService(
|
||||
Context.CLIPBOARD_SERVICE
|
||||
) as ClipboardManager
|
||||
cm.setText(text)
|
||||
Toast.makeText(
|
||||
activity,
|
||||
R.string.put_into_clipboard,
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
activity?.apply {
|
||||
copyToClipboard(text)
|
||||
toast(R.string.copied_to_clipboard)
|
||||
}
|
||||
}
|
||||
|
||||
private fun openUrl(url: String) {
|
||||
if (activity == null || url.isEmpty()) {
|
||||
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: ByteArray) {
|
||||
val ac = activity ?: return
|
||||
if (content.isNotEmpty()) {
|
||||
if (action is WifiAction &&
|
||||
Build.VERSION.SDK_INT < Build.VERSION_CODES.Q &&
|
||||
!hasLocationPermission(ac) { executeAction(content) }
|
||||
) {
|
||||
return
|
||||
}
|
||||
scope.launch {
|
||||
action.execute(ac, content)
|
||||
maybeBackOrFinish()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun askForFileNameAndSave(raw: ByteArray) {
|
||||
val ac = activity ?: return
|
||||
if (!hasWritePermission(ac) { askForFileNameAndSave(raw) }) {
|
||||
return
|
||||
}
|
||||
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
|
||||
if (intent.resolveActivity(activity.getPackageManager()) != null) {
|
||||
startActivity(intent)
|
||||
} else {
|
||||
Toast.makeText(
|
||||
activity,
|
||||
R.string.cannot_resolve_action,
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
scope.launch(Dispatchers.Main) {
|
||||
val name = ac.askForFileName() ?: return@launch
|
||||
val message = writeExternalFile(
|
||||
ac,
|
||||
name,
|
||||
"application/octet-stream"
|
||||
) {
|
||||
it.write(raw)
|
||||
}.toSaveResult()
|
||||
ac.toast(message)
|
||||
}
|
||||
}
|
||||
|
||||
private fun share(text: String) {
|
||||
val intent = Intent(Intent.ACTION_SEND)
|
||||
intent.putExtra(Intent.EXTRA_TEXT, text)
|
||||
intent.setType("text/plain")
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val CONTENT = "content"
|
||||
private const val FORMAT = "format"
|
||||
private const val SCAN = "scan"
|
||||
|
||||
fun newInstance(content: String, format: BarcodeFormat): Fragment {
|
||||
fun newInstance(scan: Scan): Fragment {
|
||||
val args = Bundle()
|
||||
args.putString(CONTENT, content)
|
||||
args.putSerializable(FORMAT, format)
|
||||
args.putParcelable(SCAN, scan)
|
||||
val fragment = DecodeFragment()
|
||||
fragment.arguments = args
|
||||
return fragment
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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.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()
|
||||
}
|
||||
|
||||
@@ -1,28 +1,30 @@
|
||||
package de.markusfisch.android.binaryeye.fragment
|
||||
|
||||
import com.google.zxing.BarcodeFormat
|
||||
|
||||
import de.markusfisch.android.binaryeye.app.addFragment
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.support.v4.app.Fragment
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.EditText
|
||||
import android.widget.SeekBar
|
||||
import android.widget.Spinner
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import android.widget.*
|
||||
import com.google.zxing.BarcodeFormat
|
||||
import com.google.zxing.EncodeHintType
|
||||
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel
|
||||
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.hideSoftKeyboard
|
||||
import de.markusfisch.android.binaryeye.app.prefs
|
||||
import de.markusfisch.android.binaryeye.view.setPaddingFromWindowInsets
|
||||
import java.util.*
|
||||
|
||||
class EncodeFragment : Fragment() {
|
||||
private lateinit var formatView: Spinner
|
||||
private lateinit var errorCorrectionLabel: TextView
|
||||
private lateinit var errorCorrectionLevel: Spinner
|
||||
private lateinit var sizeView: TextView
|
||||
private lateinit var sizeBarView: SeekBar
|
||||
private lateinit var contentView: EditText
|
||||
|
||||
private val writers = arrayListOf(
|
||||
BarcodeFormat.AZTEC,
|
||||
BarcodeFormat.CODABAR,
|
||||
@@ -41,8 +43,9 @@ class EncodeFragment : Fragment() {
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
state: Bundle?
|
||||
): View {
|
||||
activity.setTitle(R.string.compose_barcode)
|
||||
): View? {
|
||||
val ac = activity ?: return null
|
||||
ac.setTitle(R.string.compose_barcode)
|
||||
|
||||
val view = inflater.inflate(
|
||||
R.layout.fragment_encode,
|
||||
@@ -50,56 +53,107 @@ class EncodeFragment : Fragment() {
|
||||
false
|
||||
)
|
||||
|
||||
formatView = view.findViewById<Spinner>(R.id.format)
|
||||
formatView.setAdapter(
|
||||
ArrayAdapter<String>(
|
||||
activity,
|
||||
android.R.layout.simple_list_item_1,
|
||||
writers.map { it -> it.name })
|
||||
formatView = view.findViewById(R.id.format)
|
||||
val formatAdapter = ArrayAdapter(
|
||||
ac,
|
||||
android.R.layout.simple_spinner_item,
|
||||
writers.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 visibility = if (
|
||||
writers[position] == BarcodeFormat.QR_CODE
|
||||
) {
|
||||
View.VISIBLE
|
||||
} else {
|
||||
View.GONE
|
||||
}
|
||||
errorCorrectionLabel.visibility = visibility
|
||||
errorCorrectionLevel.visibility = visibility
|
||||
}
|
||||
|
||||
sizeView = view.findViewById<TextView>(R.id.size_display)
|
||||
sizeBarView = view.findViewById<SeekBar>(R.id.size_bar)
|
||||
override fun onNothingSelected(parentView: AdapterView<*>?) {}
|
||||
}
|
||||
|
||||
errorCorrectionLabel = view.findViewById(R.id.error_correction_label)
|
||||
errorCorrectionLevel = view.findViewById(R.id.error_correction_level)
|
||||
|
||||
sizeView = view.findViewById(R.id.size_display)
|
||||
sizeBarView = view.findViewById(R.id.size_bar)
|
||||
initSizeBar()
|
||||
|
||||
val contentView = view.findViewById<EditText>(R.id.content)
|
||||
contentView = view.findViewById(R.id.content)
|
||||
|
||||
val args = getArguments()
|
||||
args?.let {
|
||||
contentView.setText(args.getString(CONTENT))
|
||||
formatView.setSelection(
|
||||
writers.indexOf(
|
||||
args.getSerializable(FORMAT) as BarcodeFormat?
|
||||
)
|
||||
)
|
||||
val args = arguments
|
||||
args?.also {
|
||||
contentView.setText(it.getString(CONTENT))
|
||||
}
|
||||
|
||||
view.findViewById<View>(R.id.encode).setOnClickListener { v ->
|
||||
val format = writers.get(formatView.getSelectedItemPosition())
|
||||
var size = getSize(sizeBarView.getProgress())
|
||||
val content = contentView.getText().toString()
|
||||
if (content.isEmpty()) {
|
||||
Toast.makeText(
|
||||
v.context, R.string.error_no_content,
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
} else {
|
||||
hideSoftKeyboard(contentView)
|
||||
addFragment(
|
||||
fragmentManager, BarcodeFragment.newInstance(
|
||||
content,
|
||||
format,
|
||||
size
|
||||
)
|
||||
)
|
||||
val barcodeFormat = args?.getString(FORMAT)
|
||||
if (barcodeFormat != null) {
|
||||
formatView.setSelection(
|
||||
writers.indexOf(resolveFormat(barcodeFormat))
|
||||
)
|
||||
} else if (state == null) {
|
||||
formatView.post {
|
||||
formatView.setSelection(prefs.indexOfLastSelectedFormat)
|
||||
}
|
||||
}
|
||||
|
||||
view.findViewById<View>(R.id.encode).setOnClickListener {
|
||||
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
|
||||
}
|
||||
|
||||
private fun encode() {
|
||||
val hints = EnumMap<EncodeHintType, Any>(EncodeHintType::class.java)
|
||||
val format = writers[formatView.selectedItemPosition]
|
||||
if (format == BarcodeFormat.QR_CODE) {
|
||||
hints[EncodeHintType.ERROR_CORRECTION] = arrayListOf(
|
||||
ErrorCorrectionLevel.L,
|
||||
ErrorCorrectionLevel.M,
|
||||
ErrorCorrectionLevel.Q,
|
||||
ErrorCorrectionLevel.H
|
||||
)[errorCorrectionLevel.selectedItemPosition]
|
||||
}
|
||||
val size = getSize(sizeBarView.progress)
|
||||
val content = contentView.text.toString()
|
||||
if (content.isEmpty()) {
|
||||
Toast.makeText(
|
||||
context,
|
||||
R.string.error_no_content,
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
} else {
|
||||
activity?.hideSoftKeyboard(contentView)
|
||||
addFragment(
|
||||
fragmentManager,
|
||||
BarcodeFragment.newInstance(content, format, size, hints)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun initSizeBar() {
|
||||
updateSize(sizeBarView.getProgress())
|
||||
updateSize(sizeBarView.progress)
|
||||
sizeBarView.setOnSeekBarChangeListener(
|
||||
object : SeekBar.OnSeekBarChangeListener {
|
||||
override fun onProgressChanged(
|
||||
@@ -118,34 +172,31 @@ class EncodeFragment : Fragment() {
|
||||
|
||||
private fun updateSize(power: Int) {
|
||||
val size = getSize(power)
|
||||
sizeView.setText("${size}x$size")
|
||||
sizeView.text = getString(R.string.width_by_height, size, size)
|
||||
}
|
||||
|
||||
private fun getSize(power: Int) = 128 * (power + 1)
|
||||
|
||||
private fun hideSoftKeyboard(view: View) {
|
||||
val im = activity?.getSystemService(
|
||||
Context.INPUT_METHOD_SERVICE
|
||||
) as InputMethodManager?
|
||||
im?.let {
|
||||
im.hideSoftInputFromWindow(view.getWindowToken(), 0)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val CONTENT = "content"
|
||||
private const val FORMAT = "format"
|
||||
|
||||
fun newInstance(
|
||||
content: String,
|
||||
format: BarcodeFormat = BarcodeFormat.AZTEC
|
||||
format: String? = null
|
||||
): Fragment {
|
||||
val args = Bundle()
|
||||
args.putString(CONTENT, content)
|
||||
args.putSerializable(FORMAT, format)
|
||||
format?.let { args.putString(FORMAT, it) }
|
||||
val fragment = EncodeFragment()
|
||||
fragment.setArguments(args)
|
||||
fragment.arguments = args
|
||||
return fragment
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun resolveFormat(name: String): BarcodeFormat = try {
|
||||
BarcodeFormat.valueOf(name)
|
||||
} catch (_: IllegalArgumentException) {
|
||||
BarcodeFormat.QR_CODE
|
||||
}
|
||||
|
||||
@@ -0,0 +1,446 @@
|
||||
package de.markusfisch.android.binaryeye.fragment
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.AlertDialog
|
||||
import android.app.SearchManager
|
||||
import android.content.Context
|
||||
import android.database.Cursor
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Parcelable
|
||||
import android.support.v4.app.ActivityCompat
|
||||
import android.support.v4.app.Fragment
|
||||
import android.support.v4.content.ContextCompat
|
||||
import android.support.v7.app.AppCompatActivity
|
||||
import android.support.v7.view.ActionMode
|
||||
import android.support.v7.widget.SearchView
|
||||
import android.support.v7.widget.SwitchCompat
|
||||
import android.view.*
|
||||
import android.widget.EditText
|
||||
import android.widget.ListView
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.adapter.ScansAdapter
|
||||
import de.markusfisch.android.binaryeye.app.*
|
||||
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.view.setPaddingFromWindowInsets
|
||||
import de.markusfisch.android.binaryeye.view.useVisibility
|
||||
import de.markusfisch.android.binaryeye.widget.toast
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
class HistoryFragment : Fragment() {
|
||||
private lateinit var useHistorySwitch: SwitchCompat
|
||||
private lateinit var listView: ListView
|
||||
private lateinit var fab: View
|
||||
private lateinit var progressView: View
|
||||
|
||||
private val parentJob = Job()
|
||||
private val scope = CoroutineScope(Dispatchers.IO + parentJob)
|
||||
private val actionModeCallback = object : ActionMode.Callback {
|
||||
override fun onCreateActionMode(
|
||||
mode: ActionMode,
|
||||
menu: Menu
|
||||
): Boolean {
|
||||
mode.menuInflater.inflate(
|
||||
R.menu.fragment_history_edit,
|
||||
menu
|
||||
)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
lockStatusBarColor()
|
||||
val ac = activity ?: return false
|
||||
ac.window.statusBarColor = ContextCompat.getColor(
|
||||
ac,
|
||||
R.color.accent_dark
|
||||
)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onPrepareActionMode(
|
||||
mode: ActionMode,
|
||||
menu: Menu
|
||||
): Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onActionItemClicked(
|
||||
mode: ActionMode,
|
||||
item: MenuItem
|
||||
): Boolean {
|
||||
val ac = activity ?: return false
|
||||
return when (item.itemId) {
|
||||
R.id.copy_scan -> {
|
||||
scansAdapter?.getSelectedContent()?.let {
|
||||
ac.copyToClipboard(it)
|
||||
ac.toast(R.string.copied_to_clipboard)
|
||||
}
|
||||
closeActionMode()
|
||||
true
|
||||
}
|
||||
R.id.edit_scan -> {
|
||||
scansAdapter?.let {
|
||||
askForName(
|
||||
ac,
|
||||
it.selectedScanId,
|
||||
getScanName(it.selectedScanPosition)
|
||||
)
|
||||
}
|
||||
closeActionMode()
|
||||
true
|
||||
}
|
||||
R.id.remove_scan -> {
|
||||
scansAdapter?.let {
|
||||
askToRemoveScan(ac, it.selectedScanId)
|
||||
}
|
||||
closeActionMode()
|
||||
true
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroyActionMode(mode: ActionMode) {
|
||||
closeActionMode()
|
||||
}
|
||||
}
|
||||
|
||||
private var scansAdapter: ScansAdapter? = null
|
||||
private var listViewState: Parcelable? = null
|
||||
private var actionMode: ActionMode? = null
|
||||
private var filter: String? = null
|
||||
private var clearListMenuItem: MenuItem? = null
|
||||
private var exportHistoryMenuItem: MenuItem? = null
|
||||
|
||||
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.history)
|
||||
|
||||
val view = inflater.inflate(
|
||||
R.layout.fragment_history,
|
||||
container,
|
||||
false
|
||||
)
|
||||
|
||||
useHistorySwitch = view.findViewById(
|
||||
R.id.use_history
|
||||
) as SwitchCompat
|
||||
initHistorySwitch(useHistorySwitch)
|
||||
|
||||
listView = view.findViewById(R.id.scans)
|
||||
listView.setOnItemClickListener { _, _, _, id ->
|
||||
showScan(id)
|
||||
}
|
||||
listView.setOnItemLongClickListener { _, v, position, id ->
|
||||
v.isSelected = true
|
||||
scansAdapter?.select(id, position)
|
||||
if (actionMode == null && ac is AppCompatActivity) {
|
||||
actionMode = ac.delegate.startSupportActionMode(
|
||||
actionModeCallback
|
||||
)
|
||||
}
|
||||
true
|
||||
}
|
||||
listView.setOnScrollListener(systemBarListViewScrollListener)
|
||||
|
||||
fab = view.findViewById(R.id.share)
|
||||
fab.setOnClickListener { v ->
|
||||
pickListSeparatorAndShare(v.context)
|
||||
}
|
||||
|
||||
progressView = view.findViewById(R.id.progress_view)
|
||||
|
||||
(view.findViewById(R.id.inset_layout) as View).setPaddingFromWindowInsets()
|
||||
listView.setPaddingFromWindowInsets()
|
||||
|
||||
update()
|
||||
|
||||
return view
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
scansAdapter?.changeCursor(null)
|
||||
parentJob.cancel()
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
listViewState = listView.onSaveInstanceState()
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
inflater.inflate(R.menu.fragment_history, menu)
|
||||
initSearchView(menu.findItem(R.id.search))
|
||||
menu.setGroupVisible(R.id.scans_available, scansAdapter?.count != 0)
|
||||
clearListMenuItem = menu.findItem(R.id.clear)
|
||||
exportHistoryMenuItem = menu.findItem(R.id.export_history)
|
||||
}
|
||||
|
||||
private fun initSearchView(item: MenuItem?) {
|
||||
item ?: return
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) {
|
||||
item.isVisible = false
|
||||
} else {
|
||||
val ac = activity ?: return
|
||||
val searchView = item.actionView as SearchView
|
||||
val searchManager = ac.getSystemService(
|
||||
Context.SEARCH_SERVICE
|
||||
) as SearchManager
|
||||
searchView.setSearchableInfo(
|
||||
searchManager.getSearchableInfo(ac.componentName)
|
||||
)
|
||||
searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
|
||||
override fun onQueryTextSubmit(query: String): Boolean {
|
||||
update(query)
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onQueryTextChange(query: String): Boolean {
|
||||
update(query)
|
||||
return false
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
return when (item.itemId) {
|
||||
R.id.clear -> {
|
||||
askToRemoveScans(context)
|
||||
true
|
||||
}
|
||||
R.id.export_history -> {
|
||||
askToExportToFile(context)
|
||||
true
|
||||
}
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
}
|
||||
|
||||
private fun initHistorySwitch(switchView: SwitchCompat) {
|
||||
switchView.setOnCheckedChangeListener { _, isChecked ->
|
||||
prefs.useHistory = isChecked
|
||||
}
|
||||
if (prefs.useHistory) {
|
||||
switchView.toggle()
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateAndClearFilter() {
|
||||
filter = null
|
||||
update()
|
||||
}
|
||||
|
||||
private fun update(query: String? = null) {
|
||||
query?.let { filter = it }
|
||||
scope.launch {
|
||||
val cursor = db.getScans(filter)
|
||||
withContext(Dispatchers.Main) {
|
||||
val ac = activity ?: return@withContext
|
||||
val hasScans = cursor != null && cursor.count > 0
|
||||
if (filter == null) {
|
||||
if (!hasScans) {
|
||||
listView.emptyView = useHistorySwitch
|
||||
}
|
||||
ActivityCompat.invalidateOptionsMenu(ac)
|
||||
}
|
||||
enableMenuItems(hasScans)
|
||||
fab.visibility = if (hasScans) {
|
||||
View.VISIBLE
|
||||
} else {
|
||||
View.GONE
|
||||
}
|
||||
cursor?.let { cursor ->
|
||||
// close previous cursor
|
||||
scansAdapter?.also { it.changeCursor(null) }
|
||||
scansAdapter = ScansAdapter(ac, cursor)
|
||||
listView.adapter = scansAdapter
|
||||
listViewState?.also {
|
||||
listView.onRestoreInstanceState(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun enableMenuItems(enabled: Boolean) {
|
||||
clearListMenuItem?.isEnabled = enabled
|
||||
exportHistoryMenuItem?.isEnabled = enabled
|
||||
}
|
||||
|
||||
private fun closeActionMode() {
|
||||
unlockStatusBarColor()
|
||||
scansAdapter?.clearSelection()
|
||||
actionMode?.finish()
|
||||
actionMode = null
|
||||
scansAdapter?.notifyDataSetChanged()
|
||||
}
|
||||
|
||||
private fun showScan(id: Long) = db.getScan(id)?.also { scan ->
|
||||
closeActionMode()
|
||||
try {
|
||||
addFragment(
|
||||
fragmentManager,
|
||||
DecodeFragment.newInstance(scan)
|
||||
)
|
||||
} catch (e: IllegalArgumentException) {
|
||||
// ignore, can never happen
|
||||
}
|
||||
}
|
||||
|
||||
private fun getScanName(position: Int): String? {
|
||||
val cursor = scansAdapter?.getItem(position) as Cursor?
|
||||
return cursor?.getString(cursor.getColumnIndex(Database.SCANS_NAME))
|
||||
}
|
||||
|
||||
// dialogs don't have a parent layout
|
||||
@SuppressLint("InflateParams")
|
||||
private fun askForName(context: Context, id: Long, text: String?) {
|
||||
val view = LayoutInflater.from(context).inflate(
|
||||
R.layout.dialog_enter_name, null
|
||||
)
|
||||
val nameView = view.findViewById<EditText>(R.id.name)
|
||||
nameView.setText(text)
|
||||
AlertDialog.Builder(context)
|
||||
.setView(view)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
val name = nameView.text.toString()
|
||||
db.renameScan(id, name)
|
||||
update()
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun askToRemoveScan(context: Context, id: Long) {
|
||||
AlertDialog.Builder(context)
|
||||
.setMessage(R.string.really_remove_scan)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
db.removeScan(id)
|
||||
if (scansAdapter?.count == 1) {
|
||||
updateAndClearFilter()
|
||||
} else {
|
||||
update()
|
||||
}
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||
}
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun askToRemoveScans(context: Context) {
|
||||
AlertDialog.Builder(context)
|
||||
.setMessage(
|
||||
if (filter == null) {
|
||||
R.string.really_remove_all_scans
|
||||
} else {
|
||||
R.string.really_remove_selected_scans
|
||||
}
|
||||
)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
db.removeScans(filter)
|
||||
updateAndClearFilter()
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||
}
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun askToExportToFile(context: Context) {
|
||||
scope.launch {
|
||||
val ac = activity ?: return@launch
|
||||
progressView.useVisibility {
|
||||
if (!hasWritePermission(ac) { askToExportToFile(context) }) {
|
||||
return@useVisibility
|
||||
}
|
||||
val options = context.resources.getStringArray(
|
||||
R.array.export_options_values
|
||||
)
|
||||
val delimiter = alertDialog<String>(context) { resume ->
|
||||
setTitle(R.string.export_as)
|
||||
setItems(R.array.export_options_names) { _, which ->
|
||||
resume(options[which])
|
||||
}
|
||||
} ?: return@useVisibility
|
||||
val name = withContext(Dispatchers.Main) {
|
||||
ac.askForFileName(
|
||||
when (delimiter) {
|
||||
"db" -> ".db"
|
||||
"json" -> ".json"
|
||||
else -> ".csv"
|
||||
}
|
||||
)
|
||||
} ?: return@useVisibility
|
||||
val message = when (delimiter) {
|
||||
"db" -> exportDatabase(ac, name)
|
||||
else -> db.getScansDetailed(filter)?.use {
|
||||
when (delimiter) {
|
||||
"json" -> exportJson(context, name, it)
|
||||
else -> exportCsv(context, name, it, delimiter)
|
||||
}
|
||||
} ?: false
|
||||
}.toSaveResult()
|
||||
withContext(Dispatchers.Main) {
|
||||
ac.toast(message)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun pickListSeparatorAndShare(context: Context) {
|
||||
val separators = context.resources.getStringArray(
|
||||
R.array.list_separators_values
|
||||
)
|
||||
AlertDialog.Builder(context)
|
||||
.setTitle(R.string.pick_list_separator)
|
||||
.setItems(R.array.list_separators_names) { _, which ->
|
||||
shareScans(separators[which])
|
||||
}
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun shareScans(format: String) = scope.launch {
|
||||
progressView.useVisibility {
|
||||
var text: String? = null
|
||||
db.getScansDetailed(filter)?.use { cursor ->
|
||||
val details = format.split(":")
|
||||
text = when (details[0]) {
|
||||
"text" -> exportText(cursor, details[1])
|
||||
"csv" -> exportCsv(cursor, details[1])
|
||||
else -> exportJson(cursor)
|
||||
}
|
||||
}
|
||||
text?.let {
|
||||
withContext(Dispatchers.Main) {
|
||||
shareText(context, it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun exportText(cursor: Cursor, separator: String): String {
|
||||
val sb = StringBuilder()
|
||||
val contentIndex = cursor.getColumnIndex(Database.SCANS_CONTENT)
|
||||
if (cursor.moveToFirst()) {
|
||||
do {
|
||||
val content = cursor.getString(contentIndex)
|
||||
if (content?.isNotEmpty() == true) {
|
||||
sb.append(content)
|
||||
sb.append(separator)
|
||||
}
|
||||
} while (cursor.moveToNext())
|
||||
}
|
||||
return sb.toString()
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
package de.markusfisch.android.binaryeye.fragment
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.content.SharedPreferences
|
||||
import android.content.SharedPreferences.OnSharedPreferenceChangeListener
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.support.v7.preference.ListPreference
|
||||
import android.support.v7.preference.Preference
|
||||
import android.support.v7.preference.PreferenceFragmentCompat
|
||||
import android.support.v7.preference.PreferenceGroup
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.activity.SplashActivity
|
||||
import de.markusfisch.android.binaryeye.app.prefs
|
||||
import de.markusfisch.android.binaryeye.app.systemBarRecyclerViewScrollListener
|
||||
import de.markusfisch.android.binaryeye.preference.UrlPreference
|
||||
import de.markusfisch.android.binaryeye.view.setPaddingFromWindowInsets
|
||||
|
||||
class PreferencesFragment : PreferenceFragmentCompat() {
|
||||
private val changeListener = object : OnSharedPreferenceChangeListener {
|
||||
override fun onSharedPreferenceChanged(
|
||||
sharedPreferences: SharedPreferences,
|
||||
key: String
|
||||
) {
|
||||
val preference = findPreference(key) ?: return
|
||||
prefs.update()
|
||||
if (preference.key == "custom_locale") {
|
||||
activity?.restartApp()
|
||||
} else {
|
||||
setSummary(preference)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreatePreferences(state: Bundle?, rootKey: String?) {
|
||||
addPreferencesFromResource(R.xml.preferences)
|
||||
activity?.setTitle(R.string.preferences)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
listView.setPaddingFromWindowInsets()
|
||||
listView.removeOnScrollListener(systemBarRecyclerViewScrollListener)
|
||||
listView.addOnScrollListener(systemBarRecyclerViewScrollListener)
|
||||
preferenceScreen.sharedPreferences
|
||||
.registerOnSharedPreferenceChangeListener(changeListener)
|
||||
setSummaries(preferenceScreen)
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
preferenceScreen.sharedPreferences
|
||||
.unregisterOnSharedPreferenceChangeListener(changeListener)
|
||||
}
|
||||
|
||||
override fun onDisplayPreferenceDialog(preference: Preference) {
|
||||
if (preference is UrlPreference) {
|
||||
val fm = fragmentManager
|
||||
UrlDialogFragment.newInstance(preference.key).apply {
|
||||
setTargetFragment(this@PreferencesFragment, 0)
|
||||
show(fm, null)
|
||||
}
|
||||
} else {
|
||||
super.onDisplayPreferenceDialog(preference)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setSummaries(screen: PreferenceGroup) {
|
||||
var i = screen.preferenceCount
|
||||
while (i-- > 0) {
|
||||
setSummary(screen.getPreference(i))
|
||||
}
|
||||
}
|
||||
|
||||
private fun setSummary(preference: Preference) {
|
||||
when (preference) {
|
||||
is UrlPreference -> {
|
||||
preference.setSummary(preference.getUrl())
|
||||
}
|
||||
is ListPreference -> {
|
||||
preference.setSummary(preference.entry)
|
||||
}
|
||||
is PreferenceGroup -> {
|
||||
setSummaries(preference)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun Activity.restartApp() {
|
||||
val intent = Intent(this, SplashActivity::class.java)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
|
||||
intent.addFlags(
|
||||
Intent.FLAG_ACTIVITY_NEW_TASK or
|
||||
Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||
)
|
||||
}
|
||||
startActivity(intent)
|
||||
finish()
|
||||
// restart to begin with an unmodified Locale to follow system settings
|
||||
Runtime.getRuntime().exit(0)
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
package de.markusfisch.android.binaryeye.fragment
|
||||
|
||||
import android.os.Bundle
|
||||
import android.support.v7.preference.PreferenceDialogFragmentCompat
|
||||
import android.view.View
|
||||
import android.widget.TextView
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.app.prefs
|
||||
import de.markusfisch.android.binaryeye.database.Scan
|
||||
import de.markusfisch.android.binaryeye.net.sendAsync
|
||||
import de.markusfisch.android.binaryeye.preference.UrlPreference
|
||||
|
||||
class UrlDialogFragment : PreferenceDialogFragmentCompat() {
|
||||
private var urlView: TextView? = null
|
||||
private var testButton: TextView? = null
|
||||
|
||||
override fun onBindDialogView(view: View?) {
|
||||
super.onBindDialogView(view)
|
||||
urlView = view?.findViewById(R.id.url)
|
||||
testButton = view?.findViewById(R.id.test_url)
|
||||
testButton?.setOnClickListener {
|
||||
testUrl(testButton)
|
||||
}
|
||||
urlView?.text = urlPreference().getUrl()
|
||||
urlView?.hint = getString(
|
||||
when (prefs.sendScanType) {
|
||||
"0" -> R.string.url_hint_add_content
|
||||
else -> R.string.url_hint
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
override fun onDialogClosed(positiveResult: Boolean) {
|
||||
if (positiveResult) {
|
||||
urlPreference().setUrl(getUrl())
|
||||
}
|
||||
}
|
||||
|
||||
private fun getUrl() = completeUrl(urlView?.text.toString())
|
||||
|
||||
private fun urlPreference() = preference as UrlPreference
|
||||
|
||||
private fun testUrl(textView: TextView?) {
|
||||
val url = getUrl()
|
||||
if (url.isEmpty()) {
|
||||
return
|
||||
}
|
||||
textView ?: return
|
||||
textView.text = "…"
|
||||
Scan(
|
||||
"test",
|
||||
null,
|
||||
"none",
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
).sendAsync(url, prefs.sendScanType) { code, body ->
|
||||
textView.text = when {
|
||||
code != null -> "$code"
|
||||
body != null -> body
|
||||
else -> getString(R.string.background_request_failed)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun newInstance(key: String): UrlDialogFragment {
|
||||
val args = Bundle()
|
||||
args.putString(ARG_KEY, key)
|
||||
val fragment = UrlDialogFragment()
|
||||
fragment.arguments = args
|
||||
return fragment
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun completeUrl(template: String): String {
|
||||
var s = template.trim()
|
||||
if (s.isEmpty()) {
|
||||
return ""
|
||||
}
|
||||
if (!s.startsWith("http")) {
|
||||
s = "http://${s}"
|
||||
}
|
||||
if (prefs.sendScanType == "0" &&
|
||||
!s.matches(".*/[a-zA-Z._-]*\\?[a-zA-Z0-9_-]+=$".toRegex())
|
||||
) {
|
||||
s = "${s}?content="
|
||||
}
|
||||
return s
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package de.markusfisch.android.binaryeye.graphics
|
||||
|
||||
import android.content.ContentResolver
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import android.graphics.Matrix
|
||||
import android.graphics.RectF
|
||||
import android.net.Uri
|
||||
import java.io.IOException
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
fun loadImageUri(cr: ContentResolver, uri: Uri): Bitmap? = try {
|
||||
val options = BitmapFactory.Options()
|
||||
cr.openInputStream(uri)?.use {
|
||||
options.inJustDecodeBounds = true
|
||||
BitmapFactory.decodeStream(it, null, options)
|
||||
options.inSampleSize = calculateInSampleSize(
|
||||
options.outWidth,
|
||||
options.outHeight
|
||||
)
|
||||
options.inJustDecodeBounds = false
|
||||
}
|
||||
cr.openInputStream(uri)?.use {
|
||||
BitmapFactory.decodeStream(it, null, options)
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
null
|
||||
}
|
||||
|
||||
private fun calculateInSampleSize(
|
||||
width: Int,
|
||||
height: Int,
|
||||
reqWidth: Int = 1024,
|
||||
reqHeight: Int = 1024
|
||||
): Int {
|
||||
var inSampleSize = 1
|
||||
if (height > reqHeight || width > reqWidth) {
|
||||
val halfHeight = height / 2
|
||||
val halfWidth = width / 2
|
||||
while (
|
||||
halfHeight / inSampleSize >= reqHeight ||
|
||||
halfWidth / inSampleSize >= reqWidth
|
||||
) {
|
||||
inSampleSize *= 2
|
||||
}
|
||||
}
|
||||
return inSampleSize
|
||||
}
|
||||
|
||||
fun crop(bitmap: Bitmap, rect: RectF, rotation: Float) = try {
|
||||
val erected = erect(bitmap, rotation)
|
||||
val w = erected.width
|
||||
val h = erected.height
|
||||
val x = max(0, (rect.left * w).roundToInt())
|
||||
val y = max(0, (rect.top * h).roundToInt())
|
||||
Bitmap.createBitmap(
|
||||
erected,
|
||||
x,
|
||||
y,
|
||||
min(w - x, (rect.width() * w).roundToInt()),
|
||||
min(h - y, (rect.height() * h).roundToInt())
|
||||
)
|
||||
} catch (e: OutOfMemoryError) {
|
||||
null
|
||||
} catch (e: IllegalArgumentException) {
|
||||
null
|
||||
}
|
||||
|
||||
private fun erect(bitmap: Bitmap, rotation: Float): Bitmap = if (
|
||||
rotation % 360f != 0f
|
||||
) {
|
||||
val matrix = Matrix()
|
||||
matrix.setRotate(rotation)
|
||||
Bitmap.createBitmap(
|
||||
bitmap,
|
||||
0,
|
||||
0,
|
||||
bitmap.width,
|
||||
bitmap.height,
|
||||
matrix,
|
||||
true
|
||||
)
|
||||
} else {
|
||||
bitmap
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package de.markusfisch.android.binaryeye.graphics
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.DashPathEffect
|
||||
import android.graphics.Paint
|
||||
import android.support.v4.content.ContextCompat
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
|
||||
fun Context.getDashedBorderPaint() = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
val dp = resources.displayMetrics.density
|
||||
color = ContextCompat.getColor(applicationContext, R.color.crop_bound)
|
||||
style = Paint.Style.STROKE
|
||||
strokeWidth = dp * 2f
|
||||
pathEffect = DashPathEffect(floatArrayOf(10f * dp, 10f * dp), 0f)
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package de.markusfisch.android.binaryeye.graphics
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Paint
|
||||
import android.graphics.Point
|
||||
import android.support.v4.content.ContextCompat
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
|
||||
class Dots(context: Context) {
|
||||
private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
|
||||
private val radius = 8f * context.resources.displayMetrics.density
|
||||
|
||||
init {
|
||||
paint.color = ContextCompat.getColor(context, R.color.dot)
|
||||
paint.style = Paint.Style.FILL
|
||||
}
|
||||
|
||||
fun draw(canvas: Canvas, points: List<Point>) {
|
||||
for (point in points) {
|
||||
canvas.drawCircle(
|
||||
point.x.toFloat(),
|
||||
point.y.toFloat(),
|
||||
radius,
|
||||
paint
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package de.markusfisch.android.binaryeye.graphics
|
||||
|
||||
import android.content.res.Resources
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.drawable.BitmapDrawable
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.os.Build
|
||||
|
||||
fun Resources.getBitmapFromDrawable(
|
||||
resId: Int
|
||||
): Bitmap = getBitmapFromDrawable(getDrawableCompat(resId))
|
||||
|
||||
fun Resources.getDrawableCompat(
|
||||
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 {
|
||||
if (drawable is BitmapDrawable) {
|
||||
return drawable.bitmap
|
||||
}
|
||||
val bitmap = Bitmap.createBitmap(
|
||||
drawable.intrinsicWidth,
|
||||
drawable.intrinsicHeight,
|
||||
Bitmap.Config.ARGB_8888
|
||||
)
|
||||
val canvas = Canvas(bitmap)
|
||||
drawable.setBounds(0, 0, canvas.width, canvas.height)
|
||||
drawable.draw(canvas)
|
||||
return bitmap
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package de.markusfisch.android.binaryeye.graphics
|
||||
|
||||
import android.graphics.Point
|
||||
import android.graphics.Rect
|
||||
import com.google.zxing.Result
|
||||
import com.google.zxing.ResultPoint
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
fun mapResult(
|
||||
frameWidth: Int,
|
||||
frameHeight: Int,
|
||||
frameOrientation: Int,
|
||||
viewRect: Rect,
|
||||
result: Result
|
||||
): List<Point> = frameToView(
|
||||
frameWidth,
|
||||
frameHeight,
|
||||
frameOrientation,
|
||||
viewRect
|
||||
).map(
|
||||
result.resultPoints
|
||||
)
|
||||
|
||||
fun frameToView(
|
||||
frameWidth: Int,
|
||||
frameHeight: Int,
|
||||
frameOrientation: Int,
|
||||
viewRect: Rect
|
||||
) = Mapping(
|
||||
frameWidth,
|
||||
frameHeight,
|
||||
frameOrientation,
|
||||
viewRect.width().toFloat(),
|
||||
viewRect.height().toFloat(),
|
||||
viewRect.left,
|
||||
viewRect.top
|
||||
)
|
||||
|
||||
fun isPortrait(orientation: Int) = orientation == 90 || orientation == 270
|
||||
|
||||
data class Mapping(
|
||||
val frameWidth: Int,
|
||||
val frameHeight: Int,
|
||||
val frameOrientation: Int,
|
||||
val viewWidth: Float,
|
||||
val viewHeight: Float,
|
||||
val offsetX: Int,
|
||||
val offsetY: Int
|
||||
) {
|
||||
fun map(resultPoint: ResultPoint): Point {
|
||||
val point = Point(
|
||||
resultPoint.x.roundToInt(),
|
||||
resultPoint.y.roundToInt()
|
||||
)
|
||||
rotate(point)
|
||||
val w: Int
|
||||
val h: Int
|
||||
if (isPortrait(frameOrientation)) {
|
||||
w = frameHeight
|
||||
h = frameWidth
|
||||
} else {
|
||||
w = frameWidth
|
||||
h = frameHeight
|
||||
}
|
||||
val ratioX = viewWidth / w.toFloat()
|
||||
val ratioY = viewHeight / h.toFloat()
|
||||
point.set(
|
||||
(point.x * ratioX).roundToInt() + offsetX,
|
||||
(point.y * ratioY).roundToInt() + offsetY
|
||||
)
|
||||
return point
|
||||
}
|
||||
|
||||
fun map(points: Array<ResultPoint?>): List<Point> =
|
||||
// because ZXing apparently returns null in this array sometimes
|
||||
points.filterNotNull().map { map(it) }
|
||||
|
||||
private fun rotate(point: Point) = when (frameOrientation) {
|
||||
90 -> point.set(frameHeight - point.y, point.x)
|
||||
180 -> point.set(frameWidth - point.x, frameHeight - point.y)
|
||||
270 -> point.set(point.y, frameWidth - point.x)
|
||||
else -> {
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
package de.markusfisch.android.binaryeye.net
|
||||
|
||||
import de.markusfisch.android.binaryeye.app.toHexString
|
||||
import de.markusfisch.android.binaryeye.database.Scan
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.json.JSONObject
|
||||
import java.io.BufferedReader
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.io.InputStreamReader
|
||||
import java.net.HttpURLConnection
|
||||
import java.net.ProtocolException
|
||||
import java.net.URL
|
||||
import java.net.URLEncoder
|
||||
|
||||
fun Scan.sendAsync(url: String, type: String, callback: (Int?, String?) -> Unit) {
|
||||
CoroutineScope(Dispatchers.IO).launch(Dispatchers.IO) {
|
||||
val response = send(url, type)
|
||||
withContext(Dispatchers.Main) {
|
||||
callback(response.code, response.body)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun Scan.send(url: String, type: String): Response {
|
||||
return when (type) {
|
||||
"1" -> request(
|
||||
url + "?" + asUrlArguments()
|
||||
)
|
||||
"2" -> request(url) { con ->
|
||||
con.requestMethod = "POST"
|
||||
con.setRequestProperty("Content-Type", "application/x-www-form-urlencoded")
|
||||
con.outputStream.apply {
|
||||
write(asUrlArguments().toByteArray())
|
||||
close()
|
||||
}
|
||||
}
|
||||
"3" -> request(url) { con ->
|
||||
con.requestMethod = "POST"
|
||||
con.setRequestProperty("Content-Type", "application/json")
|
||||
con.outputStream.apply {
|
||||
write(asJson().toString().toByteArray())
|
||||
close()
|
||||
}
|
||||
}
|
||||
else -> request(
|
||||
url + urlEncode(content)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun Scan.asUrlArguments(): String = getMap().map { (k, v) ->
|
||||
"${k}=${urlEncode(v)}"
|
||||
}.joinToString("&")
|
||||
|
||||
private fun Scan.asJson() = JSONObject().apply {
|
||||
getMap().forEach { (k, v) -> put(k, v) }
|
||||
}
|
||||
|
||||
private fun urlEncode(s: String) = URLEncoder.encode(s, "utf-8")
|
||||
|
||||
private fun Scan.getMap(): Map<String, String> = mapOf(
|
||||
"content" to content,
|
||||
"raw" to raw?.toHexString(),
|
||||
"format" to format,
|
||||
"errorCorrectionLevel" to errorCorrectionLevel,
|
||||
"issueNumber" to issueNumber,
|
||||
"orientation" to orientation,
|
||||
"otherMetaData" to otherMetaData,
|
||||
"pdf417ExtraMetaData" to pdf417ExtraMetaData,
|
||||
"possibleCountry" to possibleCountry,
|
||||
"suggestedPrice" to suggestedPrice,
|
||||
"upcEanExtension" to upcEanExtension,
|
||||
"timestamp" to timestamp
|
||||
).filterNullValues()
|
||||
|
||||
private fun Map<String, String?>.filterNullValues() =
|
||||
filterValues { it != null }.mapValues { it.value as String }
|
||||
|
||||
private fun request(
|
||||
url: String,
|
||||
writer: ((HttpURLConnection) -> Any)? = null
|
||||
): Response {
|
||||
var con: HttpURLConnection? = null
|
||||
return try {
|
||||
con = URL(url).openConnection() as HttpURLConnection
|
||||
writer?.invoke(con)
|
||||
val body = con.inputStream.readHead()
|
||||
Response(con.responseCode, body)
|
||||
} catch (e: ProtocolException) {
|
||||
Response(null, e.message)
|
||||
} catch (e: IOException) {
|
||||
try {
|
||||
val body = con?.errorStream?.readHead() ?: e.message
|
||||
Response(con?.responseCode, body)
|
||||
} catch (e: IOException) {
|
||||
Response(null, e.message)
|
||||
}
|
||||
} finally {
|
||||
con?.disconnect()
|
||||
}
|
||||
}
|
||||
|
||||
private fun InputStream.readHead(): String {
|
||||
val sb = StringBuilder()
|
||||
val br = BufferedReader(InputStreamReader(this))
|
||||
var line = br.readLine()
|
||||
var i = 0
|
||||
while (line != null && sb.length < 240) {
|
||||
sb.append(line)
|
||||
line = br.readLine()
|
||||
++i
|
||||
}
|
||||
br.close()
|
||||
return sb.toString()
|
||||
}
|
||||
|
||||
private data class Response(val code: Int?, val body: String?)
|
||||
@@ -0,0 +1,219 @@
|
||||
package de.markusfisch.android.binaryeye.preference
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import android.preference.PreferenceManager
|
||||
|
||||
class Preferences {
|
||||
lateinit var preferences: SharedPreferences
|
||||
|
||||
var cropHandleX = -1
|
||||
set(value) {
|
||||
apply(CROP_HANDLE_X, value)
|
||||
field = value
|
||||
}
|
||||
var cropHandleY = -1
|
||||
set(value) {
|
||||
apply(CROP_HANDLE_Y, value)
|
||||
field = value
|
||||
}
|
||||
var cropHandleOrientation = 0
|
||||
set(value) {
|
||||
apply(CROP_HANDLE_ORIENTATION, value)
|
||||
field = value
|
||||
}
|
||||
var showCropHandle = true
|
||||
set(value) {
|
||||
apply(SHOW_CROP_HANDLE, value)
|
||||
field = value
|
||||
}
|
||||
var zoomBySwiping = true
|
||||
set(value) {
|
||||
apply(ZOOM_BY_SWIPING, value)
|
||||
field = value
|
||||
}
|
||||
var autoRotate = false
|
||||
set(value) {
|
||||
// immediately save this setting before it shouldn't change
|
||||
// on the fly while scanning
|
||||
commit(AUTO_ROTATE, value)
|
||||
field = value
|
||||
}
|
||||
var tryHarder = false
|
||||
set(value) {
|
||||
// immediately save this setting because it's only ever read
|
||||
// before the camera is opened
|
||||
commit(TRY_HARDER, value)
|
||||
field = value
|
||||
}
|
||||
var vibrate = true
|
||||
set(value) {
|
||||
apply(VIBRATE, value)
|
||||
field = value
|
||||
}
|
||||
var useHistory = false
|
||||
set(value) {
|
||||
apply(USE_HISTORY, value)
|
||||
field = value
|
||||
}
|
||||
var ignoreConsecutiveDuplicates = true
|
||||
set(value) {
|
||||
apply(IGNORE_CONSECUTIVE_DUPLICATES, value)
|
||||
field = value
|
||||
}
|
||||
var copyImmediately = false
|
||||
set(value) {
|
||||
apply(COPY_IMMEDIATELY, value)
|
||||
field = value
|
||||
}
|
||||
var openImmediately = false
|
||||
set(value) {
|
||||
apply(OPEN_IMMEDIATELY, value)
|
||||
field = value
|
||||
}
|
||||
var showMetaData = true
|
||||
set(value) {
|
||||
apply(SHOW_META_DATA, value)
|
||||
field = value
|
||||
}
|
||||
var showHexDump = true
|
||||
set(value) {
|
||||
apply(SHOW_HEX_DUMP, value)
|
||||
field = value
|
||||
}
|
||||
var closeAutomatically = false
|
||||
set(value) {
|
||||
apply(CLOSE_AUTOMATICALLY, value)
|
||||
field = value
|
||||
}
|
||||
var openWithUrl: String = ""
|
||||
set(value) {
|
||||
apply(OPEN_WITH_URL, value)
|
||||
field = value
|
||||
}
|
||||
var sendScanUrl: String = ""
|
||||
set(value) {
|
||||
apply(SEND_SCAN_URL, value)
|
||||
field = value
|
||||
}
|
||||
var sendScanType: String = "0"
|
||||
set(value) {
|
||||
apply(SEND_SCAN_TYPE, value)
|
||||
field = value
|
||||
}
|
||||
var customLocale: String = ""
|
||||
set(value) {
|
||||
apply(CUSTOM_LOCALE, value)
|
||||
field = value
|
||||
}
|
||||
var indexOfLastSelectedFormat: Int = 0
|
||||
set(value) {
|
||||
apply(INDEX_OF_LAST_SELECTED_FORMAT, value)
|
||||
field = value
|
||||
}
|
||||
var forceCompat: Boolean = false
|
||||
set(value) {
|
||||
// since the app may be about to crash when forceCompat is set,
|
||||
// it's necessary to `commit()` this synchronously
|
||||
commit(FORCE_COMPAT, value)
|
||||
field = value
|
||||
}
|
||||
|
||||
fun init(context: Context) {
|
||||
preferences = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
update()
|
||||
}
|
||||
|
||||
fun update() {
|
||||
cropHandleX = preferences.getInt(CROP_HANDLE_X, cropHandleX)
|
||||
cropHandleY = preferences.getInt(CROP_HANDLE_Y, cropHandleY)
|
||||
cropHandleOrientation = preferences.getInt(
|
||||
CROP_HANDLE_ORIENTATION,
|
||||
cropHandleOrientation
|
||||
)
|
||||
showCropHandle = preferences.getBoolean(SHOW_CROP_HANDLE, showCropHandle)
|
||||
zoomBySwiping = preferences.getBoolean(ZOOM_BY_SWIPING, zoomBySwiping)
|
||||
autoRotate = preferences.getBoolean(AUTO_ROTATE, autoRotate)
|
||||
tryHarder = preferences.getBoolean(TRY_HARDER, tryHarder)
|
||||
vibrate = preferences.getBoolean(VIBRATE, vibrate)
|
||||
useHistory = preferences.getBoolean(USE_HISTORY, useHistory)
|
||||
ignoreConsecutiveDuplicates = preferences.getBoolean(
|
||||
IGNORE_CONSECUTIVE_DUPLICATES,
|
||||
ignoreConsecutiveDuplicates
|
||||
)
|
||||
copyImmediately = preferences.getBoolean(
|
||||
COPY_IMMEDIATELY,
|
||||
copyImmediately
|
||||
)
|
||||
openImmediately = preferences.getBoolean(
|
||||
OPEN_IMMEDIATELY,
|
||||
openImmediately
|
||||
)
|
||||
showMetaData = preferences.getBoolean(SHOW_META_DATA, showMetaData)
|
||||
showHexDump = preferences.getBoolean(SHOW_HEX_DUMP, showHexDump)
|
||||
closeAutomatically = preferences.getBoolean(
|
||||
CLOSE_AUTOMATICALLY,
|
||||
closeAutomatically
|
||||
)
|
||||
preferences.getString(OPEN_WITH_URL, openWithUrl)?.also {
|
||||
openWithUrl = it
|
||||
}
|
||||
preferences.getString(SEND_SCAN_URL, sendScanUrl)?.also {
|
||||
sendScanUrl = it
|
||||
}
|
||||
preferences.getString(SEND_SCAN_TYPE, sendScanType)?.also {
|
||||
sendScanType = it
|
||||
}
|
||||
preferences.getString(CUSTOM_LOCALE, customLocale)?.also {
|
||||
customLocale = it
|
||||
}
|
||||
indexOfLastSelectedFormat = preferences.getInt(
|
||||
INDEX_OF_LAST_SELECTED_FORMAT,
|
||||
indexOfLastSelectedFormat
|
||||
)
|
||||
forceCompat = preferences.getBoolean(FORCE_COMPAT, forceCompat)
|
||||
}
|
||||
|
||||
private fun put(label: String, value: Boolean) =
|
||||
preferences.edit().putBoolean(label, value)
|
||||
|
||||
private fun commit(label: String, value: Boolean) {
|
||||
put(label, value).commit()
|
||||
}
|
||||
|
||||
private fun apply(label: String, value: Boolean) {
|
||||
put(label, value).apply()
|
||||
}
|
||||
|
||||
private fun apply(label: String, value: String) {
|
||||
preferences.edit().putString(label, value).apply()
|
||||
}
|
||||
|
||||
private fun apply(label: String, value: Int) {
|
||||
preferences.edit().putInt(label, value).apply()
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val CROP_HANDLE_X = "crop_handle_x"
|
||||
const val CROP_HANDLE_Y = "crop_handle_y"
|
||||
const val CROP_HANDLE_ORIENTATION = "crop_handle_orientation"
|
||||
const val SHOW_CROP_HANDLE = "show_crop_handle"
|
||||
const val ZOOM_BY_SWIPING = "zoom_by_swiping"
|
||||
const val AUTO_ROTATE = "auto_rotate"
|
||||
const val TRY_HARDER = "try_harder"
|
||||
const val VIBRATE = "vibrate"
|
||||
const val USE_HISTORY = "use_history"
|
||||
const val IGNORE_CONSECUTIVE_DUPLICATES = "ignore_consecutive_duplicates"
|
||||
const val OPEN_IMMEDIATELY = "open_immediately"
|
||||
const val COPY_IMMEDIATELY = "copy_immediately"
|
||||
const val SHOW_META_DATA = "show_meta_data"
|
||||
const val SHOW_HEX_DUMP = "show_hex_dump"
|
||||
const val CLOSE_AUTOMATICALLY = "close_automatically"
|
||||
const val OPEN_WITH_URL = "open_with_url"
|
||||
const val SEND_SCAN_URL = "send_scan_url"
|
||||
const val SEND_SCAN_TYPE = "send_scan_type"
|
||||
const val CUSTOM_LOCALE = "custom_locale"
|
||||
const val INDEX_OF_LAST_SELECTED_FORMAT = "index_of_last_selected_format"
|
||||
const val FORCE_COMPAT = "force_compat"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package de.markusfisch.android.binaryeye.preference
|
||||
|
||||
import android.content.Context
|
||||
import android.support.v7.preference.DialogPreference
|
||||
import android.util.AttributeSet
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
|
||||
class UrlPreference(
|
||||
context: Context?,
|
||||
attrs: AttributeSet?
|
||||
) : DialogPreference(context, attrs) {
|
||||
private var url: String? = null
|
||||
|
||||
init {
|
||||
dialogLayoutResource = R.layout.dialog_url
|
||||
}
|
||||
|
||||
fun getUrl() = url
|
||||
|
||||
fun setUrl(url: String) {
|
||||
this.url = url
|
||||
persistString(url)
|
||||
}
|
||||
|
||||
override fun onSetInitialValue(
|
||||
restorePersistedValue: Boolean,
|
||||
defaultValue: Any?
|
||||
) {
|
||||
setUrl(
|
||||
if (restorePersistedValue) {
|
||||
getPersistedString(url)
|
||||
} else {
|
||||
defaultValue as String
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package de.markusfisch.android.binaryeye.rs
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Canvas
|
||||
import android.support.v8.renderscript.Allocation
|
||||
import android.support.v8.renderscript.RenderScript
|
||||
|
||||
fun fixTransparency(rs: RenderScript, bitmap: Bitmap?): Bitmap? {
|
||||
bitmap ?: return null
|
||||
val result = analyze(rs, bitmap)
|
||||
return if (result.hasTransparentPixels) {
|
||||
val copy = Bitmap.createBitmap(
|
||||
bitmap.width,
|
||||
bitmap.height,
|
||||
bitmap.config
|
||||
)
|
||||
Canvas(copy).apply {
|
||||
drawColor(result.backgroundColor)
|
||||
drawBitmap(bitmap, 0f, 0f, null)
|
||||
}
|
||||
copy
|
||||
} else {
|
||||
bitmap
|
||||
}
|
||||
}
|
||||
|
||||
private fun analyze(rs: RenderScript, bitmap: Bitmap): Result {
|
||||
val analyzeScript = ScriptC_analyze(rs)
|
||||
val bitmapAllocation = Allocation.createFromBitmap(rs, bitmap)
|
||||
val result = analyzeScript.reduce_analyze(bitmapAllocation).get()
|
||||
bitmapAllocation.destroy()
|
||||
analyzeScript.destroy()
|
||||
return Result(
|
||||
result.x > 0,
|
||||
if (result.y > 0) 0xff000000.toInt() else 0xffffffff.toInt()
|
||||
)
|
||||
}
|
||||
|
||||
private data class Result(
|
||||
val hasTransparentPixels: Boolean,
|
||||
val backgroundColor: Int
|
||||
)
|
||||
@@ -1,29 +1,28 @@
|
||||
package de.markusfisch.android.binaryeye.rs
|
||||
|
||||
import de.markusfisch.android.binaryeye.renderscript.ScriptC_rotator
|
||||
import android.graphics.Rect
|
||||
import android.support.v8.renderscript.*
|
||||
import kotlin.math.max
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
import android.content.Context
|
||||
import android.support.v8.renderscript.Allocation
|
||||
import android.support.v8.renderscript.Element
|
||||
import android.support.v8.renderscript.RenderScript
|
||||
import android.support.v8.renderscript.ScriptIntrinsicResize
|
||||
import android.support.v8.renderscript.Type
|
||||
private const val SCALE_FACTOR = .75f
|
||||
|
||||
class Preprocessor(
|
||||
context: Context,
|
||||
rs: RenderScript,
|
||||
width: Int,
|
||||
height: Int,
|
||||
val orientation: Int
|
||||
private val roi: Rect?
|
||||
) {
|
||||
var outWidth = 0
|
||||
var outHeight = 0
|
||||
|
||||
private val rs = RenderScript.create(context)
|
||||
private val resizeScript = ScriptIntrinsicResize.create(rs)
|
||||
private val rotatorScript = ScriptC_rotator(rs)
|
||||
private val rotateScript = ScriptC_rotate(rs)
|
||||
|
||||
private var yuvType: Type? = null
|
||||
private var yuvAlloc: Allocation? = null
|
||||
private var roiType: Type? = null
|
||||
private var roiAlloc: Allocation? = null
|
||||
private var resizedType: Type? = null
|
||||
private var resizedAlloc: Allocation? = null
|
||||
private var rotatedType: Type? = null
|
||||
@@ -42,15 +41,42 @@ class Preprocessor(
|
||||
Allocation.USAGE_SCRIPT
|
||||
)
|
||||
|
||||
var f = .75f
|
||||
var w = Math.round(width * f)
|
||||
var h = Math.round(height * f)
|
||||
if (roi != null) {
|
||||
val roiWidth = roi.width()
|
||||
val roiHeight = roi.height()
|
||||
|
||||
roiType = Type.createXY(
|
||||
rs,
|
||||
Element.U8(rs),
|
||||
roiWidth,
|
||||
roiHeight
|
||||
)
|
||||
roiAlloc = Allocation.createTyped(
|
||||
rs,
|
||||
roiType,
|
||||
Allocation.USAGE_SCRIPT
|
||||
)
|
||||
|
||||
outWidth = (roiWidth * SCALE_FACTOR).roundToInt()
|
||||
outHeight = (roiHeight * SCALE_FACTOR).roundToInt()
|
||||
|
||||
// make sure the dimensions are always a multiple of 4
|
||||
outWidth -= outWidth % 4
|
||||
outHeight -= outHeight % 4
|
||||
|
||||
// make sure the dimensions are always greater than 4
|
||||
outWidth = max(4, outWidth)
|
||||
outHeight = max(4, outHeight)
|
||||
} else {
|
||||
outWidth = (width * SCALE_FACTOR).roundToInt()
|
||||
outHeight = (height * SCALE_FACTOR).roundToInt()
|
||||
}
|
||||
|
||||
resizedType = Type.createXY(
|
||||
rs,
|
||||
Element.U8(rs),
|
||||
w,
|
||||
h
|
||||
outWidth,
|
||||
outHeight
|
||||
)
|
||||
resizedAlloc = Allocation.createTyped(
|
||||
rs,
|
||||
@@ -58,28 +84,17 @@ class Preprocessor(
|
||||
Allocation.USAGE_SCRIPT
|
||||
)
|
||||
|
||||
if (orientation == 90 || orientation == 270) {
|
||||
val tmp = w
|
||||
w = h
|
||||
h = tmp
|
||||
}
|
||||
|
||||
outWidth = w
|
||||
outHeight = h
|
||||
|
||||
if (orientation != 0) {
|
||||
rotatedType = Type.createXY(
|
||||
rs,
|
||||
Element.U8(rs),
|
||||
w,
|
||||
h
|
||||
)
|
||||
rotatedAlloc = Allocation.createTyped(
|
||||
rs,
|
||||
rotatedType,
|
||||
Allocation.USAGE_SCRIPT
|
||||
)
|
||||
}
|
||||
rotatedType = Type.createXY(
|
||||
rs,
|
||||
Element.U8(rs),
|
||||
outHeight,
|
||||
outWidth
|
||||
)
|
||||
rotatedAlloc = Allocation.createTyped(
|
||||
rs,
|
||||
rotatedType,
|
||||
Allocation.USAGE_SCRIPT
|
||||
)
|
||||
}
|
||||
|
||||
fun destroy() {
|
||||
@@ -87,6 +102,10 @@ class Preprocessor(
|
||||
yuvType = null
|
||||
yuvAlloc?.destroy()
|
||||
yuvAlloc = null
|
||||
roiType?.destroy()
|
||||
roiType = null
|
||||
roiAlloc?.destroy()
|
||||
roiAlloc = null
|
||||
resizedType?.destroy()
|
||||
resizedType = null
|
||||
resizedAlloc?.destroy()
|
||||
@@ -96,38 +115,42 @@ class Preprocessor(
|
||||
rotatedAlloc?.destroy()
|
||||
rotatedAlloc = null
|
||||
resizeScript.destroy()
|
||||
rotatorScript.destroy()
|
||||
rs.destroy()
|
||||
rotateScript.destroy()
|
||||
}
|
||||
|
||||
fun process(frame: ByteArray) {
|
||||
fun resizeOnly(frame: ByteArray) {
|
||||
resize(frame)
|
||||
resizedAlloc?.copyTo(frame)
|
||||
}
|
||||
|
||||
fun resizeAndRotate(frame: ByteArray) {
|
||||
resize(frame)
|
||||
val t = resizedType ?: return
|
||||
rotateScript._inImage = resizedAlloc
|
||||
rotateScript._inWidth = t.x
|
||||
rotateScript._inHeight = t.y
|
||||
rotateScript.forEach_rotate90(
|
||||
rotatedAlloc, // ignored in kernel, just to satisfy forEach
|
||||
rotatedAlloc
|
||||
)
|
||||
rotatedAlloc?.copyTo(frame)
|
||||
}
|
||||
|
||||
private fun resize(frame: ByteArray) {
|
||||
yuvAlloc?.copyFrom(frame)
|
||||
|
||||
resizeScript.setInput(yuvAlloc)
|
||||
resizeScript.forEach_bicubic(resizedAlloc)
|
||||
|
||||
val t = resizedType
|
||||
if (t == null || orientation == 0) {
|
||||
resizedAlloc?.copyTo(frame)
|
||||
} else {
|
||||
rotatorScript._inImage = resizedAlloc
|
||||
rotatorScript._inWidth = t.x
|
||||
rotatorScript._inHeight = t.y
|
||||
when (orientation) {
|
||||
90 -> rotatorScript.forEach_rotate90(
|
||||
rotatedAlloc,
|
||||
rotatedAlloc
|
||||
)
|
||||
180 -> rotatorScript.forEach_rotate180(
|
||||
rotatedAlloc,
|
||||
rotatedAlloc
|
||||
)
|
||||
270 -> rotatorScript.forEach_rotate270(
|
||||
rotatedAlloc,
|
||||
rotatedAlloc
|
||||
resizeScript.setInput(
|
||||
if (roi != null) {
|
||||
roiAlloc?.copy2DRangeFrom(
|
||||
0, 0,
|
||||
roi.width(), roi.height(),
|
||||
yuvAlloc,
|
||||
max(0, roi.left), max(0, roi.top)
|
||||
)
|
||||
roiAlloc
|
||||
} else {
|
||||
yuvAlloc
|
||||
}
|
||||
rotatedAlloc?.copyTo(frame)
|
||||
}
|
||||
)
|
||||
resizeScript.forEach_bicubic(resizedAlloc)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
package de.markusfisch.android.binaryeye.view
|
||||
|
||||
import android.graphics.Rect
|
||||
import android.view.View
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
suspend inline fun View.useVisibility(
|
||||
whileExecuting: Int = View.VISIBLE,
|
||||
otherwise: Int = View.GONE,
|
||||
crossinline block: suspend () -> Unit
|
||||
) {
|
||||
if (visibility == whileExecuting) {
|
||||
return
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
visibility = whileExecuting
|
||||
}
|
||||
try {
|
||||
block()
|
||||
} finally {
|
||||
withContext(Dispatchers.Main) {
|
||||
visibility = otherwise
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun View.setPadding(rect: Rect) {
|
||||
this.setPadding(
|
||||
rect.left,
|
||||
rect.top,
|
||||
rect.right,
|
||||
rect.bottom
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package de.markusfisch.android.binaryeye.view
|
||||
|
||||
import android.graphics.Rect
|
||||
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
|
||||
|
||||
private var toolbarHeight = 0
|
||||
fun recordToolbarHeight(toolbar: Toolbar) {
|
||||
toolbarHeight = toolbar.layoutParams.height
|
||||
}
|
||||
|
||||
fun View.setPaddingFromWindowInsets() {
|
||||
doOnApplyWindowInsets { v, insets -> v.setPadding(insets) }
|
||||
}
|
||||
|
||||
// A slight variation of the idea from Google's Chris Banes to avoid
|
||||
// adding the toolbar height for every view that needs to be inset.
|
||||
// For the original post see here:
|
||||
// https://medium.com/androiddevelopers/windowinsets-listeners-to-layouts-8f9ccc8fa4d1
|
||||
fun View.doOnApplyWindowInsets(f: (View, Rect) -> Unit) {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
|
||||
f(this, insetsWithToolbar())
|
||||
} else {
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
private fun insetsWithToolbar(insets: WindowInsetsCompat? = null) = Rect(
|
||||
insets?.systemWindowInsetLeft ?: 0,
|
||||
(insets?.systemWindowInsetTop ?: 0) + toolbarHeight,
|
||||
insets?.systemWindowInsetRight ?: 0,
|
||||
insets?.systemWindowInsetBottom ?: 0
|
||||
)
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.KITKAT)
|
||||
private fun View.requestApplyInsetsWhenAttached() {
|
||||
if (isAttachedToWindow) {
|
||||
ViewCompat.requestApplyInsets(this)
|
||||
} else {
|
||||
addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener {
|
||||
override fun onViewAttachedToWindow(v: View) {
|
||||
v.removeOnAttachStateChangeListener(this)
|
||||
ViewCompat.requestApplyInsets(v)
|
||||
}
|
||||
|
||||
override fun onViewDetachedFromWindow(v: View) = Unit
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package de.markusfisch.android.binaryeye.widget
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Rect
|
||||
import android.util.AttributeSet
|
||||
import de.markusfisch.android.binaryeye.app.colorSystemAndToolBars
|
||||
import de.markusfisch.android.scalingimageview.widget.ScalingImageView
|
||||
|
||||
open class ConfinedScalingImageView : ScalingImageView {
|
||||
val insets = Rect()
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet, defStyle: Int) :
|
||||
super(context, attrs, defStyle)
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet) :
|
||||
this(context, attrs, 0)
|
||||
|
||||
override fun onDraw(canvas: Canvas) {
|
||||
super.onDraw(canvas)
|
||||
colorSystemAndToolBars(context, !bounds.contains(mappedRect))
|
||||
}
|
||||
|
||||
override fun layoutImage(
|
||||
changed: Boolean,
|
||||
left: Int,
|
||||
top: Int,
|
||||
right: Int,
|
||||
bottom: Int
|
||||
) {
|
||||
if (changed) {
|
||||
setBounds(
|
||||
(left + insets.left).toFloat(),
|
||||
(top + insets.top).toFloat(),
|
||||
(right - insets.right).toFloat(),
|
||||
(bottom - insets.bottom).toFloat()
|
||||
)
|
||||
}
|
||||
centerRemap()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package de.markusfisch.android.binaryeye.widget
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.widget.ScrollView
|
||||
import de.markusfisch.android.binaryeye.app.colorSystemAndToolBars
|
||||
|
||||
class ConfinedScrollView : ScrollView {
|
||||
private var scrollable = false
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet, defStyle: Int) :
|
||||
super(context, attrs, defStyle)
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet) :
|
||||
this(context, attrs, 0)
|
||||
|
||||
override fun onLayout(
|
||||
changed: Boolean,
|
||||
left: Int,
|
||||
top: Int,
|
||||
right: Int,
|
||||
bottom: Int
|
||||
) {
|
||||
super.onLayout(changed, left, top, right, bottom)
|
||||
if (changed) {
|
||||
// give Android some time to settle down before running this,
|
||||
// not putting it on the queue makes it only work sometimes
|
||||
post {
|
||||
getChildAt(0)?.also { child ->
|
||||
scrollable = height < child.height + paddingTop + paddingBottom
|
||||
colorSystemAndToolBars(
|
||||
context,
|
||||
scrollY > 0,
|
||||
scrollable
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onScrollChanged(x: Int, y: Int, oldx: Int, oldy: Int) {
|
||||
super.onScrollChanged(x, y, oldx, oldy)
|
||||
colorSystemAndToolBars(context, y > 0, scrollable)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package de.markusfisch.android.binaryeye.widget
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Rect
|
||||
import android.graphics.RectF
|
||||
import android.util.AttributeSet
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
class CropImageView(context: Context, attr: AttributeSet) :
|
||||
ConfinedScalingImageView(context, attr) {
|
||||
var onScan: (() -> Unit)? = null
|
||||
|
||||
private val lastMappedRect = RectF()
|
||||
private val onScanRunnable = Runnable { onScan?.invoke() }
|
||||
|
||||
fun getBoundsRect() = Rect(
|
||||
bounds.left.roundToInt(),
|
||||
bounds.top.roundToInt(),
|
||||
bounds.right.roundToInt(),
|
||||
bounds.bottom.roundToInt()
|
||||
)
|
||||
|
||||
override fun onDraw(canvas: Canvas) {
|
||||
super.onDraw(canvas)
|
||||
val mr = mappedRect ?: return
|
||||
if (mr != lastMappedRect) {
|
||||
removeCallbacks(onScanRunnable)
|
||||
postDelayed(onScanRunnable, 300)
|
||||
lastMappedRect.set(mr)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,381 @@
|
||||
package de.markusfisch.android.binaryeye.widget
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.graphics.*
|
||||
import android.os.Build
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
import android.util.AttributeSet
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.app.prefs
|
||||
import de.markusfisch.android.binaryeye.graphics.Dots
|
||||
import de.markusfisch.android.binaryeye.graphics.getBitmapFromDrawable
|
||||
import de.markusfisch.android.binaryeye.graphics.getDashedBorderPaint
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
class DetectorView : View {
|
||||
val currentOrientation = resources.configuration.orientation
|
||||
val roi = Rect()
|
||||
|
||||
var onRoiChange: (() -> Unit)? = null
|
||||
var onRoiChanged: (() -> Unit)? = null
|
||||
|
||||
private val dots = Dots(context)
|
||||
private val invalidateRunnable: Runnable = Runnable {
|
||||
marks = null
|
||||
invalidate()
|
||||
}
|
||||
private val roiPaint = context.getDashedBorderPaint()
|
||||
private val handleBitmap = resources.getBitmapFromDrawable(
|
||||
R.drawable.button_crop
|
||||
)
|
||||
private val handleXRadius = handleBitmap.width / 2
|
||||
private val handleYRadius = handleBitmap.height / 2
|
||||
private val handlePos = Point(-1, -1)
|
||||
private val handleHome = Point()
|
||||
private val center = Point()
|
||||
private val touchDown = Point()
|
||||
private val distToFull: Int
|
||||
private val minMoveThresholdSq: Int
|
||||
private val cornerRadius: Int
|
||||
private val fabHeight: Int
|
||||
private val padding: Int
|
||||
|
||||
private var marks: List<Point>? = null
|
||||
private var handleGrabbed = false
|
||||
private var handleActive = false
|
||||
private var minY = 0
|
||||
private var maxY = 0
|
||||
private var minDist = 0
|
||||
private var shadeColor = 0
|
||||
|
||||
init {
|
||||
val dp = context.resources.displayMetrics.density
|
||||
distToFull = (24f * dp).roundToInt()
|
||||
val minMoveThreshold = (8f * dp).roundToInt()
|
||||
minMoveThresholdSq = minMoveThreshold * minMoveThreshold
|
||||
cornerRadius = (8f * dp).roundToInt()
|
||||
fabHeight = (92f * dp).roundToInt()
|
||||
padding = (20f * dp).roundToInt()
|
||||
isSaveEnabled = true
|
||||
}
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet) :
|
||||
super(context, attrs)
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) :
|
||||
super(context, attrs, defStyleAttr)
|
||||
|
||||
fun setCropHandlePos(x: Int, y: Int, orientation: Int) {
|
||||
if (orientation == currentOrientation) {
|
||||
handlePos.set(x, y)
|
||||
} else {
|
||||
handlePos.set(y, x)
|
||||
}
|
||||
if (x > -1) {
|
||||
handleActive = true
|
||||
}
|
||||
}
|
||||
|
||||
fun getCropHandlePos() = if (handleActive) {
|
||||
handlePos
|
||||
} else {
|
||||
Point(-1, -1)
|
||||
}
|
||||
|
||||
fun mark(points: List<Point>) {
|
||||
marks = points
|
||||
invalidate()
|
||||
removeCallbacks(invalidateRunnable)
|
||||
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")
|
||||
override fun onTouchEvent(event: MotionEvent?): Boolean {
|
||||
event ?: return super.onTouchEvent(event)
|
||||
val x = event.x.roundToInt()
|
||||
val y = event.y.roundToInt()
|
||||
return when (event.actionMasked) {
|
||||
MotionEvent.ACTION_DOWN -> {
|
||||
if (prefs.showCropHandle) {
|
||||
touchDown.set(x, y)
|
||||
handleGrabbed = abs(x - handlePos.x) < handleXRadius &&
|
||||
abs(y - handlePos.y) < handleYRadius
|
||||
if (handleGrabbed) {
|
||||
onRoiChange?.invoke()
|
||||
}
|
||||
handleGrabbed
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
MotionEvent.ACTION_MOVE -> {
|
||||
if (handleGrabbed) {
|
||||
handlePos.set(x, y)
|
||||
handleActive =
|
||||
handleActive or (distSq(handlePos, touchDown) > minMoveThresholdSq)
|
||||
if (handleActive) {
|
||||
updateClipRect()
|
||||
invalidate()
|
||||
}
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
MotionEvent.ACTION_CANCEL -> {
|
||||
if (handleGrabbed) {
|
||||
snap(x, y)
|
||||
handleGrabbed = false
|
||||
}
|
||||
false
|
||||
}
|
||||
MotionEvent.ACTION_UP -> {
|
||||
if (handleGrabbed) {
|
||||
if (!handleActive) {
|
||||
setHandleToDefaultRoi()
|
||||
handleActive = true
|
||||
} else {
|
||||
snap(x, y)
|
||||
}
|
||||
if (handleActive) {
|
||||
updateClipRect()
|
||||
invalidate()
|
||||
}
|
||||
onRoiChanged?.invoke()
|
||||
handleGrabbed = false
|
||||
}
|
||||
false
|
||||
}
|
||||
else -> super.onTouchEvent(event)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setHandleToDefaultRoi() {
|
||||
val mn = min(center.x, center.y) * .8f
|
||||
handlePos.set(
|
||||
(center.x + mn).roundToInt(),
|
||||
(center.y + mn).roundToInt()
|
||||
)
|
||||
}
|
||||
|
||||
private fun snap(x: Int, y: Int) {
|
||||
val cx = clampX(x)
|
||||
val cy = clampY(y)
|
||||
val dx = abs(cx - center.x)
|
||||
val dy = abs(cy - center.y)
|
||||
// check if handle is close to the vertical or horizontal center line
|
||||
if (dx < distToFull ||
|
||||
dy < distToFull ||
|
||||
// check if handle is close to a screen corner
|
||||
((abs(cy - minY) < distToFull || abs(maxY - cy) < distToFull) &&
|
||||
abs(dx - center.x) < distToFull)
|
||||
) {
|
||||
reset()
|
||||
invalidate()
|
||||
}
|
||||
}
|
||||
|
||||
private fun reset() {
|
||||
handlePos.set(handleHome)
|
||||
handleActive = false
|
||||
roi.set(0, 0, 0, 0)
|
||||
}
|
||||
|
||||
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
|
||||
super.onLayout(changed, left, top, right, bottom)
|
||||
val width = right - left
|
||||
val height = bottom - top
|
||||
center.set(
|
||||
left + (width / 2),
|
||||
top + (height / 2)
|
||||
)
|
||||
minY = padding * 2
|
||||
maxY = height - minY
|
||||
handleHome.set(
|
||||
width - handleXRadius - paddingRight - padding,
|
||||
height - handleYRadius - paddingBottom - fabHeight
|
||||
)
|
||||
if (handleActive) {
|
||||
updateClipRect()
|
||||
} else {
|
||||
reset()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDraw(canvas: Canvas) {
|
||||
if (handleActive) {
|
||||
drawClip(canvas)
|
||||
}
|
||||
marks?.let {
|
||||
dots.draw(canvas, it)
|
||||
}
|
||||
if (prefs.showCropHandle) {
|
||||
canvas.drawBitmap(
|
||||
handleBitmap,
|
||||
(handlePos.x - handleXRadius).toFloat(),
|
||||
(handlePos.y - handleYRadius).toFloat(),
|
||||
null
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun drawClip(canvas: Canvas) {
|
||||
if (minDist < 1) {
|
||||
return
|
||||
}
|
||||
// canvas.clipRect() doesn't work reliably below KITKAT
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||
val radius = min(minDist / 2, cornerRadius).toFloat()
|
||||
canvas.save()
|
||||
canvas.clipOutPathCompat(
|
||||
calculateRoundedRectPath(
|
||||
roi.left.toFloat(),
|
||||
roi.top.toFloat(),
|
||||
roi.right.toFloat(),
|
||||
roi.bottom.toFloat(),
|
||||
radius,
|
||||
radius
|
||||
)
|
||||
)
|
||||
canvas.drawColor(shadeColor)
|
||||
canvas.restore()
|
||||
} else {
|
||||
canvas.drawRect(roi, roiPaint)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateClipRect() {
|
||||
clampHandlePos()
|
||||
val dx = abs(handlePos.x - center.x)
|
||||
val dy = abs(handlePos.y - center.y)
|
||||
minDist = min(dx, dy)
|
||||
shadeColor = (min(
|
||||
1f,
|
||||
minDist.toFloat() / distToFull.toFloat()
|
||||
) * 128f).toInt() shl 24
|
||||
roi.set(
|
||||
center.x - dx,
|
||||
center.y - dy,
|
||||
center.x + dx,
|
||||
center.y + dy
|
||||
)
|
||||
}
|
||||
|
||||
private fun clampHandlePos() {
|
||||
handlePos.x = clampX(handlePos.x)
|
||||
handlePos.y = clampY(handlePos.y)
|
||||
}
|
||||
|
||||
private fun clampX(x: Int) = min(center.x * 2, max(0, x))
|
||||
|
||||
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 {
|
||||
val dx = a.x - b.x
|
||||
val dy = a.y - b.y
|
||||
return dx * dx + dy * dy
|
||||
}
|
||||
|
||||
private fun Point.set(point: Point) {
|
||||
x = point.x
|
||||
y = point.y
|
||||
}
|
||||
|
||||
private fun Canvas.clipOutPathCompat(path: Path) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
clipOutPath(path)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
clipPath(path, Region.Op.DIFFERENCE)
|
||||
}
|
||||
}
|
||||
|
||||
private fun calculateRoundedRectPath(
|
||||
left: Float,
|
||||
top: Float,
|
||||
right: Float,
|
||||
bottom: Float,
|
||||
rx: Float,
|
||||
ry: Float
|
||||
): Path {
|
||||
val width = right - left
|
||||
val height = bottom - top
|
||||
val widthMinusCorners = width - 2 * rx
|
||||
val heightMinusCorners = height - 2 * ry
|
||||
return Path().apply {
|
||||
moveTo(right, top + ry)
|
||||
rQuadTo(0f, -ry, -rx, -ry)
|
||||
rLineTo(-widthMinusCorners, 0f)
|
||||
rQuadTo(-rx, 0f, -rx, ry)
|
||||
rLineTo(0f, heightMinusCorners)
|
||||
rQuadTo(0f, ry, rx, ry)
|
||||
rLineTo(widthMinusCorners, 0f)
|
||||
rQuadTo(rx, 0f, rx, -ry)
|
||||
rLineTo(0f, -heightMinusCorners)
|
||||
close()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package de.markusfisch.android.binaryeye.widget
|
||||
|
||||
import android.content.Context
|
||||
import android.widget.Toast
|
||||
|
||||
fun Context.toast(message: Int) = Toast.makeText(
|
||||
applicationContext,
|
||||
message,
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
|
||||
fun Context.toast(message: String) = Toast.makeText(
|
||||
applicationContext,
|
||||
message,
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
@@ -1,32 +1,22 @@
|
||||
package de.markusfisch.android.binaryeye.zxing
|
||||
|
||||
import com.google.zxing.BarcodeFormat
|
||||
import com.google.zxing.BinaryBitmap
|
||||
import com.google.zxing.DecodeHintType
|
||||
import com.google.zxing.LuminanceSource
|
||||
import com.google.zxing.MultiFormatReader
|
||||
import com.google.zxing.MultiFormatWriter
|
||||
import com.google.zxing.PlanarYUVLuminanceSource
|
||||
import com.google.zxing.ReaderException
|
||||
import com.google.zxing.Result
|
||||
import com.google.zxing.common.HybridBinarizer
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import com.google.zxing.*
|
||||
import com.google.zxing.common.BitMatrix
|
||||
import com.google.zxing.common.HybridBinarizer
|
||||
import java.util.*
|
||||
|
||||
import java.util.Arrays
|
||||
import java.util.EnumMap
|
||||
import java.util.EnumSet
|
||||
|
||||
class Zxing {
|
||||
private val multiFormatReader: MultiFormatReader = MultiFormatReader()
|
||||
class Zxing(possibleResultPoint: ResultPointCallback? = null) {
|
||||
private val hints = EnumMap<DecodeHintType, Any>(DecodeHintType::class.java)
|
||||
private val multiFormatReader = MultiFormatReader()
|
||||
|
||||
init {
|
||||
val decodeFormats = EnumSet.noneOf<BarcodeFormat>(
|
||||
val decodeFormats = EnumSet.noneOf(
|
||||
BarcodeFormat::class.java
|
||||
)
|
||||
decodeFormats.addAll(
|
||||
EnumSet.copyOf(
|
||||
Arrays.asList(
|
||||
listOf(
|
||||
BarcodeFormat.AZTEC,
|
||||
BarcodeFormat.CODABAR,
|
||||
BarcodeFormat.CODE_39,
|
||||
@@ -47,11 +37,18 @@ class Zxing {
|
||||
)
|
||||
)
|
||||
)
|
||||
hints[DecodeHintType.POSSIBLE_FORMATS] = decodeFormats
|
||||
possibleResultPoint?.let {
|
||||
hints[DecodeHintType.NEED_RESULT_POINT_CALLBACK] = it
|
||||
}
|
||||
}
|
||||
|
||||
val hints = EnumMap<DecodeHintType, Any>(DecodeHintType::class.java)
|
||||
hints.put(DecodeHintType.POSSIBLE_FORMATS, decodeFormats)
|
||||
|
||||
multiFormatReader.setHints(hints)
|
||||
fun updateHints(tryHarder: Boolean) {
|
||||
if (tryHarder) {
|
||||
hints[DecodeHintType.TRY_HARDER] = java.lang.Boolean.TRUE
|
||||
} else {
|
||||
hints.remove(DecodeHintType.TRY_HARDER)
|
||||
}
|
||||
}
|
||||
|
||||
fun decode(
|
||||
@@ -70,6 +67,39 @@ class Zxing {
|
||||
height,
|
||||
false
|
||||
)
|
||||
return decodeLuminanceSource(source, invert)
|
||||
}
|
||||
|
||||
fun decodePositiveNegative(bitmap: Bitmap): Result? =
|
||||
decode(bitmap, false) ?: decode(bitmap, true)
|
||||
|
||||
private fun decode(bitmap: Bitmap, invert: Boolean = false): Result? {
|
||||
val pixels = IntArray(bitmap.width * bitmap.height)
|
||||
return decode(pixels, bitmap, invert)
|
||||
}
|
||||
|
||||
private fun decode(
|
||||
pixels: IntArray,
|
||||
bitmap: Bitmap,
|
||||
invert: Boolean = false
|
||||
): Result? {
|
||||
val width = bitmap.width
|
||||
val height = bitmap.height
|
||||
if (bitmap.config != Bitmap.Config.ARGB_8888) {
|
||||
bitmap.copy(Bitmap.Config.ARGB_8888, true)
|
||||
} else {
|
||||
bitmap
|
||||
}.getPixels(pixels, 0, width, 0, 0, width, height)
|
||||
return decodeLuminanceSource(
|
||||
RGBLuminanceSource(width, height, pixels),
|
||||
invert
|
||||
)
|
||||
}
|
||||
|
||||
private fun decodeLuminanceSource(
|
||||
source: LuminanceSource,
|
||||
invert: Boolean
|
||||
): Result? {
|
||||
return decodeLuminanceSource(
|
||||
if (invert) {
|
||||
source.invert()
|
||||
@@ -82,44 +112,105 @@ class Zxing {
|
||||
private fun decodeLuminanceSource(source: LuminanceSource): Result? {
|
||||
val bitmap = BinaryBitmap(HybridBinarizer(source))
|
||||
return try {
|
||||
multiFormatReader.decodeWithState(bitmap)
|
||||
multiFormatReader.decode(bitmap, hints)
|
||||
} catch (e: ReaderException) {
|
||||
null
|
||||
} finally {
|
||||
multiFormatReader.reset()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val BLACK = 0xff000000.toInt()
|
||||
private const val WHITE = 0xffffffff.toInt()
|
||||
private const val BLACK = 0xff000000.toInt()
|
||||
private const val WHITE = 0xffffffff.toInt()
|
||||
|
||||
fun encodeAsBitmap(
|
||||
text: String,
|
||||
format: BarcodeFormat,
|
||||
width: Int,
|
||||
height: Int
|
||||
): Bitmap? {
|
||||
val result = MultiFormatWriter().encode(
|
||||
text, format,
|
||||
width, height, null
|
||||
)
|
||||
val w = result.getWidth()
|
||||
val h = result.getHeight()
|
||||
val pixels = IntArray(w * h)
|
||||
var offset = 0
|
||||
for (y in 0..h - 1) {
|
||||
for (x in 0..w - 1) {
|
||||
pixels[offset + x] = if (result.get(x, y))
|
||||
BLACK
|
||||
else
|
||||
WHITE
|
||||
}
|
||||
offset += w
|
||||
fun encodeAsBitmap(
|
||||
text: String,
|
||||
format: BarcodeFormat,
|
||||
width: Int,
|
||||
height: Int,
|
||||
hints: EnumMap<EncodeHintType, Any>? = null
|
||||
): Bitmap? {
|
||||
val bitMatrix = encode(text, format, hints, width, height)
|
||||
val w = bitMatrix.width
|
||||
val h = bitMatrix.height
|
||||
val pixels = IntArray(w * h)
|
||||
var offset = 0
|
||||
for (y in 0 until h) {
|
||||
for (x in 0 until w) {
|
||||
pixels[offset + x] = if (bitMatrix.get(x, y)) {
|
||||
BLACK
|
||||
} else {
|
||||
WHITE
|
||||
}
|
||||
}
|
||||
offset += w
|
||||
}
|
||||
val bitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888)
|
||||
bitmap.setPixels(pixels, 0, w, 0, 0, w, h)
|
||||
return bitmap
|
||||
}
|
||||
|
||||
fun encodeAsSvg(
|
||||
text: String,
|
||||
format: BarcodeFormat,
|
||||
hints: EnumMap<EncodeHintType, Any>? = null
|
||||
): String {
|
||||
val bitMatrix = encode(text, format, hints)
|
||||
val sb = StringBuilder()
|
||||
val w = bitMatrix.width
|
||||
var h = bitMatrix.height
|
||||
val moduleHeight = if (h == 1) w / 2 else 1
|
||||
for (y in 0 until h) {
|
||||
for (x in 0 until w) {
|
||||
if (bitMatrix.get(x, y)) {
|
||||
sb.append(" M${x},${y}h1v${moduleHeight}h-1z")
|
||||
}
|
||||
val bitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888)
|
||||
bitmap.setPixels(pixels, 0, width, 0, 0, w, h)
|
||||
return bitmap
|
||||
}
|
||||
}
|
||||
h *= moduleHeight
|
||||
return """<svg width="$w" height="$h"
|
||||
viewBox="0 0 $w $h"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="$sb"/>
|
||||
</svg>
|
||||
"""
|
||||
}
|
||||
|
||||
fun encodeAsText(
|
||||
text: String,
|
||||
format: BarcodeFormat,
|
||||
hints: EnumMap<EncodeHintType, Any>? = null
|
||||
): String {
|
||||
val bitMatrix = encode(text, format, hints)
|
||||
val w = bitMatrix.width
|
||||
val h = bitMatrix.height
|
||||
val sb = StringBuilder()
|
||||
for (y in 0 until h) {
|
||||
for (x in 0 until w) {
|
||||
sb.append(if (bitMatrix.get(x, y)) "█" else " ")
|
||||
}
|
||||
sb.append("\n")
|
||||
}
|
||||
return sb.toString()
|
||||
}
|
||||
|
||||
private fun encode(
|
||||
text: String,
|
||||
format: BarcodeFormat,
|
||||
encodeHints: EnumMap<EncodeHintType, Any>? = null,
|
||||
width: Int = 0,
|
||||
height: Int = 0
|
||||
): BitMatrix {
|
||||
val hints = encodeHints ?: EnumMap<EncodeHintType, Any>(EncodeHintType::class.java)
|
||||
if (!hints.contains(EncodeHintType.CHARACTER_SET)) {
|
||||
hints[EncodeHintType.CHARACTER_SET] = "utf-8"
|
||||
}
|
||||
return MultiFormatWriter().encode(
|
||||
text,
|
||||
format,
|
||||
width,
|
||||
height,
|
||||
hints
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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_decode_foreground"/>
|
||||
</adaptive-icon>
|
||||
@@ -0,0 +1,12 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<path
|
||||
android:pathData="M54,55.787m-5.719,0a5.719,5.719 0,1 1,11.438 0a5.719,5.719 0,1 1,-11.438 0"
|
||||
android:fillColor="#ffffff"/>
|
||||
<path
|
||||
android:pathData="M48.639,37.916L45.368,41.491L39.703,41.491C37.738,41.491 36.129,43.099 36.129,45.065L36.129,66.509C36.129,68.475 37.738,70.084 39.703,70.084L68.297,70.084C70.262,70.084 71.871,68.475 71.871,66.509L71.871,45.065C71.871,43.099 70.262,41.491 68.297,41.491L62.632,41.491L59.361,37.916ZM54,64.722C49.068,64.722 45.065,60.719 45.065,55.787C45.065,50.855 49.068,46.852 54,46.852C58.932,46.852 62.935,50.855 62.935,55.787C62.935,60.719 58.932,64.722 54,64.722Z"
|
||||
android:fillColor="#ffffff"/>
|
||||
</vector>
|
||||
@@ -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_encode_foreground"/>
|
||||
</adaptive-icon>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<path
|
||||
android:pathData="M69.75,56.25L56.25,56.25L56.25,69.75L51.75,69.75L51.75,56.25L38.25,56.25L38.25,51.75L51.75,51.75L51.75,38.25L56.25,38.25L56.25,51.75L69.75,51.75Z"
|
||||
android:fillColor="#FFFFFF"/>
|
||||
</vector>
|
||||
@@ -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_preferences_foreground"/>
|
||||
</adaptive-icon>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<path
|
||||
android:pathData="M70.057,56.114C70.147,55.439 70.192,54.742 70.192,54C70.192,53.281 70.147,52.561 70.034,51.886L74.6,48.333C75.005,48.018 75.117,47.411 74.87,46.961L70.552,39.495C70.282,39 69.72,38.843 69.225,39L63.85,41.159C62.726,40.305 61.534,39.585 60.207,39.045L59.397,33.333C59.307,32.793 58.858,32.411 58.318,32.411L49.682,32.411C49.142,32.411 48.715,32.793 48.625,33.333L47.816,39.045C46.489,39.585 45.274,40.327 44.172,41.159L38.798,39C38.303,38.82 37.741,39 37.471,39.495L33.175,46.961C32.906,47.433 32.996,48.018 33.445,48.333L38.011,51.886C37.898,52.561 37.808,53.303 37.808,54C37.808,54.697 37.853,55.439 37.966,56.114L33.4,59.667C32.996,59.982 32.883,60.589 33.13,61.039L37.448,68.505C37.718,69 38.28,69.158 38.775,69L44.15,66.841C45.274,67.696 46.466,68.415 47.793,68.955L48.603,74.667C48.715,75.207 49.142,75.589 49.682,75.589L58.318,75.589C58.858,75.589 59.307,75.207 59.375,74.667L60.184,68.955C61.511,68.415 62.726,67.696 63.828,66.841L69.202,69C69.697,69.18 70.259,69 70.529,68.505L74.847,61.039C75.117,60.544 75.005,59.982 74.577,59.667ZM54,62.096C49.547,62.096 45.904,58.453 45.904,54C45.904,49.547 49.547,45.904 54,45.904C58.453,45.904 62.096,49.547 62.096,54C62.096,58.453 58.453,62.096 54,62.096Z"
|
||||
android:fillColor="#ffffff"/>
|
||||
</vector>
|
||||
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 2.4 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 899 B |