Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9f2efeb001 | ||
|
|
105c81d409 | ||
|
|
4e1eb28ca4 | ||
|
|
b72ed746bf | ||
|
|
1aaee3c85a | ||
|
|
eb0218e733 | ||
|
|
bef7365f28 | ||
|
|
c72b6a2e5f | ||
|
|
a7e358852c | ||
|
|
7cf8cbe6d8 | ||
|
|
e379e55570 | ||
|
|
a55795a7b6 | ||
|
|
83c0cd0293 | ||
|
|
d138148cb8 | ||
|
|
c3c08879e0 | ||
|
|
d0f02faca7 | ||
|
|
c9db3514d1 | ||
|
|
f2a0e1a392 | ||
|
|
f4de041407 | ||
|
|
73ba2914e8 | ||
|
|
2907c8c49c | ||
|
|
f28f956a8d | ||
|
|
972eb6a0b0 | ||
|
|
08bb3f2127 | ||
|
|
b28e939f58 | ||
|
|
2edb8112b7 | ||
|
|
cf5a6dc8b8 | ||
|
|
3ec6f18fe0 | ||
|
|
3a8f6cb827 | ||
|
|
65c7fe52ee | ||
|
|
5d3b3eb195 | ||
|
|
0e4c2963a5 | ||
|
|
791c50cd4a | ||
|
|
c04b08ab87 | ||
|
|
f07c4ac0f4 | ||
|
|
05d63bd73f | ||
|
|
558fd5212d | ||
|
|
b918be76e4 | ||
|
|
2136a8c73e | ||
|
|
959c6e7d4d | ||
|
|
50930d07a6 | ||
|
|
36e7aab27c | ||
|
|
bb4fc8ed08 | ||
|
|
7a33ebd1a3 | ||
|
|
c8444b8935 | ||
|
|
4c6d66a4aa | ||
|
|
39f95a1c04 | ||
|
|
9e30b108e7 | ||
|
|
1b6b1cefe3 | ||
|
|
b634370a0b | ||
|
|
f32d44c3a1 | ||
|
|
2e060d1ca8 | ||
|
|
6acdce32f2 | ||
|
|
092ec694f6 | ||
|
|
7fed7877a0 | ||
|
|
b17393d5ef | ||
|
|
319ae53b3b | ||
|
|
6a47a7e477 | ||
|
|
19fb733182 | ||
|
|
ce6c6f26d2 | ||
|
|
3633af5e0a | ||
|
|
87502f7226 | ||
|
|
f4af35ad46 | ||
|
|
60f0d8bd87 | ||
|
|
1ccfcfcb8f | ||
|
|
213689e9f9 | ||
|
|
6e2f2cab92 | ||
|
|
4b3d745154 | ||
|
|
a2bcd67568 | ||
|
|
072eec5306 | ||
|
|
d505c6b07d | ||
|
|
dbfbaab030 | ||
|
|
3d1402add4 | ||
|
|
1d2bd8d778 | ||
|
|
2bb413e3e8 | ||
|
|
75a59bc3e9 | ||
|
|
9a15bf0871 | ||
|
|
53248fd23d | ||
|
|
1dde8c2817 | ||
|
|
57fea95b9b | ||
|
|
806dafbecd | ||
|
|
44b788fa74 | ||
|
|
9f167e7a2b | ||
|
|
6852b65239 | ||
|
|
3d7c9be7ba | ||
|
|
f9401a9dda | ||
|
|
79dd48b07a | ||
|
|
6a7ab2c6e5 | ||
|
|
c76d96cbe4 | ||
|
|
e1b3935659 | ||
|
|
b7a4db5f4b | ||
|
|
f23414b003 | ||
|
|
282d2499a8 | ||
|
|
a6cc8ed32f | ||
|
|
7b155369fd | ||
|
|
55a69db2c5 | ||
|
|
170878a795 | ||
|
|
18ade0fe33 | ||
|
|
4565848344 | ||
|
|
353e1b5494 | ||
|
|
9986a4ffb5 | ||
|
|
c464b7de02 | ||
|
|
e588405413 | ||
|
|
fdaf20fd45 | ||
|
|
7c8fdb8ea1 | ||
|
|
bb9d556c16 | ||
|
|
9e395d1928 | ||
|
|
95f12e7c31 | ||
|
|
9dac0b13fb | ||
|
|
7939a728bc | ||
|
|
b6ca20d813 | ||
|
|
fe7e36804e | ||
|
|
8cf1c35029 | ||
|
|
62ca2299a0 | ||
|
|
2423586c8c | ||
|
|
29b1f44bb9 | ||
|
|
acee18e016 | ||
|
|
f798dce832 | ||
|
|
97838ec7c4 | ||
|
|
767860ab07 | ||
|
|
f8fd00f21b | ||
|
|
2cf6aa0812 | ||
|
|
197554fd1b | ||
|
|
3dc35ac60d | ||
|
|
fd05209f57 | ||
|
|
6d5aed816f | ||
|
|
268c395043 | ||
|
|
867dc36811 | ||
|
|
e6e9b912b6 | ||
|
|
152b4e50fc | ||
|
|
93070f38c9 | ||
|
|
7a162d6fb0 | ||
|
|
9c03e83d3a | ||
|
|
4be9421b44 | ||
|
|
b895997967 | ||
|
|
b5bbe659db | ||
|
|
bb7715455a | ||
|
|
5283acee8f | ||
|
|
315abfb1dd | ||
|
|
05c776168e | ||
|
|
880b8e0b24 | ||
|
|
1597adb284 | ||
|
|
1c356aeaed | ||
|
|
fe934c5d6e | ||
|
|
5091565ff0 | ||
|
|
c7eb664958 | ||
|
|
c763c522e7 | ||
|
|
dc1beb5f28 | ||
|
|
76ea46cea8 | ||
|
|
0991fb85f0 | ||
|
|
09f0d2e1db | ||
|
|
bc0655788c | ||
|
|
739a801cb4 | ||
|
|
c18f1b8770 | ||
|
|
b0df09d76f | ||
|
|
e8b8e578cb | ||
|
|
09ce14125e | ||
|
|
446d1543c3 | ||
|
|
02a360beaf | ||
|
|
9742e5b790 | ||
|
|
9c08aa1184 | ||
|
|
ce8a5f89cd | ||
|
|
50795afeb4 | ||
|
|
b8c941ee62 | ||
|
|
5b1738342d | ||
|
|
004738440c | ||
|
|
a90aee570a | ||
|
|
17f2f88b7f | ||
|
|
071ea1500f | ||
|
|
25b142407b | ||
|
|
848b4bbaa8 | ||
|
|
c2dc8984c1 | ||
|
|
e37a4d21ea | ||
|
|
da2a20ba1f | ||
|
|
5fe69e5a3d | ||
|
|
f50e1dab0a | ||
|
|
b165192377 | ||
|
|
c0a4d67cc5 | ||
|
|
ef36a2f7e0 | ||
|
|
40b90e0be2 | ||
|
|
680030e89e | ||
|
|
b4fefad54f | ||
|
|
f65e942100 | ||
|
|
95d7a84648 | ||
|
|
1e4a0fde7a | ||
|
|
a6785d500e | ||
|
|
d77e464554 | ||
|
|
d7198934d7 | ||
|
|
1a3b3f263f | ||
|
|
d4c5c6a1dc | ||
|
|
8618a5b482 | ||
|
|
6cb65635e3 | ||
|
|
5e37ff41d6 | ||
|
|
03ac3ff0e9 | ||
|
|
480a248e71 | ||
|
|
b4060d70ce | ||
|
|
7ceb97e942 | ||
|
|
7100f79efc | ||
|
|
00a1ec877b | ||
|
|
89e029687b | ||
|
|
5f4f18a062 | ||
|
|
69ec2cda04 | ||
|
|
4aba885fb6 | ||
|
|
9dbb78ce67 | ||
|
|
47c252ab18 | ||
|
|
10e02b3164 | ||
|
|
7e82dbe730 | ||
|
|
e91c30f7c2 | ||
|
|
30a36e7e07 | ||
|
|
5f91fe2f02 | ||
|
|
cab8d6c038 | ||
|
|
4aac5dc61b | ||
|
|
fbbc20d327 | ||
|
|
5595b68ff4 | ||
|
|
c492897fa9 | ||
|
|
647c08bfb8 | ||
|
|
e5025ef58c | ||
|
|
74bb421b2d | ||
|
|
4ed8d4614a | ||
|
|
f038eb989b | ||
|
|
c5722a3890 | ||
|
|
41108007ad | ||
|
|
867f179a37 | ||
|
|
892916b0d0 | ||
|
|
3bd440fdb7 | ||
|
|
a4c8551be8 | ||
|
|
316644d3ae | ||
|
|
9fb2eb2d2b | ||
|
|
dd046a5d52 | ||
|
|
f3ddd458cb | ||
|
|
157d928620 | ||
|
|
94cb509f0b | ||
|
|
df18f8ebd3 | ||
|
|
705095472f | ||
|
|
f897973b3f | ||
|
|
c88d5cf4cd | ||
|
|
8ecd49731f | ||
|
|
6f3b4dbdab | ||
|
|
3c8c0737c4 | ||
|
|
9baf6d621a | ||
|
|
f0d0ee04af | ||
|
|
4d38da045a | ||
|
|
af70d9507b | ||
|
|
273f38c36c | ||
|
|
210cdfc171 | ||
|
|
3ec8c4b5d7 | ||
|
|
d99f0ca039 | ||
|
|
4a9e700d20 | ||
|
|
40e339ce89 | ||
|
|
9d476b3491 | ||
|
|
22b58265a5 | ||
|
|
bd4576b7ac | ||
|
|
a57f6d1904 | ||
|
|
842b38c986 | ||
|
|
b1fae727a9 | ||
|
|
db24f608d5 | ||
|
|
9f49a00bae | ||
|
|
947bbf3031 | ||
|
|
b3b57a7ba8 | ||
|
|
1627f3ead9 | ||
|
|
4343f5da82 | ||
|
|
67395a067d | ||
|
|
0de571f3c1 | ||
|
|
d83cd77a87 | ||
|
|
5ca7aff5df | ||
|
|
e4cc516a9b | ||
|
|
f160cf0953 | ||
|
|
ef5b5aa34a | ||
|
|
536ee5bc3d | ||
|
|
0becada572 | ||
|
|
0c773d6f05 | ||
|
|
f2df364840 | ||
|
|
5740b43568 | ||
|
|
b384b99b01 | ||
|
|
6affa4ce25 | ||
|
|
455d329fff | ||
|
|
792be87283 | ||
|
|
381e10bddc | ||
|
|
220d1b9d4c | ||
|
|
020cd2c192 | ||
|
|
e8766a0fd1 | ||
|
|
69c6d1324e | ||
|
|
2dd0c48225 | ||
|
|
faa536274b | ||
|
|
0a944f9fb8 | ||
|
|
a31982646a | ||
|
|
4320ee8fcc | ||
|
|
d890bf28fa | ||
|
|
6347bd568c | ||
|
|
9de11e2ae8 | ||
|
|
57c381e310 | ||
|
|
a11650eaa5 | ||
|
|
e85b006e40 | ||
|
|
a6c25be316 | ||
|
|
2b011325a6 | ||
|
|
fb96d5ecf8 | ||
|
|
897d61a646 | ||
|
|
ac67db6491 | ||
|
|
84bc7dad30 | ||
|
|
b21af65b68 | ||
|
|
a7adec7177 | ||
|
|
fc6fcfa743 | ||
|
|
47f95fc67a | ||
|
|
deccc9eb36 | ||
|
|
6505312b44 | ||
|
|
cb82fc3252 | ||
|
|
636ac2386a | ||
|
|
49d32ff95c | ||
|
|
fed7d561c5 | ||
|
|
8e22290c5f | ||
|
|
b33e474d12 | ||
|
|
40a05a1fc0 | ||
|
|
d947764882 | ||
|
|
1a5a8e0fa1 | ||
|
|
e6d9d54866 | ||
|
|
538155bf04 | ||
|
|
3266a58041 | ||
|
|
d594a43d6d | ||
|
|
0aa1f80451 | ||
|
|
36eff75546 | ||
|
|
8aef9c22dd | ||
|
|
1a159b35ee | ||
|
|
05ab87caa8 | ||
|
|
a3f0185f08 | ||
|
|
104f409486 | ||
|
|
eb8f578bd8 | ||
|
|
a8e420fc95 | ||
|
|
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 |
@@ -1,5 +1,208 @@
|
||||
# Change Log
|
||||
|
||||
## 1.54.1
|
||||
* Update Russian translation
|
||||
* Update Turkish translation
|
||||
* Update Italian translation
|
||||
* Fix handling GS1 and ISO15434 content types
|
||||
* Fix VerifyError on Android 4
|
||||
|
||||
## 1.54.0
|
||||
* Recreate and show barcode from read contents if possible
|
||||
* Add ebay, Amazon and barcodelookup.com to search engines
|
||||
|
||||
## 1.53.1
|
||||
* Fix proguard configuration
|
||||
|
||||
## 1.53.0
|
||||
* Migrate to ZXing-C++ and add support for Micro QR Codes
|
||||
* Multi-select scans in history listing
|
||||
* Remove preset content for Encode shortcut
|
||||
* Always show QR Code version and remove setting
|
||||
* Fix PDF-417 UTF-8 text encoding
|
||||
* Hide sensitive content from clipboard
|
||||
* Remove setting a custom language on Android 13+
|
||||
* Condense text output of generated barcodes
|
||||
* Update Russian translation
|
||||
* Update Spanish translation
|
||||
* Add Persian translation
|
||||
|
||||
## 1.52.0
|
||||
* Fix parsing timezone in calendar dates
|
||||
* Update French translation
|
||||
|
||||
## 1.51.1
|
||||
* Fix extraction of raw bytes for some barcodes
|
||||
|
||||
## 1.51.0
|
||||
* Add support for Deutsche Post Matrixcode stamp
|
||||
* Allow free image rotation for scanning from images
|
||||
* Fix setting TO field in MATMSG format
|
||||
* Update Italian translation
|
||||
* Update zh-rCN translation
|
||||
* Update pt-br translation
|
||||
|
||||
## 1.50.0
|
||||
* Add support for MATMSG format
|
||||
* Add a setting to show the QR Code version
|
||||
* Add timezone offset in calendar dates
|
||||
* Recognize colloquial incomplete URLs
|
||||
* Update ZXing library to latest version
|
||||
* Update Czech translation
|
||||
* Update Russian translation
|
||||
* Update Japanese translation
|
||||
* Update Hungarian translation
|
||||
|
||||
## 1.49.2
|
||||
* Update design of app icon
|
||||
* Update pt-br translation
|
||||
|
||||
## 1.49.1
|
||||
* Update Simplified Chinese translation
|
||||
* Return directly to caller for deep links
|
||||
* Fix metadata for deep links
|
||||
|
||||
## 1.49.0
|
||||
* Add a setting to quickly enable/disable forwarding
|
||||
* Stop playing error beep when in silent mode
|
||||
* Improve enabling/disabling torch mode
|
||||
* Update to latest ZXing version
|
||||
* Update German translation
|
||||
* Update Russian translation
|
||||
|
||||
## 1.48.1
|
||||
* Fix broken detection of binary content
|
||||
|
||||
## 1.48.0
|
||||
* Add a menu item to temporarily scan a certain format only
|
||||
* Add a monochome launcher icon for Android 13
|
||||
* Show alpha numeric characters in binary data
|
||||
* Fix showing binary data label for scans from the history
|
||||
* Fix handling empty meta strings
|
||||
* Update Turkish translation
|
||||
* Update Ukrainian translation
|
||||
* Update French translation
|
||||
* Update Polish translation
|
||||
* Update Russian translation
|
||||
|
||||
## 1.47.0
|
||||
* Add forwarding into external browser
|
||||
* Fix visibility of zoom slider when switching cameras
|
||||
|
||||
## 1.46.3
|
||||
* Fix cropping still images
|
||||
* Improve usability of zoom slider
|
||||
* Update Dutch translation
|
||||
* Update Chinese translation
|
||||
|
||||
## 1.46.2
|
||||
* Fix saving external files on Android Q and newer
|
||||
|
||||
## 1.46.1
|
||||
* Use cropping limiter settings for image picking too
|
||||
* Remove extra link on result screen
|
||||
* Update Turkish translation
|
||||
|
||||
## 1.46.0
|
||||
* Support deep links
|
||||
* Support "A:" in WIFI codes
|
||||
* Allow multiple query arguments in custom URL
|
||||
* Add Wi-Fi info data section
|
||||
* Add Vietnamese translation
|
||||
* Update Brazilian Portuguese translation
|
||||
* Update Hungarian translation
|
||||
* Update Ukrainian translation
|
||||
* Update Russian translation
|
||||
|
||||
## 1.45.0
|
||||
* Add a setting to set the delay for bulk mode
|
||||
* Add support for MECARD contact format
|
||||
* Remove unused permissions on Android Q
|
||||
|
||||
## 1.44.1
|
||||
* Update Spanish translation
|
||||
* Update Japanese translation
|
||||
* Fix in-app language switching for App Bundles
|
||||
|
||||
## 1.44.0
|
||||
* Add a setting to choose barcode formats to scan
|
||||
* Ask before removing network suggestions
|
||||
* Update Traditional Chinese Translation
|
||||
* Update Russian language
|
||||
* Update Dutch translation
|
||||
|
||||
## 1.43.0
|
||||
* Add a preference item to remove WiFi network suggestions
|
||||
* Update Traditional Chinese Translation
|
||||
* Update Russian language
|
||||
|
||||
## 1.42.0
|
||||
* Add a setting to pick a default search engine
|
||||
* Update Traditional Chinese
|
||||
* Update Dutch translation
|
||||
|
||||
## 1.41.1
|
||||
* Update Bengali translation
|
||||
* Update Simplified Chinese
|
||||
|
||||
## 1.41.0
|
||||
* Use cropping limiter by default for new installations
|
||||
* Truncate all toasts with strings from the outside
|
||||
* Add Bangla translation
|
||||
* Add missing Japanese locale
|
||||
* Update Hungarian translation
|
||||
* Update Russian app description
|
||||
|
||||
## 1.40.0
|
||||
* Add a setting to show/hide scanned data when scanning continuously
|
||||
* Only show the first 50 characters when scanning continuously
|
||||
* Share binary data as hex dump
|
||||
* Update Russian language
|
||||
|
||||
## 1.39.0
|
||||
* Fix adding contacts and calendar entries on Android 11+
|
||||
* Add a setting to always start in bulk mode
|
||||
* Add Czech translation
|
||||
* Update Spanish translation
|
||||
* Update Czech translation
|
||||
|
||||
## 1.38.0
|
||||
* Add a quick settings tile to scan from settings
|
||||
* Make custom background requests time out after 5 seconds
|
||||
* Add Japanese translation
|
||||
* Updated French translation
|
||||
|
||||
## 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
|
||||
|
||||
@@ -20,8 +20,18 @@ infer: clean
|
||||
test:
|
||||
./gradlew test
|
||||
|
||||
cat:
|
||||
./gradlew test cAT
|
||||
testview:
|
||||
adb shell am start -W -a android.intent.action.VIEW -d 'binaryeye://scan'
|
||||
|
||||
testscan:
|
||||
adb shell am start -a android.intent.action.VIEW \
|
||||
-c android.intent.category.BROWSABLE \
|
||||
-d 'binaryeye://scan/?ret=http%3A%2F%2Fmarkusfisch.de%2F%3Fresult%3D{RESULT}'
|
||||
|
||||
testurl:
|
||||
adb shell am start -a android.intent.action.VIEW \
|
||||
-c android.intent.category.BROWSABLE \
|
||||
-d 'http://markusfisch.de/BinaryEye?ret=http%3A%2F%2Fmarkusfisch.de%2F%3Fresult%3D{RESULT}'
|
||||
|
||||
install:
|
||||
adb $(TARGET) install -r app/build/outputs/apk/debug/app-debug.apk
|
||||
|
||||
@@ -4,3 +4,57 @@ 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.
|
||||
|
||||
## Permissions
|
||||
|
||||
### [ACCESS_FINE_LOCATION][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][CAMERA]
|
||||
|
||||
Required to read a barcode from the camera image.
|
||||
|
||||
### [INTERNET][INTERNET]
|
||||
|
||||
Required to send a scanned code to a custom URL you can specify in settings.
|
||||
|
||||
### [VIBRATE][VIBRATE]
|
||||
|
||||
Required to vibrate on detection.
|
||||
|
||||
### [WRITE_EXTERNAL_STORAGE][WRITE_EXTERNAL_STORAGE]
|
||||
|
||||
Required to save generated barcodes as files before Android Q.
|
||||
|
||||
### [CHANGE_WIFI_STATE][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][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`.
|
||||
|
||||
[ACCESS_FINE_LOCATION]: https://developer.android.com/reference/android/Manifest.permission#ACCESS_FINE_LOCATION
|
||||
[CAMERA]: https://developer.android.com/reference/android/Manifest.permission#CAMERA
|
||||
[INTERNET]: https://developer.android.com/reference/android/Manifest.permission#INTERNET
|
||||
[VIBRATE]: https://developer.android.com/reference/android/Manifest.permission#VIBRATE
|
||||
[WRITE_EXTERNAL_STORAGE]: https://developer.android.com/reference/android/Manifest.permission#WRITE_EXTERNAL_STORAGE
|
||||
[CHANGE_WIFI_STATE]: https://developer.android.com/reference/android/Manifest.permission#CHANGE_WIFI_STATE
|
||||
[ACCESS_WIFI_STATE]: https://developer.android.com/reference/android/Manifest.permission#ACCESS_FINE_LOCATION
|
||||
[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)
|
||||
|
||||
@@ -7,14 +7,19 @@ This one is free, without any ads and open source.
|
||||
Works in portrait and landscape orientation, can read inverted codes,
|
||||
comes in Material Design and can also generate barcodes.
|
||||
|
||||
Binary Eye uses the [ZXing][zxing] ("Zebra Crossing") barcode scanning
|
||||
library.
|
||||
Binary Eye uses the [ZXing-C++][zxing_cpp] ("Zebra Crossing") barcode
|
||||
scanning library.
|
||||
|
||||
## 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
|
||||
|
||||
@@ -34,10 +39,10 @@ ZXing can read the following barcode formats:
|
||||
* [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)
|
||||
* [MAXICODE][maxicode] (partial)
|
||||
* [PDF417][pdf417]
|
||||
* [QR CODE][qr_code]
|
||||
* [Micro QR Code][micro_qr_code]
|
||||
* [RSS 14][rss]
|
||||
* [RSS EXPANDED][rss]
|
||||
* [UPC A][upc_a]
|
||||
@@ -59,37 +64,79 @@ ZXing can generate the following barcode formats:
|
||||
* [QR CODE][qr_code]
|
||||
* [UPC A][upc_a]
|
||||
|
||||
## RenderScript
|
||||
## Deep Links and Intents
|
||||
|
||||
This app uses [RenderScript][rs] to resize and rotate the camera image.
|
||||
Unfortunately, RenderScript has some nasty gotchas.
|
||||
### Deep Links
|
||||
|
||||
### RenderScript.forceCompat()
|
||||
You can invoke Binary Eye with a web URI intent from anything that can
|
||||
open URIs. There are two options:
|
||||
|
||||
It's necessary to call `RenderScript.forceCompat()` on some devices/roms.
|
||||
1. [binaryeye://scan](binaryeye://scan)
|
||||
2. [http(s)://markusfisch.de/BinaryEye](http://markusfisch.de/BinaryEye)
|
||||
|
||||
`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 you want to get the scanned contents, you can add a `ret` query
|
||||
argument with a (URL encoded) URI template. For example:
|
||||
|
||||
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.
|
||||
[http://markusfisch.de/BinaryEye?ret=http%3A%2F%2Fexample.com%2F%3Fresult%3D{RESULT}](http://markusfisch.de/BinaryEye?ret=http%3A%2F%2Fexample.com%2F%3Fresult%3D{RESULT})
|
||||
|
||||
Calling `RenderScript.forceCompat()` means the processing is done in
|
||||
software so you probably don't want to enable it by default.
|
||||
Supported symbols are:
|
||||
|
||||
### 2D barcodes
|
||||
* `RESULT` - scanned content
|
||||
* `RESULT_BYTES` - raw result as a hex string
|
||||
* `FORMAT` - barcode format
|
||||
* `META` - the meta data, if available
|
||||
|
||||
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.
|
||||
### SCAN Intent
|
||||
|
||||
This will make your app a bit simpler and saves you from compiling a
|
||||
custom RenderScript kernel for each architecture you want to support.
|
||||
You can also use Binary Eye from other apps by using the
|
||||
`com.google.zxing.client.android.SCAN` Intent with
|
||||
[startActivityForResult()][start_activity] like this:
|
||||
|
||||
```kotlin
|
||||
startActivityForResult(
|
||||
Intent("com.google.zxing.client.android.SCAN"),
|
||||
SOME_NUMBER
|
||||
)
|
||||
```
|
||||
|
||||
And process the result in [onActivityResult()][on_activity_result] of your
|
||||
`Activity`:
|
||||
|
||||
```kotlin
|
||||
override fun onActivityResult(
|
||||
requestCode: Int,
|
||||
resultCode: Int,
|
||||
data: Intent?
|
||||
) {
|
||||
when (requestCode) {
|
||||
SOME_NUMBER -> if (resultCode == RESULT_OK) {
|
||||
val result = data.getStringExtra("SCAN_RESULT")
|
||||
…
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
If you're using AndroidX, this would be the new,
|
||||
[recommended way][intent_result]:
|
||||
|
||||
```kotlin
|
||||
class YourActivity : Activity() {
|
||||
private val resultLauncher = registerForActivityResult(StartActivityForResult()) { result ->
|
||||
if (result.resultCode == Activity.RESULT_OK) {
|
||||
val scan = result.data?.getStringExtra("SCAN_RESULT")
|
||||
…
|
||||
}
|
||||
}
|
||||
|
||||
fun openScanner() {
|
||||
resultLauncher.launch(Intent("com.google.zxing.client.android.SCAN"))
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
[play]: https://play.google.com/store/search?q=barcode%20scanner&c=apps
|
||||
[zxing]: https://github.com/zxing/zxing
|
||||
[zxing_cpp]: https://github.com/zxing-cpp/zxing-cpp
|
||||
[kotlin]: http://kotlinlang.org/
|
||||
[aztec]: https://en.wikipedia.org/wiki/Aztec_Code
|
||||
[codabar]: https://en.wikipedia.org/wiki/Codabar
|
||||
@@ -103,9 +150,11 @@ custom RenderScript kernel for each architecture you want to support.
|
||||
[maxicode]: https://en.wikipedia.org/wiki/MaxiCode
|
||||
[pdf417]: https://en.wikipedia.org/wiki/PDF417
|
||||
[qr_code]: https://en.wikipedia.org/wiki/QR_code
|
||||
[micro_qr_code]: https://en.wikipedia.org/wiki/QR_code#Micro_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
|
||||
[start_activity]: https://developer.android.com/reference/android/app/Activity#startActivityForResult(android.content.Intent,%20int)
|
||||
[on_activity_result]: https://developer.android.com/reference/android/app/Activity#onActivityResult(int,%20int,%20android.content.Intent)
|
||||
[intent_result]: https://developer.android.com/training/basics/intents/result
|
||||
|
||||
@@ -2,27 +2,19 @@ apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
|
||||
android {
|
||||
namespace 'de.markusfisch.android.binaryeye'
|
||||
compileSdkVersion sdk_version
|
||||
buildToolsVersion build_tools_version
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion 9
|
||||
targetSdkVersion sdk_version
|
||||
|
||||
versionCode 72
|
||||
versionName '1.34.0'
|
||||
|
||||
// it's recommended to set this value to the lowest API level
|
||||
// able to provide all the functionality
|
||||
renderscriptTargetApi 24
|
||||
renderscriptSupportModeEnabled true
|
||||
renderscriptSupportModeBlasEnabled true
|
||||
|
||||
testInstrumentationRunner 'android.support.test.runner.AndroidJUnitRunner'
|
||||
versionCode 104
|
||||
versionName '1.54.1'
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
config {
|
||||
release {
|
||||
keyAlias System.getenv('ANDROID_KEY_ALIAS')
|
||||
keyPassword System.getenv('ANDROID_KEY_PASSWORD')
|
||||
storePassword System.getenv('ANDROID_STORE_PASSWORD')
|
||||
@@ -46,36 +38,33 @@ android {
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||
minifyEnabled true
|
||||
shrinkResources true
|
||||
signingConfig signingConfigs.config
|
||||
signingConfig signingConfigs.release
|
||||
}
|
||||
}
|
||||
|
||||
lintOptions {
|
||||
// because at the time of writing, even the latest version
|
||||
// of RenderScript is raising this error no matter if
|
||||
// renderscriptTargetApi matches targetSdkVersion
|
||||
disable 'GradleCompatible'
|
||||
bundle {
|
||||
language {
|
||||
// To make the app bundle contain all language resources
|
||||
// so the in-app language setting works. Another solution
|
||||
// would be to use the PlayCore API to download language
|
||||
// resources on demand, but it makes much more sense to
|
||||
// simply include the ~60 kb than adding fragile code and
|
||||
// dependencies.
|
||||
enableSplit = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
androidTestImplementation('com.android.support.test:runner:1.0.2') {
|
||||
exclude module: 'support-annotations'
|
||||
}
|
||||
androidTestImplementation('com.android.support.test:rules:1.0.2') {
|
||||
exclude module: 'support-annotations'
|
||||
}
|
||||
testImplementation 'junit:junit:4.12'
|
||||
testImplementation 'junit:junit:4.13.2'
|
||||
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.2"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.1"
|
||||
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.2.0'
|
||||
implementation 'com.github.markusfisch:ScalingImageView:1.4.0'
|
||||
implementation 'com.github.markusfisch:zxing-cpp:v1.4.0.4'
|
||||
}
|
||||
|
||||
@@ -9,4 +9,14 @@
|
||||
<issue id="UnusedResources">
|
||||
<ignore path="*/res/rs/debug/raw/bc*" />
|
||||
</issue>
|
||||
<!-- Android 12 requires apps that request ACCESS_FINE_LOCATION
|
||||
to also request ACCESS_COARSE_LOCATION. But this app doesn't
|
||||
request nor requires ACCESS_FINE_LOCATION above SDK 28. So
|
||||
this is a false positive. -->
|
||||
<issue id="CoarseFineLocation" severity="ignore"/>
|
||||
<!-- Androidx SplashScreen compat library would require minSdk 21,
|
||||
make the app unnecessarily larger and Android 12 automatically
|
||||
replaces our custom splash screen anyway, which is exactly what
|
||||
we want. So there is no reason to complain. -->
|
||||
<issue id="CustomSplashScreen" severity="ignore"/>
|
||||
</lint>
|
||||
|
||||
@@ -3,4 +3,5 @@
|
||||
-dontwarn android.support.v8.**
|
||||
|
||||
-keep class android.support.v7.widget.SearchView { *; }
|
||||
-keep class android.support.v8.renderscript.** { *; }
|
||||
|
||||
-keep class de.markusfisch.android.zxingcpp.** { *; }
|
||||
|
||||
|
Before Width: | Height: | Size: 97 KiB |
|
Before Width: | Height: | Size: 79 KiB |
|
Before Width: | Height: | Size: 88 KiB |
|
Before Width: | Height: | Size: 77 KiB |
|
Before Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 47 KiB |
@@ -1,103 +0,0 @@
|
||||
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 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)
|
||||
class PreprocessorTest {
|
||||
@Test
|
||||
fun processSamples() {
|
||||
val zxing = Zxing()
|
||||
val assets = InstrumentationRegistry.getInstrumentation()
|
||||
.context.assets
|
||||
val pattern = Pattern.compile(
|
||||
"[0-9]+-([0-9]+)deg.jpg"
|
||||
)
|
||||
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 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 preprocessor = Preprocessor(
|
||||
InstrumentationRegistry.getTargetContext(),
|
||||
frameWidth,
|
||||
frameHeight,
|
||||
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()
|
||||
|
||||
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
|
||||
@@ -4,35 +4,7 @@
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M52,-884.362h29v71h-29z"/>
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M52,-884.362h29v71h-29z"/>
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M52,-884.362h29v71h-29z"/>
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M32.002,-24.974h3.127v7.656h-3.127z"/>
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M52,-884.362h29v71h-29z"/>
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M52,-884.362h29v71h-29z"/>
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M52,-884.362h29v71h-29z"/>
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M52,-884.362h29v71h-29z"/>
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M52,-884.362h29v71h-29z"/>
|
||||
<path
|
||||
android:pathData="m54.826,27.893c-6.851,0 -13.701,2.581 -18.924,7.745 -10.445,10.328 -10.413,27.062 0,37.423l36.218,36.036 37.879,0.902 -0.07,-38.363 -36.18,-35.998c-5.207,-5.18 -12.073,-7.745 -18.924,-7.745z"
|
||||
android:strokeAlpha="1"
|
||||
android:pathData="M54.826,27.894C47.976,27.894 41.125,30.475 35.903,35.639C25.457,45.967 25.489,62.701 35.903,73.062L72.121,109.098L108,108L109.93,71.637L73.75,35.639C68.543,30.459 61.677,27.894 54.826,27.894Z"
|
||||
android:strokeLineJoin="miter"
|
||||
android:strokeWidth="8.60725021"
|
||||
android:fillColor="#000000"
|
||||
@@ -41,43 +13,25 @@
|
||||
android:fillAlpha="0.1254902"
|
||||
android:strokeLineCap="butt"/>
|
||||
<path
|
||||
android:pathData="M28.001,54.118a26.525,26.227 0,1 0,53.049 0a26.525,26.227 0,1 0,-53.049 0z"
|
||||
android:strokeAlpha="1"
|
||||
android:pathData="M27.475,54a26.525,26.5 0,1 0,53.05 0a26.525,26.5 0,1 0,-53.05 0z"
|
||||
android:strokeLineJoin="miter"
|
||||
android:strokeWidth="4"
|
||||
android:fillColor="#ffffff"
|
||||
android:strokeColor="#00000000"
|
||||
android:fillAlpha="1"
|
||||
android:strokeLineCap="butt"/>
|
||||
<path
|
||||
android:pathData="M39.26,54.988a14.509,14.346 0,1 0,29.019 0a14.509,14.346 0,1 0,-29.019 0z"
|
||||
android:strokeAlpha="1"
|
||||
android:pathData="M72.745,54.001C72.745,58.588 71.099,62.791 68.362,66.049L68.362,41.953C71.099,45.21 72.745,49.412 72.745,54.001ZM54.001,72.745C53.463,72.745 52.931,72.723 52.406,72.679L52.406,35.322C52.931,35.278 53.463,35.255 54.001,35.255C54.538,35.255 55.071,35.278 55.596,35.322L55.596,72.679C55.071,72.723 54.538,72.745 54.001,72.745ZM35.255,54.001C35.255,49.412 36.903,45.21 39.638,41.953L39.638,66.049C36.903,62.791 35.255,58.588 35.255,54.001ZM61.98,70.969C60.961,71.447 59.895,71.838 58.789,72.13L58.789,35.873C59.895,36.164 60.961,36.553 61.98,37.032L61.98,70.969ZM49.213,72.13C46.877,71.514 44.717,70.458 42.831,69.056L42.831,38.945C44.717,37.543 46.877,36.487 49.213,35.873L49.213,72.13Z"
|
||||
android:strokeLineJoin="miter"
|
||||
android:strokeWidth="4"
|
||||
android:fillColor="#000000"
|
||||
android:strokeColor="#00000000"
|
||||
android:fillAlpha="1"
|
||||
android:strokeLineCap="butt"/>
|
||||
<path
|
||||
android:pathData="M54.034,44.513a9.535,9.428 0,1 0,19.07 0a9.535,9.428 0,1 0,-19.07 0z"
|
||||
android:strokeAlpha="1"
|
||||
android:strokeLineJoin="miter"
|
||||
android:strokeWidth="10"
|
||||
android:fillColor="#ffffff"
|
||||
android:strokeColor="#00000000"
|
||||
android:fillAlpha="1"
|
||||
android:strokeLineCap="butt"/>
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M52,-884.362h29v71h-29z"/>
|
||||
<path
|
||||
android:pathData="m62.745,24.783c-2.055,1.011 -2.87,3.415 -1.847,5.446 0.588,1.168 1.642,1.916 2.839,2.168 -0.564,0.884 -0.835,1.972 -0.925,3.156 -1.782,-0.45 -3.735,-0.191 -5.25,0.639a0.799,0.808 90,0 0,-0.132 0.172,0.799 0.808,90 0,0 -0.132,0.172 0.799,0.808 90,0 0,-0.132 0.39,0.799 0.808,90 0,0 0.044,0.304 0.799,0.808 90,0 0,0.437 0.433,0.799 0.808,90 0,0 0.131,0.043 0.799,0.808 90,0 0,0.131 0.043,0.799 0.808,90 0,0 0.218,-0 0.799,0.808 90,0 0,0.263 -0.129c1.29,-0.707 2.97,-0.892 4.375,-0.425 0.096,0.882 0.341,1.782 0.695,2.683 -0.456,0.072 -0.936,0.177 -1.357,0.343 -1.207,0.48 -2.245,1.302 -3.022,2.244a0.799,0.808 90,0 0,-0.044 0.129,0.799 0.808,90 0,0 -0.088,0.258 0.799,0.808 90,0 0,0.044 0.304,0.799 0.808,90 0,0 0.393,0.563 0.799,0.808 90,0 0,0.394 0.131,0.799 0.808,90 0,0 0.175,-0.086 0.799,0.808 90,0 0,0.175 -0.086,0.799 0.808,90 0,0 0.219,-0.215c0.922,-1.115 2.409,-1.925 3.897,-2.025 0.494,0.845 1.016,1.575 1.658,2.209 -1.232,1.149 -2.043,2.664 -2.239,4.234a0.799,0.808 90,0 0,-0.001 0.215,0.799 0.808,90 0,0 0.174,0.346 0.799,0.808 90,0 0,0.043 0.086,0.799 0.808,90 0,0 0.219,0.217 0.799,0.808 90,0 0,0.131 0.043,0.799 0.808,90 0,0 0.131,0.043 0.799,0.808 90,0 0,0.218 -0,0.799 0.808,90 0,0 0.218,-0 0.799,0.808 90,0 0,0.132 -0.172,0.799 0.808,90 0,0 0.175,-0.086 0.799,0.808 90,0 0,0.044 -0.129,0.799 0.808,90 0,0 0.088,-0.258 0.799,0.808 90,0 0,0.044 -0.129c0.159,-1.272 0.816,-2.493 1.799,-3.371 1.966,1.394 4.252,1.797 6.122,0.876 1.892,-0.931 2.903,-3.018 2.941,-5.444 1.327,-0.259 2.813,0.014 3.936,0.656a0.799,0.808 90,0 0,0.174 0.129,0.799 0.808,90 0,0 0.131,0.043 0.799,0.808 90,0 0,0.088 -0.043,0.799 0.808,90 0,0 0.218,-0 0.799,0.808 90,0 0,0.087 -0.043,0.799 0.808,90 0,0 0.175,-0.086 0.799,0.808 90,0 0,0.132 -0.172,0.799 0.808,90 0,0 0.132,-0.172 0.799,0.808 90,0 0,0.045 -0.562,0.799 0.808,90 0,0 -0.043,-0.086 0.799,0.808 90,0 0,-0.262 -0.303,0.799 0.808,90 0,0 -0.131,-0.043c-1.386,-0.792 -3.097,-1.066 -4.766,-0.787 -0.125,-0.89 -0.309,-1.783 -0.695,-2.683 0.982,-1.073 2.47,-1.756 3.896,-1.81a0.799,0.808 90,0 0,0.131 0.043,0.799 0.808,90 0,0 0.087,-0.043 0.799,0.808 90,0 0,0.482 -0.345,0.799 0.808,90 0,0 0.044,-0.129 0.799,0.808 90,0 0,0.088 -0.476,0.799 0.808,90 0,0 -0.043,-0.086 0.799,0.808 90,0 0,-0.262 -0.303,0.799 0.808,90 0,0 -0.043,-0.086 0.799,0.808 90,0 0,-0.394 -0.131,0.799 0.808,90 0,0 -0.131,-0.043c-1.227,0.048 -2.518,0.369 -3.633,1.031 -0.388,0.233 -0.762,0.548 -1.095,0.864 -0.513,-0.821 -1.093,-1.559 -1.746,-2.166 0.47,-1.39 1.632,-2.605 2.98,-3.195a0.799,0.808 90,0 0,0.175 -0.086,0.799 0.808,90 0,0 0.132,-0.172 0.799,0.808 90,0 0,0.132 -0.172,0.799 0.808,90 0,0 0.132,-0.39 0.799,0.808 90,0 0,-0.043 -0.086,0.799 0.808,90 0,0 -0.174,-0.346 0.799,0.808 90,0 0,-0.043 -0.086,0.799 0.808,90 0,0 -0.437,-0.217 0.799,0.808 90,0 0,-0.175 0.086,0.799 0.808,90 0,0 -0.131,-0.043 0.799,0.808 90,0 0,-0.087 0.043,0.799 0.808,90 0,0 -0.131,-0.043c-1.584,0.694 -2.97,2.079 -3.681,3.756 -1.012,-0.644 -2.048,-1.087 -3.104,-1.173 0.53,-1.101 0.642,-2.414 0.05,-3.59 -1.023,-2.031 -3.541,-2.795 -5.596,-1.783z"
|
||||
android:pathData="m62.745,28.783c-2.055,1.011 -2.87,3.415 -1.847,5.446 0.588,1.168 1.642,1.916 2.839,2.168 -0.564,0.884 -0.835,1.972 -0.925,3.156 -1.782,-0.45 -3.735,-0.191 -5.25,0.639a0.799,0.808 90.001,0 0,-0.132 0.172,0.799 0.808,90.001 0,0 -0.132,0.172 0.799,0.808 90.001,0 0,-0.132 0.39,0.799 0.808,90.001 0,0 0.044,0.304 0.799,0.808 90.001,0 0,0.437 0.433,0.799 0.808,90.001 0,0 0.131,0.043 0.799,0.808 90.001,0 0,0.131 0.043,0.799 0.808,90.001 0,0 0.218,-0 0.799,0.808 90.001,0 0,0.263 -0.129c1.29,-0.707 2.97,-0.892 4.375,-0.425 0.096,0.882 0.341,1.782 0.695,2.683 -0.456,0.072 -0.936,0.177 -1.357,0.343 -1.207,0.48 -2.245,1.302 -3.022,2.244a0.799,0.808 90.001,0 0,-0.044 0.129,0.799 0.808,90.001 0,0 -0.088,0.258 0.799,0.808 90.001,0 0,0.044 0.304,0.799 0.808,90.001 0,0 0.393,0.563 0.799,0.808 90.001,0 0,0.394 0.131,0.799 0.808,90.001 0,0 0.175,-0.086 0.799,0.808 90.001,0 0,0.175 -0.086,0.799 0.808,90.001 0,0 0.219,-0.215c0.922,-1.115 2.409,-1.925 3.897,-2.025 0.494,0.845 1.016,1.575 1.658,2.209 -1.232,1.149 -2.043,2.664 -2.239,4.234a0.799,0.808 90.001,0 0,-0.001 0.215,0.799 0.808,90.001 0,0 0.174,0.346 0.799,0.808 90.001,0 0,0.043 0.086,0.799 0.808,90.001 0,0 0.219,0.217 0.799,0.808 90.001,0 0,0.131 0.043,0.799 0.808,90.001 0,0 0.131,0.043 0.799,0.808 90.001,0 0,0.218 -0,0.799 0.808,90.001 0,0 0.218,-0 0.799,0.808 90.001,0 0,0.132 -0.172,0.799 0.808,90.001 0,0 0.175,-0.086 0.799,0.808 90.001,0 0,0.044 -0.129,0.799 0.808,90.001 0,0 0.088,-0.258 0.799,0.808 90.001,0 0,0.044 -0.129c0.159,-1.272 0.816,-2.493 1.799,-3.371 1.966,1.394 4.252,1.797 6.122,0.876 1.892,-0.931 2.903,-3.018 2.941,-5.444 1.327,-0.259 2.813,0.014 3.936,0.656a0.799,0.808 90.001,0 0,0.174 0.129,0.799 0.808,90.001 0,0 0.131,0.043 0.799,0.808 90.001,0 0,0.088 -0.043,0.799 0.808,90.001 0,0 0.218,-0 0.799,0.808 90.001,0 0,0.087 -0.043,0.799 0.808,90.001 0,0 0.175,-0.086 0.799,0.808 90.001,0 0,0.132 -0.172,0.799 0.808,90.001 0,0 0.132,-0.172 0.799,0.808 90.001,0 0,0.045 -0.562,0.799 0.808,90.001 0,0 -0.043,-0.086 0.799,0.808 90.001,0 0,-0.262 -0.303,0.799 0.808,90.001 0,0 -0.131,-0.043c-1.386,-0.792 -3.097,-1.066 -4.766,-0.787 -0.125,-0.89 -0.309,-1.783 -0.695,-2.683 0.982,-1.073 2.47,-1.756 3.896,-1.81a0.799,0.808 90.001,0 0,0.131 0.043,0.799 0.808,90.001 0,0 0.087,-0.043 0.799,0.808 90.001,0 0,0.482 -0.345,0.799 0.808,90.001 0,0 0.044,-0.129 0.799,0.808 90.001,0 0,0.088 -0.476,0.799 0.808,90.001 0,0 -0.043,-0.086 0.799,0.808 90.001,0 0,-0.262 -0.303,0.799 0.808,90.001 0,0 -0.043,-0.086 0.799,0.808 90.001,0 0,-0.394 -0.131,0.799 0.808,90.001 0,0 -0.131,-0.043c-1.227,0.048 -2.518,0.369 -3.633,1.031 -0.388,0.233 -0.762,0.548 -1.095,0.864 -0.513,-0.821 -1.093,-1.559 -1.746,-2.166 0.47,-1.39 1.632,-2.605 2.98,-3.195a0.799,0.808 90.001,0 0,0.175 -0.086,0.799 0.808,90.001 0,0 0.132,-0.172 0.799,0.808 90.001,0 0,0.132 -0.172,0.799 0.808,90.001 0,0 0.132,-0.39 0.799,0.808 90.001,0 0,-0.043 -0.086,0.799 0.808,90.001 0,0 -0.174,-0.346 0.799,0.808 90.001,0 0,-0.043 -0.086,0.799 0.808,90.001 0,0 -0.437,-0.217 0.799,0.808 90.001,0 0,-0.175 0.086,0.799 0.808,90.001 0,0 -0.131,-0.043 0.799,0.808 90.001,0 0,-0.087 0.043,0.799 0.808,90.001 0,0 -0.131,-0.043c-1.584,0.694 -2.97,2.079 -3.681,3.756 -1.012,-0.644 -2.048,-1.087 -3.104,-1.173 0.53,-1.101 0.642,-2.414 0.05,-3.59 -1.023,-2.031 -3.541,-2.795 -5.596,-1.783z"
|
||||
android:fillColor="#ff2a2a"
|
||||
android:strokeColor="#00000000"
|
||||
android:fillAlpha="1"/>
|
||||
android:strokeColor="#00000000"/>
|
||||
<path
|
||||
android:pathData="m66.624,32.489c0.281,-0.138 1.896,2.465 3.62,5.889 1.724,3.424 2.893,6.354 2.613,6.492 -0.281,0.138 -1.94,-2.551 -3.663,-5.975 -1.724,-3.424 -2.85,-6.268 -2.569,-6.406z"
|
||||
android:pathData="m66.624,36.489c0.281,-0.138 1.896,2.465 3.62,5.889 1.724,3.424 2.893,6.354 2.613,6.492 -0.281,0.138 -1.94,-2.551 -3.663,-5.975 -1.724,-3.424 -2.85,-6.268 -2.569,-6.406z"
|
||||
android:fillColor="#d40000"
|
||||
android:strokeColor="#00000000"
|
||||
android:fillAlpha="1"/>
|
||||
android:strokeColor="#00000000"/>
|
||||
</vector>
|
||||
|
||||
|
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 5.5 KiB After Width: | Height: | Size: 5.4 KiB |
|
Before Width: | Height: | Size: 8.5 KiB After Width: | Height: | Size: 8.2 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
@@ -1,26 +1,39 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="de.markusfisch.android.binaryeye"
|
||||
android:installLocation="auto">
|
||||
<supports-screens
|
||||
android:largeScreens="true"
|
||||
android:xlargeScreens="true"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"
|
||||
android:maxSdkVersion="28"/>
|
||||
<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.WRITE_EXTERNAL_STORAGE"
|
||||
android:maxSdkVersion="28"/>
|
||||
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"
|
||||
android:maxSdkVersion="28"/>
|
||||
<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="@xml/backup_rules"
|
||||
android:dataExtractionRules="@xml/extraction_rules"
|
||||
android:supportsRtl="true"
|
||||
android:resizeableActivity="true"
|
||||
android:label="@string/app_name"
|
||||
@@ -28,8 +41,8 @@
|
||||
android:theme="@style/AppTheme">
|
||||
<activity
|
||||
android:name=".activity.SplashActivity"
|
||||
android:label="@string/app_name"
|
||||
android:theme="@style/SplashTheme">
|
||||
android:theme="@style/SplashTheme"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
@@ -40,7 +53,8 @@
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".activity.CameraActivity"
|
||||
android:label="@string/scan_code">
|
||||
android:label="@string/scan_code"
|
||||
android:exported="true">
|
||||
<intent-filter android:label="@string/compose_barcode">
|
||||
<action android:name="android.intent.action.SEND"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
@@ -54,15 +68,22 @@
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<category android:name="android.intent.category.BROWSABLE"/>
|
||||
<data
|
||||
android:host="www.markusfisch.de"
|
||||
android:pathPrefix="/BinaryEye"
|
||||
android:scheme="http"/>
|
||||
<data android:host="markusfisch.de"/>
|
||||
<data android:pathPrefix="/BinaryEye"/>
|
||||
<data android:scheme="http"/>
|
||||
<data android:scheme="https"/>
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<category android:name="android.intent.category.BROWSABLE"/>
|
||||
<data android:scheme="binaryeye" android:host="scan"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".activity.PickActivity"
|
||||
android:label="@string/pick_code_to_scan"
|
||||
android:exported="true"
|
||||
tools:ignore="AppLinkUrlError">
|
||||
<intent-filter android:label="@string/decode_barcode">
|
||||
<action android:name="android.intent.action.SEND"/>
|
||||
@@ -77,8 +98,17 @@
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".activity.MainActivity"
|
||||
android:label="@string/app_name"
|
||||
android:exported="true"/>
|
||||
<service
|
||||
android:name=".service.ScanTileService"
|
||||
android:icon="@drawable/ic_action_scan"
|
||||
android:label="@string/scan_code"
|
||||
android:exported="true"
|
||||
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
|
||||
<intent-filter>
|
||||
<action android:name="android.service.quicksettings.action.QS_TILE"/>
|
||||
</intent-filter>
|
||||
</service>
|
||||
<provider
|
||||
android:name="android.support.v4.content.FileProvider"
|
||||
android:authorities="${applicationId}.provider"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package de.markusfisch.android.binaryeye.actions
|
||||
|
||||
import de.markusfisch.android.binaryeye.actions.mail.MailAction
|
||||
import de.markusfisch.android.binaryeye.actions.mail.MatMsgAction
|
||||
import de.markusfisch.android.binaryeye.actions.otpauth.OtpauthAction
|
||||
import de.markusfisch.android.binaryeye.actions.search.OpenOrSearchAction
|
||||
import de.markusfisch.android.binaryeye.actions.sms.SmsAction
|
||||
@@ -15,13 +16,16 @@ object ActionRegistry {
|
||||
|
||||
private val REGISTRY: Set<IAction> = setOf(
|
||||
MailAction,
|
||||
MatMsgAction,
|
||||
OtpauthAction,
|
||||
SmsAction,
|
||||
TelAction,
|
||||
VCardAction,
|
||||
VEventAction,
|
||||
WebAction,
|
||||
WifiAction
|
||||
WifiAction,
|
||||
// Try WebAction last because recognizing colloquial URLs is
|
||||
// very aggressive.
|
||||
WebAction
|
||||
)
|
||||
|
||||
fun getAction(data: ByteArray): IAction = REGISTRY.find {
|
||||
|
||||
@@ -2,8 +2,8 @@ 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.content.execShareIntent
|
||||
import de.markusfisch.android.binaryeye.content.openUrl
|
||||
import de.markusfisch.android.binaryeye.widget.toast
|
||||
|
||||
interface IAction {
|
||||
@@ -18,31 +18,38 @@ 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)
|
||||
val intent = createIntent(context, data)
|
||||
if (intent == null) {
|
||||
context.toast(errorMsg)
|
||||
} else {
|
||||
context.execShareIntent(intent)
|
||||
}
|
||||
}
|
||||
|
||||
abstract suspend fun createIntent(context: Context, data: ByteArray): 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))
|
||||
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))
|
||||
context.openUrl(String(data))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
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 MatMsgAction : IntentAction() {
|
||||
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).startsWith("MATMSG:")
|
||||
}
|
||||
|
||||
override suspend fun createIntent(
|
||||
context: Context,
|
||||
data: ByteArray
|
||||
): Intent? {
|
||||
val mm = MatMsg(String(data))
|
||||
// Allow incomplete but not completely missing data.
|
||||
if (mm.isEmpty()) {
|
||||
return null
|
||||
}
|
||||
return Intent(
|
||||
Intent.ACTION_SENDTO,
|
||||
Uri.parse("mailto:")
|
||||
).apply {
|
||||
mm.to?.let {
|
||||
putExtra(Intent.EXTRA_EMAIL, arrayOf(mm.to))
|
||||
}
|
||||
mm.sub?.let {
|
||||
putExtra(Intent.EXTRA_SUBJECT, mm.sub)
|
||||
}
|
||||
mm.body?.let {
|
||||
putExtra(Intent.EXTRA_TEXT, mm.body)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Public so this can be tested.
|
||||
class MatMsg(encoded: String) {
|
||||
// Allow arbitrary order.
|
||||
val to = encoded.extractFirst("""[:;]TO:([\w.%@+-]+);""")
|
||||
val sub = encoded.extractFirst("""[:;]SUB:([^;]+);""")
|
||||
val body = encoded.extractFirst("""[:;]BODY:([^;]+);""")
|
||||
|
||||
fun isEmpty() = to == null && sub == null && body == null
|
||||
}
|
||||
|
||||
private fun String.extractFirst(regex: String) =
|
||||
regex.toRegex().find(this)?.groupValues?.get(1)
|
||||
@@ -1,15 +1,12 @@
|
||||
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
|
||||
import de.markusfisch.android.binaryeye.content.openUrl
|
||||
import de.markusfisch.android.binaryeye.net.urlEncode
|
||||
|
||||
object OpenOrSearchAction : IAction {
|
||||
override val iconResId: Int = R.drawable.ic_action_search
|
||||
@@ -18,28 +15,39 @@ object OpenOrSearchAction : IAction {
|
||||
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)
|
||||
view(context, String(data), true)
|
||||
}
|
||||
|
||||
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)
|
||||
private suspend fun view(
|
||||
context: Context,
|
||||
url: String,
|
||||
search: Boolean
|
||||
) {
|
||||
if (!context.openUrl(url) && search) {
|
||||
openSearch(context, url)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private suspend fun getSearchIntent(context: Context, query: String): Intent? {
|
||||
private suspend fun openSearch(context: Context, query: String) {
|
||||
val defaultSearchUrl = prefs.defaultSearchUrl
|
||||
if (defaultSearchUrl.isNotEmpty()) {
|
||||
view(
|
||||
context,
|
||||
defaultSearchUrl + query.urlEncode(),
|
||||
false
|
||||
)
|
||||
return
|
||||
}
|
||||
val names = context.resources.getStringArray(
|
||||
R.array.search_engines_names
|
||||
).toMutableList()
|
||||
val urls = context.resources.getStringArray(
|
||||
R.array.search_engines_values
|
||||
).toMutableList()
|
||||
// Remove the "Always ask" entry. The arrays search_engines_*
|
||||
// are used in the preferences too.
|
||||
names.removeFirst()
|
||||
urls.removeFirst()
|
||||
if (prefs.openWithUrl.isNotEmpty()) {
|
||||
names.add(prefs.openWithUrl)
|
||||
urls.add(prefs.openWithUrl)
|
||||
@@ -47,9 +55,9 @@ object OpenOrSearchAction : IAction {
|
||||
val queryUri = alertDialog<String>(context) { resume ->
|
||||
setTitle(R.string.pick_search_engine)
|
||||
setItems(names.toTypedArray()) { _, which ->
|
||||
resume(urls[which] + URLEncoder.encode(query, "utf-8"))
|
||||
resume(urls[which] + query.urlEncode())
|
||||
}
|
||||
} ?: return null
|
||||
return openUri(context, queryUri, false)
|
||||
} ?: return
|
||||
view(context, queryUri, false)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,8 +7,9 @@ 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)
|
||||
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
|
||||
@@ -28,7 +29,9 @@ object SmsAction : IntentAction() {
|
||||
Intent.ACTION_SENDTO,
|
||||
Uri.parse("smsto:$number")
|
||||
).apply {
|
||||
if (message.isNotEmpty()) putExtra("sms_body", message)
|
||||
if (message.isNotEmpty()) {
|
||||
putExtra("sms_body", message)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,10 +4,12 @@ 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
|
||||
import de.markusfisch.android.binaryeye.content.parseAndNormalizeUri
|
||||
|
||||
object TelAction : IntentAction() {
|
||||
private val telRegex = """^tel:(\+?[0-9]+)$""".toRegex(RegexOption.IGNORE_CASE)
|
||||
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
|
||||
@@ -17,7 +19,11 @@ object TelAction : IntentAction() {
|
||||
return String(data).matches(telRegex)
|
||||
}
|
||||
|
||||
override suspend fun createIntent(context: Context, data: ByteArray): Intent {
|
||||
return Intent(Intent.ACTION_DIAL, parseAndNormalizeUri(String(data)))
|
||||
}
|
||||
override suspend fun createIntent(
|
||||
context: Context,
|
||||
data: ByteArray
|
||||
): Intent = Intent(
|
||||
Intent.ACTION_DIAL,
|
||||
String(data).parseAndNormalizeUri()
|
||||
)
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ object VTypeParser {
|
||||
|
||||
fun parseVType(data: String): String? = vTypeRegex.matchEntire(
|
||||
data
|
||||
)?.groupValues?.get(1)?.toUpperCase(Locale.US)
|
||||
)?.groupValues?.get(1)?.uppercase(Locale.US)
|
||||
|
||||
fun parseMap(
|
||||
data: String
|
||||
@@ -26,7 +26,7 @@ object VTypeParser {
|
||||
it.groupValues[2]
|
||||
)
|
||||
}
|
||||
}.groupBy({ it.first.toUpperCase(Locale.US) }) { it.second }
|
||||
}.groupBy({ it.first.uppercase(Locale.US) }) { it.second }
|
||||
}
|
||||
|
||||
data class VTypeProperty(
|
||||
|
||||
@@ -9,7 +9,6 @@ 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
|
||||
@@ -20,10 +19,19 @@ object VCardAction : IntentAction() {
|
||||
get() = R.string.vcard_failed
|
||||
|
||||
override fun canExecuteOn(data: ByteArray): Boolean {
|
||||
return VTypeParser.parseVType(String(data)) == "VCARD"
|
||||
var stringData = String(data)
|
||||
// Quick & dirty MECARD support: simply make it a VCARD.
|
||||
if (stringData.startsWith("MECARD:")) {
|
||||
stringData = stringData
|
||||
.trim()
|
||||
.replace("MECARD:", "BEGIN:VCARD\n")
|
||||
.replace(";;", "\nEND:VCARD\n")
|
||||
.replace(";", "\n")
|
||||
}
|
||||
return VTypeParser.parseVType(stringData) == "VCARD"
|
||||
}
|
||||
|
||||
override suspend fun createIntent(context: Context, data: ByteArray): Intent? {
|
||||
override suspend fun createIntent(context: Context, data: ByteArray): Intent {
|
||||
val info = VTypeParser.parseMap(String(data))
|
||||
|
||||
return Intent(Intent.ACTION_INSERT_OR_EDIT).apply {
|
||||
@@ -50,7 +58,7 @@ object VCardAction : IntentAction() {
|
||||
}
|
||||
putExtra(
|
||||
extraPhone,
|
||||
phoneProperty.value.toLowerCase(Locale.US).removePrefix("tel:")
|
||||
phoneProperty.value.lowercase(Locale.US).removePrefix("tel:")
|
||||
)
|
||||
}
|
||||
info["EMAIL"]?.forEachIndexed { index, mailProperty ->
|
||||
@@ -142,7 +150,7 @@ object VCardAction : IntentAction() {
|
||||
} ?: this
|
||||
|
||||
private val String.mailType: Int?
|
||||
get() = when (this.toUpperCase(Locale.US)) {
|
||||
get() = when (this.uppercase(Locale.US)) {
|
||||
"HOME" -> ContactsContract.CommonDataKinds.Email.TYPE_HOME
|
||||
"WORK" -> ContactsContract.CommonDataKinds.Email.TYPE_WORK
|
||||
"OTHER" -> ContactsContract.CommonDataKinds.Email.TYPE_OTHER
|
||||
@@ -151,7 +159,7 @@ object VCardAction : IntentAction() {
|
||||
}
|
||||
|
||||
private val String.phoneType: Int?
|
||||
get() = when (this.toUpperCase(Locale.US)) {
|
||||
get() = when (this.uppercase(Locale.US)) {
|
||||
"HOME" -> ContactsContract.CommonDataKinds.Phone.TYPE_HOME
|
||||
"MOBILE", "CELL" -> ContactsContract.CommonDataKinds.Phone.TYPE_MOBILE
|
||||
"WORK" -> ContactsContract.CommonDataKinds.Phone.TYPE_WORK
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package de.markusfisch.android.binaryeye.actions.vtype.vevent
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
@@ -45,37 +44,49 @@ object VEventAction : IntentAction() {
|
||||
}
|
||||
info["DTSTART"]?.singleOrNull()?.also { eventStart ->
|
||||
dateFormats.simpleFindParse(eventStart.value)?.also {
|
||||
putExtra(CalendarContract.EXTRA_EVENT_BEGIN_TIME, it.time)
|
||||
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)
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
private val dateFormats = listOf(
|
||||
"yyyy-MM-dd'T'HH:mm:ssXXX",
|
||||
"yyyy-MM-dd'T'HH:mm:ssZ",
|
||||
"yyyy-MM-dd'T'HH:mm:ssz",
|
||||
"yyyy-MM-dd'T'HH:mm:ss",
|
||||
"yyyyMMdd'T'HHmmssXXX",
|
||||
"yyyyMMdd'T'HHmmssZ",
|
||||
"yyyyMMdd'T'HHmmssz",
|
||||
"yyyyMMdd'T'HHmmss",
|
||||
"yyyy-MM-dd",
|
||||
"yyyyMMdd"
|
||||
)
|
||||
|
||||
private fun List<String>.simpleFindParse(date: String): Date? {
|
||||
for (pattern in this) {
|
||||
return SimpleDateFormat(
|
||||
pattern,
|
||||
Locale.getDefault()
|
||||
).simpleParse(date) ?: continue
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun SimpleDateFormat.simpleParse(date: String): Date? = try {
|
||||
parse(date)
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
|
||||
@@ -1,11 +1,28 @@
|
||||
package de.markusfisch.android.binaryeye.actions.web
|
||||
|
||||
import android.content.Context
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.actions.SchemeAction
|
||||
import de.markusfisch.android.binaryeye.actions.IAction
|
||||
import de.markusfisch.android.binaryeye.content.openUrl
|
||||
|
||||
object WebAction : IAction {
|
||||
private val colloquialRegex =
|
||||
"""^(http[s]*://)*([a-z0-9]+[a-z0-9-]*[a-z0-9]+[.])+[a-z]{2,}([?#/]+[\w/=&;._-]*)*$""".toRegex(
|
||||
RegexOption.IGNORE_CASE
|
||||
)
|
||||
|
||||
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
|
||||
|
||||
override fun canExecuteOn(data: ByteArray): Boolean {
|
||||
return String(data).trim().matches(colloquialRegex)
|
||||
}
|
||||
|
||||
override suspend fun execute(context: Context, data: ByteArray) {
|
||||
var url = String(data).trim()
|
||||
if (!url.startsWith("http") && !url.startsWith("ftp")) {
|
||||
url = "http://${url}"
|
||||
}
|
||||
context.openUrl(url)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,12 @@ import android.support.annotation.RequiresApi
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import java.util.*
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.Q)
|
||||
fun removeNetworkSuggestions(context: Context): Int =
|
||||
(context.applicationContext.getSystemService(
|
||||
Context.WIFI_SERVICE
|
||||
) as WifiManager).removeNetworkSuggestions(listOf())
|
||||
|
||||
/**
|
||||
* Normal:
|
||||
* WIFI:S:[network SSID];T:<WPA|WEP|nopass|>;P:[network password];H:<true|false|>;;
|
||||
@@ -45,7 +51,7 @@ object WifiConnector {
|
||||
invoke(parsedData.password)
|
||||
}
|
||||
return if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
||||
// WifiConfiguration is deprecated in Android Q
|
||||
// WifiConfiguration is deprecated in Android Q.
|
||||
@Suppress("DEPRECATION")
|
||||
WifiConfiguration().apply(parsedData)
|
||||
} else {
|
||||
@@ -58,7 +64,7 @@ object WifiConnector {
|
||||
Context.WIFI_SERVICE
|
||||
) as WifiManager
|
||||
return if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
||||
// WifiConfiguration is deprecated in Android Q
|
||||
// WifiConfiguration is deprecated in Android Q.
|
||||
@Suppress("DEPRECATION")
|
||||
val wifiConfig = config as WifiConfiguration
|
||||
if (wifiManager.enableWifi() &&
|
||||
@@ -72,7 +78,7 @@ object WifiConnector {
|
||||
} else {
|
||||
val suggestion = (config as WifiNetworkSuggestion.Builder).build()
|
||||
val suggestions = listOf(suggestion)
|
||||
// remove previous conflicting network suggestion
|
||||
// Remove previous conflicting network suggestion.
|
||||
wifiManager.removeNetworkSuggestions(suggestions)
|
||||
val result = wifiManager.addNetworkSuggestions(suggestions)
|
||||
if (result == WifiManager.STATUS_NETWORK_SUGGESTIONS_SUCCESS) {
|
||||
@@ -84,17 +90,17 @@ object WifiConnector {
|
||||
}
|
||||
|
||||
internal fun parseMap(string: String): Map<String, String>? {
|
||||
// normally those codes should have the last semicolon, but many
|
||||
// generators don't add it
|
||||
// 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
|
||||
// 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]
|
||||
pair.groupValues[1].uppercase(Locale.US) to pair.groupValues[2]
|
||||
}.toMap()
|
||||
}
|
||||
}
|
||||
@@ -102,7 +108,7 @@ object WifiConnector {
|
||||
internal class SimpleDataAccessor private constructor(
|
||||
private val inputMap: Map<String, String>
|
||||
) {
|
||||
// this should be private but because of testing it isn't possible
|
||||
// This should be private but because of testing it isn't possible.
|
||||
internal val ssid: String
|
||||
get() = inputMap.getValue("S").unescape
|
||||
internal val securityType: String
|
||||
@@ -112,7 +118,7 @@ object WifiConnector {
|
||||
internal val hidden: Boolean
|
||||
get() = inputMap["H"]?.unescape == "true"
|
||||
internal val anonymousIdentity: String
|
||||
get() = inputMap["AI"]?.unescape ?: ""
|
||||
get() = inputMap["AI"]?.unescape ?: inputMap["A"]?.unescape ?: ""
|
||||
internal val identity: String
|
||||
get() = inputMap["I"]?.unescape ?: ""
|
||||
internal val eapMethod: Int?
|
||||
@@ -164,7 +170,7 @@ object WifiConnector {
|
||||
): WifiNetworkSuggestion.Builder? {
|
||||
fun WifiNetworkSuggestion.Builder.applyCommon(
|
||||
data: SimpleDataAccessor
|
||||
): WifiNetworkSuggestion.Builder? {
|
||||
): WifiNetworkSuggestion.Builder {
|
||||
setSsid(data.ssid)
|
||||
return this
|
||||
}
|
||||
@@ -172,47 +178,44 @@ object WifiConnector {
|
||||
fun WifiNetworkSuggestion.Builder.applySecurity(
|
||||
data: SimpleDataAccessor
|
||||
): WifiNetworkSuggestion.Builder? {
|
||||
when (data.securityType) {
|
||||
"WPA", "WPA2" -> {
|
||||
data.password?.let {
|
||||
setWpa2Passphrase(it)
|
||||
try {
|
||||
when (data.securityType) {
|
||||
"WPA", "WPA2" -> {
|
||||
data.password?.let { setWpa2Passphrase(it) }
|
||||
}
|
||||
"WPA2-EAP" -> {
|
||||
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" -> {
|
||||
setWpa3EnterpriseConfig(WifiEnterpriseConfig().apply {
|
||||
identity = data.identity
|
||||
anonymousIdentity = data.anonymousIdentity
|
||||
password = data.password
|
||||
data.eapMethod?.let { eapMethod = it }
|
||||
data.phase2Method?.let { phase2Method = 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 }
|
||||
})
|
||||
}
|
||||
} catch (e: IllegalArgumentException) {
|
||||
// Don't accept passphrases that aren't ASCII encodeable.
|
||||
return null
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
return this.applyCommon(data)?.applySecurity(data)
|
||||
return this.applyCommon(data).applySecurity(data)
|
||||
}
|
||||
|
||||
// WifiConfiguration is deprecated in Android Q
|
||||
// WifiConfiguration is deprecated in Android Q.
|
||||
@Suppress("DEPRECATION")
|
||||
private fun WifiConfiguration.apply(
|
||||
data: SimpleDataAccessor
|
||||
@@ -222,7 +225,7 @@ object WifiConnector {
|
||||
|
||||
fun WifiConfiguration.applyCommon(
|
||||
data: SimpleDataAccessor
|
||||
): WifiConfiguration? {
|
||||
): WifiConfiguration {
|
||||
allowedAuthAlgorithms.clear()
|
||||
allowedGroupCiphers.clear()
|
||||
allowedKeyManagement.clear()
|
||||
@@ -274,7 +277,7 @@ object WifiConnector {
|
||||
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
|
||||
@Suppress("DEPRECATION") // TKIP is insecure and has bad performance.
|
||||
allowedPairwiseCiphers.set(WifiConfiguration.PairwiseCipher.TKIP)
|
||||
allowedPairwiseCiphers.set(WifiConfiguration.PairwiseCipher.CCMP)
|
||||
allowedGroupCiphers.set(WifiConfiguration.GroupCipher.TKIP)
|
||||
@@ -292,7 +295,7 @@ object WifiConnector {
|
||||
return this
|
||||
}
|
||||
|
||||
return this.applyCommon(data)?.applySecurity(data)
|
||||
return this.applyCommon(data).applySecurity(data)
|
||||
}
|
||||
|
||||
private inline fun <T> requireSdk(version: Int, block: () -> T): T? {
|
||||
@@ -300,8 +303,8 @@ object WifiConnector {
|
||||
}
|
||||
}
|
||||
|
||||
// keep possibility of wrongly unescaped \ by explicitly searching
|
||||
// for special chars
|
||||
// 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 ->
|
||||
@@ -327,12 +330,12 @@ 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
|
||||
// because Q doesn't allow apps to enable/disable Wi-Fi anymore.
|
||||
@Suppress("DEPRECATION")
|
||||
return isWifiEnabled || setWifiEnabled(true)
|
||||
}
|
||||
|
||||
// WifiConfiguration is deprecated in Android Q
|
||||
// WifiConfiguration is deprecated in Android Q.
|
||||
@Suppress("DEPRECATION")
|
||||
private fun WifiManager.removeOldNetwork(
|
||||
wifiConfig: WifiConfiguration
|
||||
@@ -345,13 +348,13 @@ private fun WifiManager.removeOldNetwork(
|
||||
removeNetwork(it)
|
||||
}
|
||||
} catch (e: SecurityException) {
|
||||
// the user didn't allow ACCESS_FINE_LOCATION which is
|
||||
// required to access configuredNetworks and that's fine
|
||||
// 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
|
||||
// WifiConfiguration is deprecated in Android Q.
|
||||
@Suppress("DEPRECATION")
|
||||
private fun WifiManager.enableNewNetwork(
|
||||
wifiConfig: WifiConfiguration
|
||||
|
||||
@@ -1,54 +1,59 @@
|
||||
package de.markusfisch.android.binaryeye.activity
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.AlertDialog
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.graphics.Matrix
|
||||
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.support.design.widget.FloatingActionButton
|
||||
import android.support.v7.app.AppCompatActivity
|
||||
import android.support.v7.widget.Toolbar
|
||||
import android.support.v8.renderscript.RSRuntimeException
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.widget.SeekBar
|
||||
import com.google.zxing.Result
|
||||
import com.google.zxing.ResultMetadataType
|
||||
import com.google.zxing.ResultPointCallback
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
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.graphics.Mapping
|
||||
import de.markusfisch.android.binaryeye.graphics.frameToView
|
||||
import de.markusfisch.android.binaryeye.graphics.isPortrait
|
||||
import de.markusfisch.android.binaryeye.content.copyToClipboard
|
||||
import de.markusfisch.android.binaryeye.content.execShareIntent
|
||||
import de.markusfisch.android.binaryeye.content.openUrl
|
||||
import de.markusfisch.android.binaryeye.content.toHexString
|
||||
import de.markusfisch.android.binaryeye.database.toScan
|
||||
import de.markusfisch.android.binaryeye.graphics.FrameMetrics
|
||||
import de.markusfisch.android.binaryeye.graphics.mapPosition
|
||||
import de.markusfisch.android.binaryeye.graphics.setFrameRoi
|
||||
import de.markusfisch.android.binaryeye.graphics.setFrameToView
|
||||
import de.markusfisch.android.binaryeye.net.sendAsync
|
||||
import de.markusfisch.android.binaryeye.rs.Preprocessor
|
||||
import de.markusfisch.android.binaryeye.net.urlEncode
|
||||
import de.markusfisch.android.binaryeye.os.error
|
||||
import de.markusfisch.android.binaryeye.os.getVibrator
|
||||
import de.markusfisch.android.binaryeye.os.vibrate
|
||||
import de.markusfisch.android.binaryeye.view.initSystemBars
|
||||
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 de.markusfisch.android.zxingcpp.ZxingCpp
|
||||
import de.markusfisch.android.zxingcpp.ZxingCpp.Format
|
||||
import de.markusfisch.android.zxingcpp.ZxingCpp.Result
|
||||
import java.nio.charset.Charset
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
class CameraActivity : AppCompatActivity() {
|
||||
private val zxing = Zxing(ResultPointCallback { point ->
|
||||
point?.let {
|
||||
getMapping()?.map(it)?.let {
|
||||
detectorView.post {
|
||||
detectorView.mark(listOf(it))
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
private val frameRoi = Rect()
|
||||
private val matrix = Matrix()
|
||||
|
||||
private lateinit var vibrator: Vibrator
|
||||
private lateinit var cameraView: CameraView
|
||||
@@ -56,17 +61,15 @@ class CameraActivity : AppCompatActivity() {
|
||||
private lateinit var zoomBar: SeekBar
|
||||
private lateinit var flashFab: FloatingActionButton
|
||||
|
||||
private var preprocessor: Preprocessor? = null
|
||||
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 formats = setOf<String>()
|
||||
private var frameMetrics = FrameMetrics()
|
||||
private var decoding = true
|
||||
private var returnResult = false
|
||||
private var returnUrlTemplate: String? = null
|
||||
private var finishAfterShowingResult = false
|
||||
private var frontFacing = false
|
||||
private var bulkMode = false
|
||||
private var bulkMode = prefs.bulkMode
|
||||
private var restrictFormat: String? = null
|
||||
private var ignoreNext: String? = null
|
||||
private var fallbackBuffer: IntArray? = null
|
||||
|
||||
@@ -102,11 +105,20 @@ class CameraActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
override fun attachBaseContext(base: Context?) {
|
||||
base?.applyLocale(prefs.customLocale)
|
||||
super.attachBaseContext(base)
|
||||
}
|
||||
|
||||
override fun onCreate(state: Bundle?) {
|
||||
super.onCreate(state)
|
||||
setContentView(R.layout.activity_camera)
|
||||
|
||||
vibrator = getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
|
||||
// Necessary to get the right translation after setting a
|
||||
// custom locale.
|
||||
setTitle(R.string.scan_code)
|
||||
|
||||
vibrator = getVibrator()
|
||||
|
||||
initSystemBars(this)
|
||||
setSupportActionBar(findViewById(R.id.toolbar) as Toolbar)
|
||||
@@ -129,25 +141,50 @@ class CameraActivity : AppCompatActivity() {
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
resetPreProcessor()
|
||||
fallbackBuffer = null
|
||||
saveZoom()
|
||||
saveCropHandlePos()
|
||||
detectorView.saveCropHandlePos()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
System.gc()
|
||||
zxing.updateHints(prefs.tryHarder)
|
||||
returnResult = "com.google.zxing.client.android.SCAN" == intent.action
|
||||
if (hasCameraPermission(this)) {
|
||||
updateHints()
|
||||
if (prefs.bulkMode && bulkMode != prefs.bulkMode) {
|
||||
bulkMode = prefs.bulkMode
|
||||
invalidateOptionsMenu()
|
||||
ignoreNext = null
|
||||
}
|
||||
setReturnTarget(intent)
|
||||
if (hasCameraPermission()) {
|
||||
openCamera()
|
||||
}
|
||||
}
|
||||
|
||||
private fun resetPreProcessor() {
|
||||
preprocessor?.destroy()
|
||||
preprocessor = null
|
||||
private fun updateHints() {
|
||||
val restriction = restrictFormat
|
||||
formats = if (restriction != null) {
|
||||
title = getString(
|
||||
R.string.scan_format,
|
||||
prettifyFormatName(restriction)
|
||||
)
|
||||
setOf(restriction)
|
||||
} else {
|
||||
setTitle(R.string.scan_code)
|
||||
prefs.barcodeFormats
|
||||
}
|
||||
}
|
||||
|
||||
private fun setReturnTarget(intent: Intent?) {
|
||||
when {
|
||||
intent?.action == "com.google.zxing.client.android.SCAN" -> {
|
||||
returnResult = true
|
||||
}
|
||||
intent?.dataString?.isReturnUrl() == true -> {
|
||||
finishAfterShowingResult = true
|
||||
returnUrlTemplate = intent.data?.getQueryParameter("ret")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun openCamera() {
|
||||
@@ -170,8 +207,6 @@ class CameraActivity : AppCompatActivity() {
|
||||
|
||||
private fun closeCamera() {
|
||||
cameraView.close()
|
||||
// closing the camera will also shut off the flash
|
||||
flash = false
|
||||
}
|
||||
|
||||
override fun onRestoreInstanceState(savedState: Bundle) {
|
||||
@@ -180,6 +215,7 @@ class CameraActivity : AppCompatActivity() {
|
||||
zoomBar.progress = savedState.getInt(ZOOM_LEVEL)
|
||||
frontFacing = savedState.getBoolean(FRONT_FACING)
|
||||
bulkMode = savedState.getBoolean(BULK_MODE)
|
||||
restrictFormat = savedState.getString(RESTRICT_FORMAT)
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
@@ -187,12 +223,13 @@ class CameraActivity : AppCompatActivity() {
|
||||
outState.putInt(ZOOM_LEVEL, zoomBar.progress)
|
||||
outState.putBoolean(FRONT_FACING, frontFacing)
|
||||
outState.putBoolean(BULK_MODE, bulkMode)
|
||||
outState.putString(RESTRICT_FORMAT, restrictFormat)
|
||||
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
|
||||
// Always give crop handle precedence over other controls
|
||||
// because it can easily overlap and would then be inaccessible.
|
||||
if (detectorView.onTouchEvent(ev)) {
|
||||
return true
|
||||
}
|
||||
@@ -216,11 +253,11 @@ class CameraActivity : AppCompatActivity() {
|
||||
true
|
||||
}
|
||||
R.id.pick_file -> {
|
||||
val chooseFile = Intent(Intent.ACTION_GET_CONTENT)
|
||||
chooseFile.type = "image/*"
|
||||
startActivityForResult(
|
||||
Intent.createChooser(
|
||||
chooseFile,
|
||||
Intent(Intent.ACTION_GET_CONTENT).apply {
|
||||
type = "image/*"
|
||||
},
|
||||
getString(R.string.pick_file)
|
||||
),
|
||||
PICK_FILE_RESULT_CODE
|
||||
@@ -234,6 +271,11 @@ class CameraActivity : AppCompatActivity() {
|
||||
R.id.bulk_mode -> {
|
||||
bulkMode = bulkMode xor true
|
||||
item.isChecked = bulkMode
|
||||
ignoreNext = null
|
||||
true
|
||||
}
|
||||
R.id.restrict_format -> {
|
||||
showRestrictionDialog()
|
||||
true
|
||||
}
|
||||
R.id.preferences -> {
|
||||
@@ -258,16 +300,33 @@ class CameraActivity : AppCompatActivity() {
|
||||
openCamera()
|
||||
}
|
||||
|
||||
private fun showRestrictionDialog() {
|
||||
val names = resources.getStringArray(
|
||||
R.array.barcode_formats_names
|
||||
).toMutableList()
|
||||
val formats = resources.getStringArray(
|
||||
R.array.barcode_formats_values
|
||||
).toMutableList()
|
||||
if (restrictFormat != null) {
|
||||
names.add(0, getString(R.string.remove_restriction))
|
||||
formats.add(0, null)
|
||||
}
|
||||
AlertDialog.Builder(this).apply {
|
||||
setTitle(R.string.restrict_format)
|
||||
setItems(names.toTypedArray()) { _, which ->
|
||||
restrictFormat = formats[which]
|
||||
updateHints()
|
||||
}
|
||||
show()
|
||||
}
|
||||
}
|
||||
|
||||
private fun openReadme() {
|
||||
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)
|
||||
}
|
||||
execShareIntent(intent)
|
||||
}
|
||||
|
||||
private fun handleSendText(intent: Intent) {
|
||||
@@ -309,8 +368,8 @@ class CameraActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
MotionEvent.ACTION_UP -> {
|
||||
// stop calling focusTo() as soon as it returns false
|
||||
// to avoid throwing and catching future exceptions
|
||||
// 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) {
|
||||
@@ -328,7 +387,7 @@ class CameraActivity : AppCompatActivity() {
|
||||
override fun onConfigureParameters(
|
||||
parameters: Camera.Parameters
|
||||
) {
|
||||
if (parameters.isZoomSupported) {
|
||||
zoomBar.visibility = if (parameters.isZoomSupported) {
|
||||
val max = parameters.maxZoom
|
||||
if (zoomBar.max != max) {
|
||||
zoomBar.max = max
|
||||
@@ -336,8 +395,9 @@ class CameraActivity : AppCompatActivity() {
|
||||
saveZoom()
|
||||
}
|
||||
parameters.zoom = zoomBar.progress
|
||||
View.VISIBLE
|
||||
} else {
|
||||
zoomBar.visibility = View.GONE
|
||||
View.GONE
|
||||
}
|
||||
val sceneModes = parameters.supportedSceneModes
|
||||
sceneModes?.let {
|
||||
@@ -358,25 +418,30 @@ class CameraActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
override fun onCameraReady(camera: Camera) {
|
||||
// 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
|
||||
frameMetrics = FrameMetrics(
|
||||
cameraView.frameWidth,
|
||||
cameraView.frameHeight,
|
||||
cameraView.frameOrientation
|
||||
)
|
||||
updateFrameRoiAndMappingMatrix()
|
||||
ignoreNext = null
|
||||
decoding = true
|
||||
camera.setPreviewCallback { frameData, _ ->
|
||||
if (decoding) {
|
||||
decodeFrame(
|
||||
ZxingCpp.readByteArray(
|
||||
frameData,
|
||||
frameWidth,
|
||||
frameHeight,
|
||||
frameOrientation
|
||||
frameMetrics.width,
|
||||
frameRoi.left, frameRoi.top,
|
||||
frameRoi.width(), frameRoi.height(),
|
||||
frameMetrics.orientation,
|
||||
formats.joinToString(),
|
||||
prefs.tryHarder,
|
||||
prefs.autoRotate,
|
||||
tryInvert = true,
|
||||
tryDownscale = true
|
||||
)?.let { result ->
|
||||
if (result.text != ignoreNext) {
|
||||
postResult(result)
|
||||
postResult(result.redact())
|
||||
decoding = false
|
||||
}
|
||||
}
|
||||
@@ -401,7 +466,7 @@ class CameraActivity : AppCompatActivity() {
|
||||
progress: Int,
|
||||
fromUser: Boolean
|
||||
) {
|
||||
setZoom(progress)
|
||||
cameraView.camera?.setZoom(progress)
|
||||
}
|
||||
|
||||
override fun onStartTrackingTouch(seekBar: SeekBar) {}
|
||||
@@ -412,16 +477,13 @@ class CameraActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
private fun setZoom(zoom: Int) {
|
||||
val camera: Camera? = cameraView.camera
|
||||
camera?.let {
|
||||
try {
|
||||
val params = camera.parameters
|
||||
params.zoom = zoom
|
||||
camera.parameters = params
|
||||
} catch (e: RuntimeException) {
|
||||
// ignore; there's nothing we can do
|
||||
}
|
||||
private fun Camera.setZoom(zoom: Int) {
|
||||
try {
|
||||
val params = parameters
|
||||
params.zoom = zoom
|
||||
parameters = params
|
||||
} catch (e: RuntimeException) {
|
||||
// Ignore. There's nothing we can do.
|
||||
}
|
||||
}
|
||||
|
||||
@@ -446,25 +508,21 @@ class CameraActivity : AppCompatActivity() {
|
||||
}
|
||||
detectorView.onRoiChanged = {
|
||||
decoding = true
|
||||
recreatePreprocessor = true
|
||||
updateFrameRoiAndMappingMatrix()
|
||||
}
|
||||
detectorView.setPaddingFromWindowInsets()
|
||||
restoreCropHandlePos()
|
||||
detectorView.restoreCropHandlePos()
|
||||
}
|
||||
|
||||
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 updateFrameRoiAndMappingMatrix() {
|
||||
val viewRect = cameraView.previewRect
|
||||
val viewRoi = if (detectorView.roi.width() < 1) {
|
||||
viewRect
|
||||
} else {
|
||||
detectorView.roi
|
||||
}
|
||||
frameRoi.setFrameRoi(frameMetrics, viewRect, viewRoi)
|
||||
matrix.setFrameToView(frameMetrics, viewRect, viewRoi)
|
||||
}
|
||||
|
||||
private fun updateFlashFab(unavailable: Boolean) {
|
||||
@@ -479,221 +537,151 @@ class CameraActivity : AppCompatActivity() {
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
private fun toggleTorchMode() {
|
||||
val camera = cameraView.camera
|
||||
val parameters = camera?.parameters
|
||||
parameters?.flashMode = if (flash) {
|
||||
val camera = cameraView.camera ?: return
|
||||
val parameters = camera.parameters ?: return
|
||||
parameters.flashMode = if (
|
||||
parameters.flashMode != Camera.Parameters.FLASH_MODE_OFF
|
||||
) {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
private fun createPreprocessorAndMapping(
|
||||
frameWidth: Int,
|
||||
frameHeight: Int,
|
||||
frameOrientation: Int
|
||||
): Preprocessor {
|
||||
val frameRoi = calculateFrameRect(
|
||||
frameWidth,
|
||||
frameHeight,
|
||||
frameOrientation
|
||||
)
|
||||
val pp = Preprocessor(
|
||||
this,
|
||||
frameWidth,
|
||||
frameHeight,
|
||||
frameRoi
|
||||
)
|
||||
val viewRect = if (frameRoi != null) {
|
||||
detectorView.roi
|
||||
} else {
|
||||
cameraView.previewRect
|
||||
}
|
||||
nativeMapping = frameToView(
|
||||
pp.outWidth,
|
||||
pp.outHeight,
|
||||
frameOrientation,
|
||||
viewRect
|
||||
)
|
||||
rotatedMapping = frameToView(
|
||||
pp.outHeight,
|
||||
pp.outWidth,
|
||||
(frameOrientation - 90 + 360) % 360,
|
||||
viewRect
|
||||
)
|
||||
return pp
|
||||
}
|
||||
|
||||
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()
|
||||
)
|
||||
}
|
||||
try {
|
||||
camera.parameters = parameters
|
||||
} catch (e: RuntimeException) {
|
||||
toast(e.message ?: getString(R.string.error_flash))
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
returnResult,
|
||||
bulkMode
|
||||
detectorView.update(
|
||||
matrix.mapPosition(
|
||||
result.position,
|
||||
detectorView.coordinates
|
||||
)
|
||||
)
|
||||
vibrator.vibrate()
|
||||
val returnUri = returnUrlTemplate?.let {
|
||||
try {
|
||||
completeUrl(it, result)
|
||||
} catch (e: Exception) {
|
||||
e.message?.let { message ->
|
||||
toast(message)
|
||||
}
|
||||
null
|
||||
}
|
||||
}
|
||||
when {
|
||||
returnResult -> {
|
||||
setResult(Activity.RESULT_OK, getReturnIntent(result))
|
||||
finish()
|
||||
}
|
||||
returnUri != null -> execShareIntent(
|
||||
Intent(Intent.ACTION_VIEW, returnUri)
|
||||
)
|
||||
else -> {
|
||||
showResult(
|
||||
this@CameraActivity,
|
||||
result,
|
||||
vibrator,
|
||||
bulkMode
|
||||
)
|
||||
// If this app was invoked via a deep link but without
|
||||
// a return URI, we probably don't want to return to
|
||||
// the camera screen after scanning, but to the caller.
|
||||
if (finishAfterShowingResult) {
|
||||
finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
if (bulkMode) {
|
||||
ignoreNext = result.text
|
||||
toast(result.text)
|
||||
if (prefs.showToastInBulkMode) {
|
||||
toast(result.text)
|
||||
}
|
||||
detectorView.postDelayed({
|
||||
decoding = true
|
||||
}, 500)
|
||||
}, prefs.bulkModeDelay.toLong())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getMapping() = if (rotate) rotatedMapping else nativeMapping
|
||||
|
||||
companion object {
|
||||
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"
|
||||
private const val RESTRICT_FORMAT = "restrict_format"
|
||||
}
|
||||
}
|
||||
|
||||
fun Result.redact() = if (
|
||||
format == Format.PDF_417.name &&
|
||||
text.isNotEmpty()
|
||||
) {
|
||||
Result(
|
||||
format,
|
||||
contentType,
|
||||
String(
|
||||
text.toByteArray(Charset.forName("ISO-8859-1")),
|
||||
Charset.forName("UTF-8")
|
||||
),
|
||||
position,
|
||||
orientation,
|
||||
rawBytes,
|
||||
ecLevel,
|
||||
symbologyIdentifier,
|
||||
sequenceSize,
|
||||
sequenceIndex,
|
||||
sequenceId,
|
||||
readerInit,
|
||||
lineCount,
|
||||
versionNumber,
|
||||
gtin
|
||||
)
|
||||
} else {
|
||||
this
|
||||
}
|
||||
|
||||
fun showResult(
|
||||
activity: Activity,
|
||||
result: Result,
|
||||
isResult: Boolean = false,
|
||||
bulkMode: Boolean = false
|
||||
vibrator: Vibrator,
|
||||
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(
|
||||
activity.applicationContext,
|
||||
prefs.sendScanUrl,
|
||||
prefs.sendScanType
|
||||
)
|
||||
}
|
||||
val scan = result.toScan()
|
||||
if (prefs.useHistory) {
|
||||
scan.id = db.insertScan(scan)
|
||||
}
|
||||
if (prefs.sendScanActive && prefs.sendScanUrl.isNotEmpty()) {
|
||||
if (prefs.sendScanType == "4") {
|
||||
activity.openUrl(
|
||||
prefs.sendScanUrl + scan.content.urlEncode()
|
||||
)
|
||||
return
|
||||
}
|
||||
scan.sendAsync(
|
||||
prefs.sendScanUrl,
|
||||
prefs.sendScanType
|
||||
) { code, body ->
|
||||
if (code == null || code < 200 || code > 299) {
|
||||
vibrator.error()
|
||||
if (!activity.isSilent()) {
|
||||
beepBeepBeep()
|
||||
}
|
||||
}
|
||||
if (body != null && body.isNotEmpty()) {
|
||||
activity.toast(body)
|
||||
} else if (code == null || code > 299) {
|
||||
activity.toast(R.string.background_request_failed)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!bulkMode) {
|
||||
activity.startActivity(
|
||||
MainActivity.getDecodeIntent(activity, scan)
|
||||
@@ -701,43 +689,45 @@ fun showResult(
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
private fun getReturnIntent(result: Result) = Intent().apply {
|
||||
putExtra("SCAN_RESULT", result.text)
|
||||
putExtra("SCAN_RESULT_FORMAT", result.format)
|
||||
putExtra("SCAN_RESULT_ORIENTATION", result.orientation)
|
||||
putExtra("SCAN_RESULT_ERROR_CORRECTION_LEVEL", result.ecLevel)
|
||||
if (result.rawBytes.isNotEmpty()) {
|
||||
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 String.isReturnUrl() = listOf(
|
||||
"binaryeye://scan",
|
||||
"http://markusfisch.de/BinaryEye",
|
||||
"https://markusfisch.de/BinaryEye"
|
||||
).firstOrNull { startsWith(it) } != null
|
||||
|
||||
private fun completeUrl(urlTemplate: String, result: Result) = Uri.parse(
|
||||
urlTemplate
|
||||
.replace("{RESULT}", result.text.urlEncode())
|
||||
.replace("{RESULT_BYTES}", result.rawBytes.toHexString())
|
||||
.replace(
|
||||
"{FORMAT}", result.format.urlEncode()
|
||||
)
|
||||
// And support {CODE} from the old ZXing app, too.
|
||||
.replace("{CODE}", result.text.urlEncode())
|
||||
)
|
||||
|
||||
private fun Context.isSilent(): Boolean {
|
||||
val am = getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
||||
return when (am.ringerMode) {
|
||||
AudioManager.RINGER_MODE_SILENT,
|
||||
AudioManager.RINGER_MODE_VIBRATE -> true
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
private fun beepBeepBeep() {
|
||||
ToneGenerator(AudioManager.STREAM_ALARM, 100).startTone(
|
||||
ToneGenerator.TONE_CDMA_CONFIRM,
|
||||
1000
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2,23 +2,39 @@ package de.markusfisch.android.binaryeye.activity
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.support.v4.app.Fragment
|
||||
import android.support.v7.app.AppCompatActivity
|
||||
import android.support.v7.widget.Toolbar
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.app.colorSystemAndToolBars
|
||||
import de.markusfisch.android.binaryeye.app.initSystemBars
|
||||
import de.markusfisch.android.binaryeye.app.setFragment
|
||||
import de.markusfisch.android.binaryeye.app.*
|
||||
import de.markusfisch.android.binaryeye.database.Scan
|
||||
import de.markusfisch.android.binaryeye.fragment.DecodeFragment
|
||||
import de.markusfisch.android.binaryeye.fragment.EncodeFragment
|
||||
import de.markusfisch.android.binaryeye.fragment.HistoryFragment
|
||||
import de.markusfisch.android.binaryeye.fragment.PreferencesFragment
|
||||
import de.markusfisch.android.binaryeye.view.colorSystemAndToolBars
|
||||
import de.markusfisch.android.binaryeye.view.initSystemBars
|
||||
import de.markusfisch.android.binaryeye.view.recordToolbarHeight
|
||||
|
||||
class MainActivity : AppCompatActivity() {
|
||||
override fun onRequestPermissionsResult(
|
||||
requestCode: Int,
|
||||
permissions: Array<String>,
|
||||
grantResults: IntArray
|
||||
) {
|
||||
when (requestCode) {
|
||||
PERMISSION_LOCATION, PERMISSION_WRITE -> if (grantResults.isNotEmpty() &&
|
||||
grantResults[0] == PackageManager.PERMISSION_GRANTED
|
||||
) {
|
||||
permissionGrantedCallback?.invoke()
|
||||
permissionGrantedCallback = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSupportNavigateUp(): Boolean {
|
||||
val fm = supportFragmentManager
|
||||
if (fm != null && fm.backStackEntryCount > 0) {
|
||||
@@ -29,6 +45,11 @@ 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)
|
||||
@@ -44,7 +65,7 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
if (state == null) {
|
||||
setFragment(supportFragmentManager, getFragmentForIntent(intent))
|
||||
supportFragmentManager?.setFragment(getFragmentForIntent(intent))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,16 +73,14 @@ class MainActivity : AppCompatActivity() {
|
||||
private const val PREFERENCES = "preferences"
|
||||
private const val HISTORY = "history"
|
||||
private const val ENCODE = "encode"
|
||||
private const val DECODED = "decoded"
|
||||
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(ENCODE) -> EncodeFragment.newInstance()
|
||||
intent.hasExtra(DECODED) -> DecodeFragment.newInstance(
|
||||
intent.getParcelableExtra(DECODED)!!
|
||||
)
|
||||
|
||||
@@ -3,7 +3,9 @@ package de.markusfisch.android.binaryeye.activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Matrix
|
||||
import android.graphics.Rect
|
||||
import android.graphics.RectF
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.os.Parcelable
|
||||
@@ -12,36 +14,54 @@ import android.support.v7.app.AppCompatActivity
|
||||
import android.support.v7.widget.Toolbar
|
||||
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.colorSystemAndToolBars
|
||||
import de.markusfisch.android.binaryeye.app.initSystemBars
|
||||
import de.markusfisch.android.binaryeye.app.vibrate
|
||||
import de.markusfisch.android.binaryeye.app.applyLocale
|
||||
import de.markusfisch.android.binaryeye.app.prefs
|
||||
import de.markusfisch.android.binaryeye.graphics.crop
|
||||
import de.markusfisch.android.binaryeye.graphics.fixTransparency
|
||||
import de.markusfisch.android.binaryeye.graphics.loadImageUri
|
||||
import de.markusfisch.android.binaryeye.graphics.mapResult
|
||||
import de.markusfisch.android.binaryeye.view.doOnApplyWindowInsets
|
||||
import de.markusfisch.android.binaryeye.graphics.mapPosition
|
||||
import de.markusfisch.android.binaryeye.os.getVibrator
|
||||
import de.markusfisch.android.binaryeye.os.vibrate
|
||||
import de.markusfisch.android.binaryeye.view.colorSystemAndToolBars
|
||||
import de.markusfisch.android.binaryeye.view.initSystemBars
|
||||
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
|
||||
import de.markusfisch.android.zxingcpp.ZxingCpp
|
||||
import de.markusfisch.android.zxingcpp.ZxingCpp.Result
|
||||
import kotlinx.coroutines.*
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
class PickActivity : AppCompatActivity() {
|
||||
private val zxing = Zxing()
|
||||
private val matrix = Matrix()
|
||||
private val parentJob = Job()
|
||||
private val scope = CoroutineScope(Dispatchers.IO + parentJob)
|
||||
|
||||
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)
|
||||
|
||||
zxing.updateHints(true)
|
||||
vibrator = getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
|
||||
// Necessary to get the right translation after setting a custom
|
||||
// locale.
|
||||
setTitle(R.string.pick_code_to_scan)
|
||||
|
||||
vibrator = getVibrator()
|
||||
|
||||
initSystemBars(this)
|
||||
val toolbar = findViewById(R.id.toolbar) as Toolbar
|
||||
@@ -52,67 +72,109 @@ class PickActivity : AppCompatActivity() {
|
||||
colorSystemAndToolBars(this@PickActivity)
|
||||
}
|
||||
|
||||
val 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
|
||||
}
|
||||
|
||||
val bitmap = getBitmapFromIntent()?.fixTransparency()
|
||||
if (bitmap == null) {
|
||||
applicationContext.toast(R.string.error_no_content)
|
||||
finish()
|
||||
return
|
||||
}
|
||||
|
||||
var result: Result? = null
|
||||
val scannedRect = Rect()
|
||||
fun scanWithinBounds() = crop(
|
||||
bitmap,
|
||||
cropImageView.normalizedRectInBounds,
|
||||
cropImageView.imageRotation
|
||||
)?.let {
|
||||
scannedRect.set(0, 0, it.width, it.height)
|
||||
CoroutineScope(Dispatchers.IO).launch(Dispatchers.IO) {
|
||||
result = zxing.decodePositiveNegative(it)
|
||||
result?.let {
|
||||
withContext(Dispatchers.Main) {
|
||||
if (!isFinishing) {
|
||||
vibrator.vibrate()
|
||||
}
|
||||
cropImageView.updateResultPoints(
|
||||
mapResult(
|
||||
scannedRect.width(),
|
||||
scannedRect.height(),
|
||||
0,
|
||||
cropImageView.getBoundsRect(),
|
||||
it
|
||||
)
|
||||
)
|
||||
cropImageView = findViewById(R.id.image) as CropImageView
|
||||
cropImageView.restrictTranslation = false
|
||||
cropImageView.freeRotation = true
|
||||
cropImageView.setImageBitmap(bitmap)
|
||||
cropImageView.onScan = {
|
||||
scanWithinBounds(bitmap)
|
||||
}
|
||||
|
||||
detectorView = findViewById(R.id.detector_view) as DetectorView
|
||||
detectorView.onRoiChanged = {
|
||||
scanWithinBounds(bitmap)
|
||||
}
|
||||
detectorView.setPaddingFromWindowInsets()
|
||||
detectorView.restoreCropHandlePos()
|
||||
|
||||
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 viewRoi = if (detectorView.roi.width() < 1) {
|
||||
cropImageView.getBoundsRect()
|
||||
} else {
|
||||
detectorView.roi
|
||||
}
|
||||
val mappedRect = cropImageView.mappedRect
|
||||
val cropped = bitmap.crop(
|
||||
getNormalizedRoi(mappedRect, viewRoi),
|
||||
cropImageView.imageRotation,
|
||||
cropImageView.pivotX,
|
||||
cropImageView.pivotY
|
||||
) ?: return
|
||||
val croppedInView = Rect(
|
||||
max(viewRoi.left, mappedRect.left.roundToInt()),
|
||||
max(viewRoi.top, mappedRect.top.roundToInt()),
|
||||
min(viewRoi.right, mappedRect.right.roundToInt()),
|
||||
min(viewRoi.bottom, mappedRect.bottom.roundToInt())
|
||||
)
|
||||
matrix.apply {
|
||||
setScale(
|
||||
croppedInView.width().toFloat() / cropped.width,
|
||||
croppedInView.height().toFloat() / cropped.height
|
||||
)
|
||||
postTranslate(
|
||||
croppedInView.left.toFloat(),
|
||||
croppedInView.top.toFloat()
|
||||
)
|
||||
}
|
||||
scope.launch {
|
||||
ZxingCpp.readBitmap(
|
||||
cropped,
|
||||
0, 0,
|
||||
cropped.width, cropped.height,
|
||||
0,
|
||||
prefs.barcodeFormats.joinToString(),
|
||||
tryHarder = true,
|
||||
tryRotate = true,
|
||||
tryInvert = true,
|
||||
tryDownscale = true
|
||||
)?.let {
|
||||
withContext(Dispatchers.Main) {
|
||||
if (isFinishing) {
|
||||
return@withContext
|
||||
}
|
||||
result = it
|
||||
vibrator.vibrate()
|
||||
detectorView.update(
|
||||
matrix.mapPosition(
|
||||
it.position,
|
||||
detectorView.coordinates
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cropImageView = findViewById(R.id.image) as CropImageView
|
||||
cropImageView.setImageBitmap(bitmap)
|
||||
cropImageView.onScan = {
|
||||
scanWithinBounds()
|
||||
}
|
||||
cropImageView.doOnApplyWindowInsets { v, insets ->
|
||||
(v as CropImageView).windowInsets.set(insets)
|
||||
}
|
||||
|
||||
findViewById(R.id.scan).setOnClickListener {
|
||||
scanImage(result)
|
||||
}
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
detectorView.saveCropHandlePos()
|
||||
parentJob.cancel()
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||
@@ -138,20 +200,32 @@ class PickActivity : AppCompatActivity() {
|
||||
val uri = intent.getParcelableExtra<Parcelable>(
|
||||
Intent.EXTRA_STREAM
|
||||
) as? Uri ?: return null
|
||||
return loadImageUri(contentResolver, uri)
|
||||
return contentResolver.loadImageUri(uri)
|
||||
}
|
||||
|
||||
private fun loadImageToOpen(intent: Intent): Bitmap? {
|
||||
val uri = intent.data ?: return null
|
||||
return loadImageUri(contentResolver, uri)
|
||||
return contentResolver.loadImageUri(uri)
|
||||
}
|
||||
|
||||
private fun scanImage(result: Result?) {
|
||||
if (result != null) {
|
||||
showResult(this, result)
|
||||
private fun showResult() {
|
||||
val r = result
|
||||
if (r != null) {
|
||||
showResult(this, r.redact(), vibrator)
|
||||
finish()
|
||||
return
|
||||
} else {
|
||||
applicationContext.toast(R.string.no_barcode_found)
|
||||
}
|
||||
applicationContext.toast(R.string.no_barcode_found)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getNormalizedRoi(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,17 +3,15 @@ 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?) {
|
||||
super.onCreate(state)
|
||||
|
||||
// it's important _not_ to inflate a layout file here
|
||||
// It's important _not_ to inflate a layout file here
|
||||
// because that would happen after the app is fully
|
||||
// initialized what is too late
|
||||
// initialized what is too late.
|
||||
|
||||
setRestartCount(intent)
|
||||
startActivity(Intent(applicationContext, CameraActivity::class.java))
|
||||
finish()
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package de.markusfisch.android.binaryeye.adapter
|
||||
|
||||
import android.content.Context
|
||||
import android.database.Cursor
|
||||
import android.support.v4.content.ContextCompat
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
@@ -9,39 +10,72 @@ 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.ParseException
|
||||
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)
|
||||
private val selections = mutableMapOf<Long, Int>()
|
||||
private val selectedColor = ContextCompat.getColor(
|
||||
context, R.color.selected_row
|
||||
)
|
||||
|
||||
fun select(id: Long, position: Int) {
|
||||
selectedScanId = id
|
||||
selectedScanPosition = position
|
||||
fun select(view: View, id: Long, position: Int) {
|
||||
view.select(
|
||||
if (selections[id] == null) {
|
||||
selections[id] = position
|
||||
true
|
||||
} else {
|
||||
selections.remove(id)
|
||||
false
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
fun clearSelection() {
|
||||
selectedScanId = 0L
|
||||
selectedScanPosition = -1
|
||||
selections.clear()
|
||||
}
|
||||
|
||||
fun getSelectedContent() = if (selectedScanPosition < 0) {
|
||||
null
|
||||
} else {
|
||||
val cursor = getItem(selectedScanPosition)
|
||||
if (cursor is Cursor) {
|
||||
cursor.getString(contentIndex)
|
||||
} else {
|
||||
null
|
||||
fun forSelection(callback: (Long, Int) -> Unit) {
|
||||
selections.forEach {
|
||||
callback.invoke(it.key, it.value)
|
||||
}
|
||||
}
|
||||
|
||||
fun getSelectedIds(): List<Long> = mutableListOf<Long>().apply {
|
||||
forSelection { id, _ ->
|
||||
add(id)
|
||||
}
|
||||
}
|
||||
|
||||
fun getSelectedContent(separator: String): String {
|
||||
val sb = StringBuilder()
|
||||
forSelection { _, position ->
|
||||
getContent(position)?.let {
|
||||
if (sb.isNotEmpty()) {
|
||||
sb.append(separator)
|
||||
}
|
||||
sb.append(it)
|
||||
}
|
||||
}
|
||||
return sb.toString()
|
||||
}
|
||||
|
||||
fun getName(
|
||||
position: Int
|
||||
) = (getItem(position) as Cursor?)?.getString(nameIndex)
|
||||
|
||||
fun getContent(
|
||||
position: Int
|
||||
) = (getItem(position) as Cursor?)?.getString(contentIndex)
|
||||
|
||||
override fun newView(
|
||||
context: Context,
|
||||
cursor: Cursor,
|
||||
@@ -55,38 +89,45 @@ class ScansAdapter(context: Context, cursor: Cursor) :
|
||||
context: Context,
|
||||
cursor: Cursor
|
||||
) {
|
||||
val holder = getViewHolder(view)
|
||||
holder.timeView.text = 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
|
||||
getViewHolder(view).apply {
|
||||
timeView.text = formatDateTime(cursor.getString(timeIndex))
|
||||
val name = cursor.getString(nameIndex)
|
||||
val content = cursor.getString(contentIndex)
|
||||
var icon = 0
|
||||
contentView.text = when {
|
||||
name?.isNotEmpty() == true -> {
|
||||
icon = R.drawable.ic_label
|
||||
name
|
||||
}
|
||||
content?.isEmpty() == true -> context.getString(
|
||||
R.string.binary_data
|
||||
)
|
||||
else -> content
|
||||
}
|
||||
content?.isEmpty() == true -> context.getString(R.string.binary_data)
|
||||
else -> content
|
||||
contentView.setCompoundDrawablesWithIntrinsicBounds(
|
||||
icon, 0, 0, 0
|
||||
)
|
||||
formatView.text = prettifyFormatName(cursor.getString(formatIndex))
|
||||
}
|
||||
holder.contentView.setCompoundDrawablesWithIntrinsicBounds(
|
||||
icon, 0, 0, 0
|
||||
)
|
||||
holder.formatView.text = cursor.getString(formatIndex)
|
||||
// view.isSelected needs to be put on the queue to work
|
||||
val selected = cursor.getLong(idIndex) == selectedScanId
|
||||
val selected = selections[cursor.getLong(idIndex)] != null
|
||||
view.post {
|
||||
view.isSelected = selected
|
||||
// Needs to be put on the queue to work.
|
||||
view.select(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 fun View.select(selected: Boolean) {
|
||||
setBackgroundColor(if (selected) selectedColor else 0)
|
||||
}
|
||||
|
||||
private fun getViewHolder(
|
||||
view: View
|
||||
): ViewHolder = 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(
|
||||
@@ -95,3 +136,19 @@ class ScansAdapter(context: Context, cursor: Cursor) :
|
||||
val formatView: TextView
|
||||
)
|
||||
}
|
||||
|
||||
fun prettifyFormatName(format: String) = format.replace("_", " ")
|
||||
|
||||
private fun formatDateTime(rfc: String): String {
|
||||
try {
|
||||
SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.US).parse(rfc)?.let {
|
||||
return DateFormat.getDateTimeInstance(
|
||||
DateFormat.LONG,
|
||||
DateFormat.SHORT
|
||||
).format(it)
|
||||
}
|
||||
} catch (e: ParseException) {
|
||||
// Ignore and fall through.
|
||||
}
|
||||
return rfc
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package de.markusfisch.android.binaryeye.app
|
||||
|
||||
import android.app.AlertDialog
|
||||
import android.content.Context
|
||||
import android.support.v7.app.AlertDialog
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlin.coroutines.resume
|
||||
@@ -11,7 +11,7 @@ suspend inline fun <T : Any> alertDialog(
|
||||
context: Context,
|
||||
crossinline build: AlertDialog.Builder.(resume: (T?) -> Unit) -> Unit
|
||||
): T? = withContext(Dispatchers.Main) {
|
||||
suspendCoroutine<T?> { continuation ->
|
||||
suspendCoroutine { continuation ->
|
||||
AlertDialog.Builder(context).apply {
|
||||
setOnCancelListener {
|
||||
continuation.resume(null)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
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
|
||||
|
||||
@@ -13,9 +12,5 @@ class BinaryEyeApp : Application() {
|
||||
super.onCreate()
|
||||
db.open(this)
|
||||
prefs.init(this)
|
||||
|
||||
if (prefs.forceCompat) {
|
||||
RenderScript.forceCompat()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
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("simple text", text)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
package de.markusfisch.android.binaryeye.app
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.support.v4.app.Fragment
|
||||
import android.support.v4.app.FragmentManager
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
|
||||
fun setFragment(fm: FragmentManager?, fragment: Fragment) {
|
||||
fm?.let { getTransaction(fm, fragment).commit() }
|
||||
}
|
||||
|
||||
fun addFragment(fm: FragmentManager?, fragment: Fragment) {
|
||||
fm?.let { getTransaction(fm, fragment).addToBackStack(null).commit() }
|
||||
}
|
||||
|
||||
@SuppressLint("CommitTransaction")
|
||||
private fun getTransaction(
|
||||
fm: FragmentManager,
|
||||
fragment: Fragment
|
||||
) = fm.beginTransaction().replace(R.id.content_frame, fragment)
|
||||
@@ -1,14 +0,0 @@
|
||||
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,19 @@
|
||||
package de.markusfisch.android.binaryeye.app
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.support.v4.app.Fragment
|
||||
import android.support.v4.app.FragmentManager
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
|
||||
fun FragmentManager.setFragment(fragment: Fragment) {
|
||||
getTransaction(fragment).commit()
|
||||
}
|
||||
|
||||
fun FragmentManager.addFragment(fragment: Fragment) {
|
||||
getTransaction(fragment).addToBackStack(null).commit()
|
||||
}
|
||||
|
||||
@SuppressLint("CommitTransaction")
|
||||
private fun FragmentManager.getTransaction(
|
||||
fragment: Fragment
|
||||
) = beginTransaction().replace(R.id.content_frame, fragment)
|
||||
@@ -0,0 +1,26 @@
|
||||
package de.markusfisch.android.binaryeye.app
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import java.util.*
|
||||
|
||||
fun Context.applyLocale(localeName: String) {
|
||||
if (localeName.isEmpty()) {
|
||||
return
|
||||
}
|
||||
val localeParts = localeName.split("-")
|
||||
val locale = if (localeParts.size == 2) {
|
||||
Locale(localeParts[0], localeParts[1])
|
||||
} else {
|
||||
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)
|
||||
}
|
||||
@@ -6,44 +6,42 @@ 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): Boolean {
|
||||
return hasPermission(
|
||||
activity,
|
||||
Manifest.permission.CAMERA,
|
||||
PERMISSION_CAMERA
|
||||
)
|
||||
}
|
||||
fun Activity.hasCameraPermission() = hasPermission(
|
||||
Manifest.permission.CAMERA,
|
||||
PERMISSION_CAMERA
|
||||
)
|
||||
|
||||
const val PERMISSION_WRITE = 2
|
||||
fun hasWritePermission(activity: Activity): Boolean {
|
||||
fun Activity.hasWritePermission(callback: () -> Any): Boolean {
|
||||
permissionGrantedCallback = callback
|
||||
return hasPermission(
|
||||
activity,
|
||||
Manifest.permission.WRITE_EXTERNAL_STORAGE,
|
||||
PERMISSION_WRITE
|
||||
)
|
||||
}
|
||||
|
||||
const val PERMISSION_LOCATION = 3
|
||||
fun hasLocationPermission(activity: Activity): Boolean {
|
||||
fun Activity.hasLocationPermission(callback: () -> Any): Boolean {
|
||||
permissionGrantedCallback = callback
|
||||
return hasPermission(
|
||||
activity,
|
||||
Manifest.permission.ACCESS_FINE_LOCATION,
|
||||
PERMISSION_LOCATION
|
||||
)
|
||||
}
|
||||
|
||||
private fun hasPermission(
|
||||
activity: Activity,
|
||||
private fun Activity.hasPermission(
|
||||
permission: String,
|
||||
requestCode: Int
|
||||
) = if (ContextCompat.checkSelfPermission(
|
||||
activity,
|
||||
this,
|
||||
permission
|
||||
) != PackageManager.PERMISSION_GRANTED
|
||||
) {
|
||||
ActivityCompat.requestPermissions(
|
||||
activity,
|
||||
this,
|
||||
arrayOf(permission),
|
||||
requestCode
|
||||
)
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
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)
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
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,24 +0,0 @@
|
||||
package de.markusfisch.android.binaryeye.app
|
||||
|
||||
import android.os.Build
|
||||
import android.os.VibrationEffect
|
||||
import android.os.Vibrator
|
||||
|
||||
private const val MILLISECONDS = 100L
|
||||
|
||||
fun Vibrator.vibrate() {
|
||||
if (!prefs.vibrate) {
|
||||
return
|
||||
}
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
||||
@Suppress("DEPRECATION")
|
||||
this.vibrate(MILLISECONDS)
|
||||
} else {
|
||||
this.vibrate(
|
||||
VibrationEffect.createOneShot(
|
||||
MILLISECONDS,
|
||||
VibrationEffect.DEFAULT_AMPLITUDE
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package de.markusfisch.android.binaryeye.content
|
||||
|
||||
import android.content.ClipData
|
||||
import android.content.ClipDescription
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.os.PersistableBundle
|
||||
|
||||
fun Context.copyToClipboard(text: String, isSensitive: Boolean = false) {
|
||||
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).apply {
|
||||
setSensitive(isSensitive)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun ClipData.setSensitive(isSensitive: Boolean) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
description.extras = PersistableBundle().apply {
|
||||
putBoolean(EXTRA_IS_SENSITIVE, isSensitive)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val EXTRA_IS_SENSITIVE = if (
|
||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU
|
||||
) {
|
||||
ClipDescription.EXTRA_IS_SENSITIVE
|
||||
} else {
|
||||
"android.content.extra.IS_SENSITIVE"
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
package de.markusfisch.android.binaryeye.content
|
||||
|
||||
fun ByteArray.toHexString() = joinToString("") { "%02X".format(it) }
|
||||
@@ -0,0 +1,83 @@
|
||||
package de.markusfisch.android.binaryeye.content
|
||||
|
||||
import android.content.ActivityNotFoundException
|
||||
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 Context.execShareIntent(intent: Intent): Boolean = if (
|
||||
!startIntent(intent)
|
||||
) {
|
||||
toast(R.string.cannot_resolve_action)
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
|
||||
fun Context.startIntent(intent: Intent): Boolean = try {
|
||||
// Avoid using `intent.resolveActivity()` at API level 30+ due
|
||||
// to the new package visibility restrictions. In order for
|
||||
// `resolveActivity()` to "see" another package, we would need
|
||||
// to list that package/intent in a `<queries>` block in the
|
||||
// Manifest. But since we used `resolveActivity()` only to avoid
|
||||
// an exception if the Intent cannot be resolved, it's much easier
|
||||
// and more robust to just try and catch that exception if
|
||||
// necessary.
|
||||
startActivity(intent)
|
||||
true
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
false
|
||||
}
|
||||
|
||||
fun Context.openUrl(url: String): Boolean = openUri(url.parseAndNormalizeUri())
|
||||
|
||||
fun Context.openUri(uri: Uri): Boolean = execShareIntent(
|
||||
Intent(Intent.ACTION_VIEW, uri)
|
||||
)
|
||||
|
||||
fun String.parseAndNormalizeUri(): Uri = Uri.parse(this).let {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
|
||||
it.normalizeScheme()
|
||||
} else {
|
||||
it
|
||||
}
|
||||
}
|
||||
|
||||
fun Context.shareText(text: String, mimeType: String = "text/plain") {
|
||||
execShareIntent(Intent(Intent.ACTION_SEND).apply {
|
||||
putExtra(Intent.EXTRA_TEXT, text)
|
||||
type = mimeType
|
||||
})
|
||||
}
|
||||
|
||||
fun Context.shareFile(file: File, mimeType: String) {
|
||||
getUriForFile(file)?.let {
|
||||
shareUri(it, mimeType)
|
||||
}
|
||||
}
|
||||
|
||||
private fun Context.getUriForFile(file: File): Uri? = if (
|
||||
Build.VERSION.SDK_INT < Build.VERSION_CODES.N
|
||||
) {
|
||||
Uri.fromFile(file)
|
||||
} else {
|
||||
FileProvider.getUriForFile(
|
||||
this,
|
||||
BuildConfig.APPLICATION_ID + ".provider",
|
||||
file
|
||||
)
|
||||
}
|
||||
|
||||
private fun Context.shareUri(uri: Uri, mimeType: String) {
|
||||
execShareIntent(Intent(Intent.ACTION_SEND).apply {
|
||||
putExtra(Intent.EXTRA_STREAM, uri)
|
||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
type = mimeType
|
||||
})
|
||||
}
|
||||
@@ -2,23 +2,22 @@ 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 de.markusfisch.android.binaryeye.content.toHexString
|
||||
import de.markusfisch.android.binaryeye.io.writeExternalFile
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.OutputStream
|
||||
|
||||
fun exportCsv(
|
||||
context: Context,
|
||||
fun Context.exportCsv(
|
||||
name: String,
|
||||
cursor: Cursor,
|
||||
delimiter: String
|
||||
) = writeExternalFile(context, name, "text/csv") { outputStream ->
|
||||
) = writeExternalFile(name, "text/csv") { outputStream ->
|
||||
exportCsv(outputStream, cursor, delimiter)
|
||||
}
|
||||
|
||||
fun exportCsv(cursor: Cursor, delimiter: String): String? {
|
||||
fun Cursor.exportCsv(delimiter: String): String {
|
||||
val outputStream = ByteArrayOutputStream()
|
||||
exportCsv(outputStream, cursor, delimiter)
|
||||
exportCsv(outputStream, this, delimiter)
|
||||
return outputStream.toString()
|
||||
}
|
||||
|
||||
@@ -35,13 +34,14 @@ private fun exportCsv(
|
||||
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
|
||||
Database.SCANS_VERSION_NUMBER,
|
||||
Database.SCANS_SEQUENCE_SIZE,
|
||||
Database.SCANS_SEQUENCE_INDEX,
|
||||
Database.SCANS_SEQUENCE_ID,
|
||||
Database.SCANS_GTIN_COUNTRY,
|
||||
Database.SCANS_GTIN_ADD_ON,
|
||||
Database.SCANS_GTIN_PRICE,
|
||||
Database.SCANS_GTIN_ISSUE_NUMBER
|
||||
)
|
||||
val indices = columns.map {
|
||||
cursor.getColumnIndex(it)
|
||||
@@ -87,6 +87,8 @@ private fun Cursor.toCsvRecord(
|
||||
return sb.toString().toByteArray()
|
||||
}
|
||||
|
||||
private fun String.quoteAndEscape() = "\"${this
|
||||
.replace("\n", " ")
|
||||
.replace("\"", "\"\"")}\""
|
||||
private fun String.quoteAndEscape() = "\"${
|
||||
this
|
||||
.replace("\n", " ")
|
||||
.replace("\"", "\"\"")
|
||||
}\""
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
package de.markusfisch.android.binaryeye.database
|
||||
|
||||
import android.database.Cursor
|
||||
|
||||
// Overwrite Kotlin's ".use" function for Cursor because Cursor cannot
|
||||
// be cast to Closeable below API level 16. This should be removed when
|
||||
// the minSDK is increased to at least JELLY_BEAN (16).
|
||||
inline fun <R> Cursor.use(block: (Cursor) -> R): R = try {
|
||||
block.invoke(this)
|
||||
} finally {
|
||||
close()
|
||||
}
|
||||
|
||||
fun Cursor.getInt(name: String): Int {
|
||||
val idx = getColumnIndex(name)
|
||||
return if (idx < 0) 0 else getInt(idx)
|
||||
}
|
||||
|
||||
fun Cursor.getLong(name: String): Long {
|
||||
val idx = getColumnIndex(name)
|
||||
return if (idx < 0) 0L else getLong(idx)
|
||||
}
|
||||
|
||||
fun Cursor.getString(name: String): String {
|
||||
val idx = getColumnIndex(name)
|
||||
return if (idx < 0) "" else getString(idx) ?: ""
|
||||
}
|
||||
|
||||
fun Cursor.getBlob(name: String): ByteArray? {
|
||||
val idx = getColumnIndex(name)
|
||||
return if (idx < 0) null else getBlob(idx)
|
||||
}
|
||||
@@ -5,7 +5,6 @@ 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 {
|
||||
@@ -25,7 +24,7 @@ class Database {
|
||||
FROM $SCANS
|
||||
${getWhereClause(query)}
|
||||
ORDER BY $SCANS_DATETIME DESC
|
||||
""", getWhereArguments(query)
|
||||
""".trimMargin(), getWhereArguments(query)
|
||||
)
|
||||
|
||||
fun getScansDetailed(query: String? = null): Cursor? = db.rawQuery(
|
||||
@@ -37,17 +36,18 @@ class Database {
|
||||
$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
|
||||
$SCANS_VERSION_NUMBER,
|
||||
$SCANS_SEQUENCE_SIZE,
|
||||
$SCANS_SEQUENCE_INDEX,
|
||||
$SCANS_SEQUENCE_ID,
|
||||
$SCANS_GTIN_COUNTRY,
|
||||
$SCANS_GTIN_ADD_ON,
|
||||
$SCANS_GTIN_PRICE,
|
||||
$SCANS_GTIN_ISSUE_NUMBER
|
||||
FROM $SCANS
|
||||
${getWhereClause(query)}
|
||||
ORDER BY $SCANS_DATETIME DESC
|
||||
""", getWhereArguments(query)
|
||||
""".trimMargin(), getWhereArguments(query)
|
||||
)
|
||||
|
||||
private fun getWhereClause(
|
||||
@@ -78,16 +78,17 @@ class Database {
|
||||
$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
|
||||
$SCANS_VERSION_NUMBER,
|
||||
$SCANS_SEQUENCE_SIZE,
|
||||
$SCANS_SEQUENCE_INDEX,
|
||||
$SCANS_SEQUENCE_ID,
|
||||
$SCANS_GTIN_COUNTRY,
|
||||
$SCANS_GTIN_ADD_ON,
|
||||
$SCANS_GTIN_PRICE,
|
||||
$SCANS_GTIN_ISSUE_NUMBER
|
||||
FROM $SCANS
|
||||
WHERE $SCANS_ID = ?
|
||||
""", arrayOf("$id")
|
||||
""".trimMargin(), arrayOf("$id")
|
||||
)?.use {
|
||||
if (it.moveToFirst()) {
|
||||
Scan(
|
||||
@@ -95,13 +96,14 @@ class Database {
|
||||
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.getInt(SCANS_VERSION_NUMBER),
|
||||
it.getInt(SCANS_SEQUENCE_SIZE),
|
||||
it.getInt(SCANS_SEQUENCE_INDEX),
|
||||
it.getString(SCANS_SEQUENCE_ID),
|
||||
it.getString(SCANS_GTIN_COUNTRY),
|
||||
it.getString(SCANS_GTIN_ADD_ON),
|
||||
it.getString(SCANS_GTIN_PRICE),
|
||||
it.getString(SCANS_GTIN_ISSUE_NUMBER),
|
||||
it.getString(SCANS_DATETIME),
|
||||
it.getLong(SCANS_ID)
|
||||
)
|
||||
@@ -110,47 +112,49 @@ class Database {
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
fun insertScan(scan: Scan): Long = db.insert(
|
||||
SCANS,
|
||||
null,
|
||||
ContentValues().apply {
|
||||
put(SCANS_DATETIME, scan.dateTime)
|
||||
val isRaw = scan.raw != null
|
||||
if (isRaw) {
|
||||
put(SCANS_CONTENT, "")
|
||||
put(SCANS_RAW, scan.raw)
|
||||
} else {
|
||||
put(SCANS_CONTENT, scan.content)
|
||||
}
|
||||
put(SCANS_FORMAT, scan.format)
|
||||
scan.errorCorrectionLevel?.let {
|
||||
put(SCANS_ERROR_CORRECTION_LEVEL, it)
|
||||
}
|
||||
put(SCANS_VERSION_NUMBER, scan.versionNumber)
|
||||
put(SCANS_SEQUENCE_SIZE, scan.sequenceSize)
|
||||
put(SCANS_SEQUENCE_INDEX, scan.sequenceIndex)
|
||||
put(SCANS_SEQUENCE_ID, scan.sequenceId)
|
||||
scan.country?.let { put(SCANS_GTIN_COUNTRY, it) }
|
||||
scan.addOn?.let { put(SCANS_GTIN_ADD_ON, it) }
|
||||
scan.price?.let { put(SCANS_GTIN_PRICE, it) }
|
||||
scan.issueNumber?.let { put(SCANS_GTIN_ISSUE_NUMBER, it) }
|
||||
if (prefs.ignoreConsecutiveDuplicates) {
|
||||
val id = getIdOfLastScan(
|
||||
get(SCANS_CONTENT) as String,
|
||||
if (isRaw) 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
|
||||
): Long = db.rawQuery(
|
||||
"""SELECT
|
||||
$SCANS_ID,
|
||||
$SCANS_CONTENT,
|
||||
$SCANS_RAW,
|
||||
@@ -158,21 +162,20 @@ class Database {
|
||||
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
|
||||
}
|
||||
""".trimMargin(), null
|
||||
)?.use {
|
||||
if (it.count > 0 &&
|
||||
it.moveToFirst() &&
|
||||
it.getString(SCANS_CONTENT) == content &&
|
||||
(raw == null || it.getBlob(SCANS_RAW)
|
||||
?.contentEquals(raw) == true) &&
|
||||
it.getString(SCANS_FORMAT) == format
|
||||
) {
|
||||
it.getLong(SCANS_ID)
|
||||
} else {
|
||||
0L
|
||||
}
|
||||
} ?: 0L
|
||||
|
||||
fun removeScan(id: Long) {
|
||||
db.delete(SCANS, "$SCANS_ID = ?", arrayOf("$id"))
|
||||
@@ -189,7 +192,7 @@ class Database {
|
||||
}
|
||||
|
||||
private class OpenHelper(context: Context) :
|
||||
SQLiteOpenHelper(context, FILE_NAME, null, 4) {
|
||||
SQLiteOpenHelper(context, FILE_NAME, null, 5) {
|
||||
override fun onCreate(db: SQLiteDatabase) {
|
||||
createScans(db)
|
||||
}
|
||||
@@ -208,6 +211,9 @@ class Database {
|
||||
if (oldVersion < 4) {
|
||||
addNameColumn(db)
|
||||
}
|
||||
if (oldVersion < 5) {
|
||||
migrateToZxingCpp(db)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -221,6 +227,7 @@ class Database {
|
||||
const val SCANS_RAW = "raw"
|
||||
const val SCANS_FORMAT = "format"
|
||||
const val SCANS_ERROR_CORRECTION_LEVEL = "error_correction_level"
|
||||
const val SCANS_VERSION_NUMBER = "version_number"
|
||||
const val SCANS_ISSUE_NUMBER = "issue_number"
|
||||
const val SCANS_ORIENTATION = "orientation"
|
||||
const val SCANS_OTHER_META_DATA = "other_meta_data"
|
||||
@@ -228,9 +235,16 @@ class Database {
|
||||
const val SCANS_POSSIBLE_COUNTRY = "possible_country"
|
||||
const val SCANS_SUGGESTED_PRICE = "suggested_price"
|
||||
const val SCANS_UPC_EAN_EXTENSION = "upc_ean_extension"
|
||||
const val SCANS_SEQUENCE_SIZE = "sequence_size"
|
||||
const val SCANS_SEQUENCE_INDEX = "sequence_index"
|
||||
const val SCANS_SEQUENCE_ID = "sequence_id"
|
||||
const val SCANS_GTIN_COUNTRY = "gtin_country"
|
||||
const val SCANS_GTIN_ADD_ON = "gtin_add_on"
|
||||
const val SCANS_GTIN_PRICE = "gtin_price"
|
||||
const val SCANS_GTIN_ISSUE_NUMBER = "gtin_issue_number"
|
||||
|
||||
private fun createScans(db: SQLiteDatabase) {
|
||||
db.execSQL("DROP TABLE IF EXISTS $SCANS")
|
||||
db.execSQL("DROP TABLE IF EXISTS $SCANS".trimMargin())
|
||||
db.execSQL(
|
||||
"""CREATE TABLE $SCANS (
|
||||
$SCANS_ID INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
@@ -240,46 +254,56 @@ class Database {
|
||||
$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
|
||||
)"""
|
||||
$SCANS_VERSION_NUMBER INTEGER,
|
||||
$SCANS_SEQUENCE_SIZE INTEGER,
|
||||
$SCANS_SEQUENCE_INDEX INTEGER,
|
||||
$SCANS_SEQUENCE_ID TEXT,
|
||||
$SCANS_GTIN_COUNTRY TEXT,
|
||||
$SCANS_GTIN_ADD_ON TEXT,
|
||||
$SCANS_GTIN_PRICE TEXT,
|
||||
$SCANS_GTIN_ISSUE_NUMBER TEXT
|
||||
)""".trimMargin()
|
||||
)
|
||||
}
|
||||
|
||||
private fun addRawColumn(db: SQLiteDatabase) {
|
||||
db.execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_RAW BLOB")
|
||||
db.execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_RAW BLOB".trimMargin())
|
||||
}
|
||||
|
||||
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")
|
||||
db.apply {
|
||||
execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_ERROR_CORRECTION_LEVEL TEXT".trimMargin())
|
||||
execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_ISSUE_NUMBER INT".trimMargin())
|
||||
execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_ORIENTATION INT".trimMargin())
|
||||
execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_OTHER_META_DATA TEXT".trimMargin())
|
||||
execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_PDF417_EXTRA_METADATA TEXT".trimMargin())
|
||||
execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_POSSIBLE_COUNTRY TEXT".trimMargin())
|
||||
execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_SUGGESTED_PRICE TEXT".trimMargin())
|
||||
execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_UPC_EAN_EXTENSION TEXT".trimMargin())
|
||||
}
|
||||
}
|
||||
|
||||
private fun addNameColumn(db: SQLiteDatabase) {
|
||||
db.execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_NAME TEXT")
|
||||
db.execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_NAME TEXT".trimMargin())
|
||||
}
|
||||
|
||||
private fun migrateToZxingCpp(db: SQLiteDatabase) {
|
||||
db.apply {
|
||||
execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_VERSION_NUMBER INTEGER".trimMargin())
|
||||
execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_SEQUENCE_SIZE INTEGER".trimMargin())
|
||||
execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_SEQUENCE_INDEX INTEGER".trimMargin())
|
||||
execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_SEQUENCE_ID TEXT".trimMargin())
|
||||
execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_GTIN_COUNTRY TEXT".trimMargin())
|
||||
execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_GTIN_ADD_ON TEXT".trimMargin())
|
||||
execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_GTIN_PRICE TEXT".trimMargin())
|
||||
execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_GTIN_ISSUE_NUMBER TEXT".trimMargin())
|
||||
execSQL("UPDATE $SCANS SET $SCANS_SEQUENCE_SIZE = -1")
|
||||
execSQL("UPDATE $SCANS SET $SCANS_SEQUENCE_INDEX = -1")
|
||||
execSQL("UPDATE $SCANS SET $SCANS_GTIN_COUNTRY = $SCANS_POSSIBLE_COUNTRY")
|
||||
execSQL("UPDATE $SCANS SET $SCANS_GTIN_ADD_ON = $SCANS_UPC_EAN_EXTENSION")
|
||||
execSQL("UPDATE $SCANS SET $SCANS_GTIN_PRICE = $SCANS_SUGGESTED_PRICE")
|
||||
execSQL("UPDATE $SCANS SET $SCANS_GTIN_ISSUE_NUMBER = $SCANS_ISSUE_NUMBER")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
)
|
||||
|
||||
@@ -2,20 +2,19 @@ package de.markusfisch.android.binaryeye.database
|
||||
|
||||
import android.app.Activity
|
||||
import android.os.Environment
|
||||
import de.markusfisch.android.binaryeye.app.writeExternalFile
|
||||
import de.markusfisch.android.binaryeye.io.writeExternalFile
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
|
||||
fun exportDatabase(activity: Activity, fileName: String): Boolean {
|
||||
fun Activity.exportDatabase(fileName: String): Boolean {
|
||||
val dbFile = File(
|
||||
Environment.getDataDirectory(),
|
||||
"//data//${activity.packageName}//databases//${Database.FILE_NAME}"
|
||||
"//data//${packageName}//databases//${Database.FILE_NAME}"
|
||||
)
|
||||
if (!dbFile.exists()) {
|
||||
return false
|
||||
}
|
||||
return writeExternalFile(
|
||||
activity,
|
||||
fileName,
|
||||
"application/vnd.sqlite3"
|
||||
) {
|
||||
|
||||
@@ -2,24 +2,23 @@ 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 de.markusfisch.android.binaryeye.content.toHexString
|
||||
import de.markusfisch.android.binaryeye.io.writeExternalFile
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONException
|
||||
import org.json.JSONObject
|
||||
|
||||
fun exportJson(
|
||||
context: Context,
|
||||
fun Context.exportJson(
|
||||
name: String,
|
||||
cursor: Cursor
|
||||
) = writeExternalFile(context, name, "application/json") { outputStream ->
|
||||
exportJson(cursor)?.let {
|
||||
) = writeExternalFile(name, "application/json") { outputStream ->
|
||||
cursor.exportJson()?.let {
|
||||
outputStream.write(it.toByteArray())
|
||||
}
|
||||
}
|
||||
|
||||
fun exportJson(cursor: Cursor): String? {
|
||||
if (!cursor.moveToFirst()) {
|
||||
fun Cursor.exportJson(): String? {
|
||||
if (!moveToFirst()) {
|
||||
return null
|
||||
}
|
||||
val columns = arrayOf(
|
||||
@@ -27,30 +26,31 @@ fun exportJson(cursor: Cursor): String? {
|
||||
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
|
||||
Database.SCANS_VERSION_NUMBER,
|
||||
Database.SCANS_SEQUENCE_SIZE,
|
||||
Database.SCANS_SEQUENCE_INDEX,
|
||||
Database.SCANS_SEQUENCE_ID,
|
||||
Database.SCANS_GTIN_COUNTRY,
|
||||
Database.SCANS_GTIN_ADD_ON,
|
||||
Database.SCANS_GTIN_PRICE,
|
||||
Database.SCANS_GTIN_ISSUE_NUMBER
|
||||
)
|
||||
val indices = columns.map {
|
||||
Pair(cursor.getColumnIndex(it), it)
|
||||
Pair(getColumnIndex(it), it)
|
||||
}
|
||||
val contentIndex = cursor.getColumnIndex(Database.SCANS_CONTENT)
|
||||
val rawIndex = cursor.getColumnIndex(Database.SCANS_RAW)
|
||||
val contentIndex = getColumnIndex(Database.SCANS_CONTENT)
|
||||
val rawIndex = getColumnIndex(Database.SCANS_RAW)
|
||||
val root = JSONArray()
|
||||
do {
|
||||
var deviation: Pair<Int, String>? = null
|
||||
if (cursor.getString(contentIndex)?.isEmpty() == true) {
|
||||
if (getString(contentIndex)?.isEmpty() == true) {
|
||||
deviation = Pair(
|
||||
contentIndex,
|
||||
cursor.getBlob(rawIndex).toHexString()
|
||||
getBlob(rawIndex).toHexString()
|
||||
)
|
||||
}
|
||||
root.put(cursor.toJsonObject(indices, deviation))
|
||||
} while (cursor.moveToNext())
|
||||
root.put(toJsonObject(indices, deviation))
|
||||
} while (moveToNext())
|
||||
return root.toString()
|
||||
}
|
||||
|
||||
|
||||
@@ -1,42 +1,32 @@
|
||||
package de.markusfisch.android.binaryeye.database
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.os.Build
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
import android.text.format.DateFormat
|
||||
import com.google.zxing.Result
|
||||
import com.google.zxing.ResultMetadataType
|
||||
import de.markusfisch.android.zxingcpp.ZxingCpp
|
||||
import de.markusfisch.android.zxingcpp.ZxingCpp.ContentType
|
||||
import de.markusfisch.android.zxingcpp.ZxingCpp.Result
|
||||
|
||||
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(),
|
||||
val errorCorrectionLevel: String? = null,
|
||||
val versionNumber: Int = 0,
|
||||
val sequenceSize: Int = -1,
|
||||
val sequenceIndex: Int = -1,
|
||||
val sequenceId: String = "",
|
||||
val country: String? = null,
|
||||
val addOn: String? = null,
|
||||
val price: String? = null,
|
||||
val issueNumber: String? = null,
|
||||
val dateTime: 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
|
||||
// this isn't handled well by Kotlin.
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) {
|
||||
return true
|
||||
@@ -48,70 +38,76 @@ data class Scan(
|
||||
other as Scan
|
||||
|
||||
return id == other.id &&
|
||||
timestamp == other.timestamp &&
|
||||
dateTime == other.dateTime &&
|
||||
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
|
||||
versionNumber == other.versionNumber &&
|
||||
sequenceSize == other.sequenceSize &&
|
||||
sequenceIndex == other.sequenceIndex &&
|
||||
sequenceId == other.sequenceId &&
|
||||
country == other.country &&
|
||||
addOn == other.addOn &&
|
||||
price == other.price &&
|
||||
issueNumber == other.issueNumber
|
||||
}
|
||||
|
||||
// Needs to be overwritten manually, as ByteArray is an array and
|
||||
// this isn't handled well by Kotlin
|
||||
// this isn't handled well by Kotlin.
|
||||
override fun hashCode(): Int {
|
||||
var result = id.hashCode()
|
||||
result = 31 * result + timestamp.hashCode()
|
||||
result = 31 * result + dateTime.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 + versionNumber
|
||||
result = 31 * result + sequenceSize
|
||||
result = 31 * result + sequenceIndex
|
||||
result = 31 * result + sequenceId.hashCode()
|
||||
result = 31 * result + (country?.hashCode() ?: 0)
|
||||
result = 31 * result + (addOn?.hashCode() ?: 0)
|
||||
result = 31 * result + (price?.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),
|
||||
raw = parcel.readSizedByteArray(),
|
||||
format = parcel.readString() ?: "",
|
||||
errorCorrectionLevel = parcel.readString(),
|
||||
versionNumber = parcel.readInt(),
|
||||
sequenceSize = parcel.readInt(),
|
||||
sequenceIndex = parcel.readInt(),
|
||||
sequenceId = parcel.readString() ?: "",
|
||||
country = parcel.readString(),
|
||||
addOn = parcel.readString(),
|
||||
price = 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() ?: "",
|
||||
dateTime = 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)
|
||||
parcel.apply {
|
||||
writeString(content)
|
||||
writeSizedByteArray(raw)
|
||||
writeString(format)
|
||||
writeString(errorCorrectionLevel)
|
||||
writeInt(versionNumber)
|
||||
writeInt(sequenceSize)
|
||||
writeInt(sequenceIndex)
|
||||
writeString(sequenceId)
|
||||
writeString(country)
|
||||
writeString(addOn)
|
||||
writeString(price)
|
||||
writeString(issueNumber)
|
||||
writeString(dateTime)
|
||||
writeLong(id)
|
||||
}
|
||||
}
|
||||
|
||||
override fun describeContents() = 0
|
||||
@@ -125,43 +121,87 @@ data class Scan(
|
||||
}
|
||||
}
|
||||
|
||||
private fun getDateTime(time: Long = System.currentTimeMillis()) = DateFormat.format(
|
||||
"yyyy-MM-dd HH:mm:ss",
|
||||
fun Result.toScan(): Scan {
|
||||
val content: String
|
||||
val raw: ByteArray?
|
||||
when (contentType) {
|
||||
ContentType.GS1,
|
||||
ContentType.ISO15434,
|
||||
ContentType.TEXT -> {
|
||||
content = text
|
||||
raw = null
|
||||
}
|
||||
else -> {
|
||||
content = ""
|
||||
raw = rawBytes
|
||||
}
|
||||
}
|
||||
return Scan(
|
||||
content,
|
||||
raw,
|
||||
format,
|
||||
ecLevel,
|
||||
versionNumber,
|
||||
sequenceSize,
|
||||
sequenceIndex,
|
||||
sequenceId,
|
||||
gtin?.country,
|
||||
gtin?.addOn,
|
||||
gtin?.price,
|
||||
gtin?.issueNumber
|
||||
)
|
||||
}
|
||||
|
||||
fun Scan.recreate(
|
||||
size: Int = 128,
|
||||
margin: Int = -1
|
||||
): Bitmap? {
|
||||
if (content.isEmpty()) {
|
||||
return null
|
||||
}
|
||||
val ecLevel = when (errorCorrectionLevel) {
|
||||
"L" -> 0
|
||||
"M" -> 1
|
||||
"Q" -> 2
|
||||
"H" -> 3
|
||||
else -> -1
|
||||
}
|
||||
return try {
|
||||
val format = ZxingCpp.Format.valueOf(format)
|
||||
ZxingCpp.encodeAsBitmap(
|
||||
content, format, size, size, margin, ecLevel
|
||||
)
|
||||
} catch (_: RuntimeException) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun getDateTime(
|
||||
time: Long = System.currentTimeMillis()
|
||||
) = DateFormat.format(
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2) {
|
||||
// Up to API level 17, 'k' was interpreted as 'H', which wasn't
|
||||
// implemented until API level 18.
|
||||
"yyyy-MM-dd kk:mm:ss"
|
||||
} else {
|
||||
"yyyy-MM-dd HH:mm:ss"
|
||||
},
|
||||
time
|
||||
).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?) {
|
||||
private fun Parcel.writeSizedByteArray(array: ByteArray?) {
|
||||
val size = array?.size ?: 0
|
||||
parcel.writeInt(size)
|
||||
writeInt(size)
|
||||
if (size > 0) {
|
||||
parcel.writeByteArray(array)
|
||||
writeByteArray(array)
|
||||
}
|
||||
}
|
||||
|
||||
private fun readByteArray(parcel: Parcel): ByteArray? {
|
||||
val size = parcel.readInt()
|
||||
private fun Parcel.readSizedByteArray(): ByteArray? {
|
||||
val size = readInt()
|
||||
return if (size > 0) {
|
||||
val array = ByteArray(size)
|
||||
parcel.readByteArray(array)
|
||||
readByteArray(array)
|
||||
array
|
||||
} else {
|
||||
null
|
||||
|
||||
@@ -2,22 +2,28 @@ package de.markusfisch.android.binaryeye.fragment
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.AlertDialog
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.support.v4.app.Fragment
|
||||
import android.view.*
|
||||
import android.widget.EditText
|
||||
import com.google.zxing.BarcodeFormat
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.app.*
|
||||
import de.markusfisch.android.binaryeye.app.hasWritePermission
|
||||
import de.markusfisch.android.binaryeye.content.copyToClipboard
|
||||
import de.markusfisch.android.binaryeye.content.shareFile
|
||||
import de.markusfisch.android.binaryeye.content.shareText
|
||||
import de.markusfisch.android.binaryeye.io.addSuffixIfNotGiven
|
||||
import de.markusfisch.android.binaryeye.io.toSaveResult
|
||||
import de.markusfisch.android.binaryeye.io.writeExternalFile
|
||||
import de.markusfisch.android.binaryeye.view.doOnApplyWindowInsets
|
||||
import de.markusfisch.android.binaryeye.view.setPaddingFromWindowInsets
|
||||
import de.markusfisch.android.binaryeye.widget.ConfinedScalingImageView
|
||||
import de.markusfisch.android.binaryeye.widget.toast
|
||||
import de.markusfisch.android.binaryeye.zxing.Zxing
|
||||
import kotlinx.coroutines.Dispatchers.Main
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import de.markusfisch.android.zxingcpp.ZxingCpp
|
||||
import de.markusfisch.android.zxingcpp.ZxingCpp.Format
|
||||
import kotlinx.coroutines.*
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
@@ -26,13 +32,13 @@ import java.util.*
|
||||
|
||||
class BarcodeFragment : Fragment() {
|
||||
private enum class FileType {
|
||||
PNG, SVG
|
||||
PNG, SVG, TXT
|
||||
}
|
||||
|
||||
private var barcodeBitmap: Bitmap? = null
|
||||
private var barcodeSvg: String? = null
|
||||
private var content: String = ""
|
||||
private var format: BarcodeFormat? = null
|
||||
private val parentJob = Job()
|
||||
private val scope = CoroutineScope(Dispatchers.IO + parentJob)
|
||||
|
||||
private lateinit var barcode: Barcode
|
||||
|
||||
override fun onCreate(state: Bundle?) {
|
||||
super.onCreate(state)
|
||||
@@ -53,13 +59,13 @@ class BarcodeFragment : Fragment() {
|
||||
false
|
||||
)
|
||||
|
||||
val args = arguments ?: return view
|
||||
val content = args.getString(CONTENT) ?: return view
|
||||
val format = args.getSerializable(FORMAT) as BarcodeFormat? ?: return view
|
||||
val size = args.getInt(SIZE)
|
||||
val bitmap: Bitmap
|
||||
try {
|
||||
barcodeBitmap = Zxing.encodeAsBitmap(content, format, size, size)
|
||||
barcodeSvg = Zxing.encodeAsSvg(content, format, size, size)
|
||||
barcode = arguments?.toProperties() ?: throw IllegalArgumentException(
|
||||
"Illegal arguments"
|
||||
)
|
||||
// Catch exceptions from encoding.
|
||||
bitmap = barcode.bitmap
|
||||
} catch (e: Exception) {
|
||||
var message = e.message
|
||||
if (message == null || message.isEmpty()) {
|
||||
@@ -71,22 +77,21 @@ class BarcodeFragment : Fragment() {
|
||||
fragmentManager.popBackStack()
|
||||
return null
|
||||
}
|
||||
this.content = content
|
||||
this.format = format
|
||||
|
||||
val imageView = view.findViewById<ConfinedScalingImageView>(
|
||||
R.id.barcode
|
||||
)
|
||||
imageView.setImageBitmap(barcodeBitmap)
|
||||
imageView.setImageBitmap(bitmap)
|
||||
imageView.post {
|
||||
// make sure to invoke this after ScalingImageView.onLayout()
|
||||
// Make sure to invoke this after ScalingImageView.onLayout().
|
||||
imageView.minWidth /= 2f
|
||||
}
|
||||
|
||||
view.findViewById<View>(R.id.share).setOnClickListener {
|
||||
val bitmap = barcodeBitmap
|
||||
bitmap?.let {
|
||||
share(bitmap)
|
||||
it.context?.apply {
|
||||
pickFileType(R.string.share_as) { fileType ->
|
||||
shareAs(fileType)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,34 +103,75 @@ class BarcodeFragment : Fragment() {
|
||||
return view
|
||||
}
|
||||
|
||||
private fun Bundle.toProperties() = Barcode(
|
||||
getString(CONTENT) ?: throw IllegalArgumentException(
|
||||
"content cannot be null"
|
||||
),
|
||||
Format.valueOf(
|
||||
getString(FORMAT) ?: throw IllegalArgumentException(
|
||||
"format cannot be null"
|
||||
)
|
||||
),
|
||||
getInt(SIZE),
|
||||
getInt(EC_LEVEL)
|
||||
)
|
||||
|
||||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
parentJob.cancel()
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
inflater.inflate(R.menu.fragment_barcode, menu)
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
return when (item.itemId) {
|
||||
R.id.export_svg -> {
|
||||
askForFileNameAndSave(FileType.SVG)
|
||||
R.id.copy_to_clipboard -> {
|
||||
context.apply {
|
||||
copyToClipboard(barcode.text)
|
||||
toast(R.string.copied_to_clipboard)
|
||||
}
|
||||
true
|
||||
}
|
||||
R.id.export_png -> {
|
||||
askForFileNameAndSave(FileType.PNG)
|
||||
R.id.export_to_file -> {
|
||||
context.pickFileType(R.string.export_as) {
|
||||
askForFileNameAndSave(it)
|
||||
}
|
||||
true
|
||||
}
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
}
|
||||
|
||||
// dialogs do not have a parent view
|
||||
private fun Context.pickFileType(
|
||||
title: Int,
|
||||
action: (FileType) -> Unit
|
||||
) {
|
||||
val fileTypes = FileType.values()
|
||||
AlertDialog.Builder(this)
|
||||
.setTitle(title)
|
||||
.setItems(fileTypes.map { it.name }.toTypedArray()) { _, which ->
|
||||
action(fileTypes[which])
|
||||
}
|
||||
.show()
|
||||
}
|
||||
|
||||
// Dialogs do not have a parent view.
|
||||
@SuppressLint("InflateParams")
|
||||
private fun askForFileNameAndSave(fileType: FileType) {
|
||||
val ac = activity ?: return
|
||||
if (!hasWritePermission(ac)) {
|
||||
// Write permission is only required before Android Q.
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q &&
|
||||
!ac.hasWritePermission { askForFileNameAndSave(fileType) }
|
||||
) {
|
||||
return
|
||||
}
|
||||
val view = ac.layoutInflater.inflate(R.layout.dialog_save_file, null)
|
||||
val editText = view.findViewById<EditText>(R.id.file_name)
|
||||
editText.setText(encodeFileName("${format.toString()}_$content"))
|
||||
editText.setText(
|
||||
encodeFileName("${barcode.format}_${barcode.content}")
|
||||
)
|
||||
AlertDialog.Builder(ac)
|
||||
.setView(view)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
@@ -133,17 +179,21 @@ class BarcodeFragment : Fragment() {
|
||||
when (fileType) {
|
||||
FileType.PNG -> saveAs(
|
||||
addSuffixIfNotGiven(fileName, ".png"),
|
||||
"image/png"
|
||||
MIME_PNG
|
||||
) {
|
||||
barcodeBitmap?.saveAsPng(it)
|
||||
barcode.bitmap.saveAsPng(it)
|
||||
}
|
||||
FileType.SVG -> saveAs(
|
||||
addSuffixIfNotGiven(fileName, ".svg"),
|
||||
"image/svg+xmg"
|
||||
MIME_SVG
|
||||
) { outputStream ->
|
||||
barcodeSvg?.let {
|
||||
outputStream.write(it.toByteArray())
|
||||
}
|
||||
outputStream.write(barcode.svg.toByteArray())
|
||||
}
|
||||
FileType.TXT -> saveAs(
|
||||
addSuffixIfNotGiven(fileName, ".txt"),
|
||||
MIME_TXT
|
||||
) { outputStream ->
|
||||
outputStream.write(barcode.text.toByteArray())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -158,18 +208,26 @@ class BarcodeFragment : Fragment() {
|
||||
write: (outputStream: OutputStream) -> Unit
|
||||
) {
|
||||
val ac = activity ?: return
|
||||
GlobalScope.launch {
|
||||
val message = writeExternalFile(ac, fileName, mimeType, write).toSaveResult()
|
||||
GlobalScope.launch(Main) {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
val message = ac.writeExternalFile(fileName, mimeType, write).toSaveResult()
|
||||
withContext(Dispatchers.Main) {
|
||||
ac.toast(message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun share(bitmap: Bitmap) {
|
||||
GlobalScope.launch {
|
||||
private fun Context.shareAs(fileType: FileType) {
|
||||
when (fileType) {
|
||||
FileType.PNG -> share(barcode.bitmap)
|
||||
FileType.SVG -> shareText(barcode.svg, MIME_SVG)
|
||||
FileType.TXT -> shareText(barcode.text)
|
||||
}
|
||||
}
|
||||
|
||||
private fun Context.share(bitmap: Bitmap) {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
val file = File(
|
||||
context.externalCacheDir,
|
||||
externalCacheDir,
|
||||
"shared_barcode.png"
|
||||
)
|
||||
val success = try {
|
||||
@@ -180,11 +238,11 @@ class BarcodeFragment : Fragment() {
|
||||
} catch (e: IOException) {
|
||||
false
|
||||
}
|
||||
GlobalScope.launch(Main) {
|
||||
withContext(Dispatchers.Main) {
|
||||
if (success) {
|
||||
shareFile(context, file, "image/png")
|
||||
shareFile(file, "image/png")
|
||||
} else {
|
||||
activity?.toast(R.string.error_saving_file)
|
||||
toast(R.string.error_saving_file)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -194,16 +252,22 @@ class BarcodeFragment : Fragment() {
|
||||
private const val CONTENT = "content"
|
||||
private const val FORMAT = "format"
|
||||
private const val SIZE = "size"
|
||||
private const val EC_LEVEL = "ec_level"
|
||||
private const val 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
|
||||
format: Format,
|
||||
size: Int,
|
||||
ecLevel: Int = -1
|
||||
): Fragment {
|
||||
val args = Bundle()
|
||||
args.putString(CONTENT, content)
|
||||
args.putSerializable(FORMAT, format)
|
||||
args.putString(FORMAT, format.name)
|
||||
args.putInt(SIZE, size)
|
||||
args.putInt(EC_LEVEL, ecLevel)
|
||||
val fragment = BarcodeFragment()
|
||||
fragment.arguments = args
|
||||
return fragment
|
||||
@@ -211,10 +275,29 @@ class BarcodeFragment : Fragment() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun Bitmap.saveAsPng(outputStream: OutputStream, quality: Int = 90) {
|
||||
this.compress(Bitmap.CompressFormat.PNG, quality, outputStream)
|
||||
private data class Barcode(
|
||||
val content: String,
|
||||
val format: Format,
|
||||
val size: Int,
|
||||
val ecLevel: Int
|
||||
) {
|
||||
val bitmap: Bitmap
|
||||
get() = ZxingCpp.encodeAsBitmap(
|
||||
content, format, size, size, -1, ecLevel
|
||||
)
|
||||
val svg: String
|
||||
get() = ZxingCpp.encodeAsSvg(
|
||||
content, format, -1, ecLevel
|
||||
)
|
||||
val text: String
|
||||
get() = ZxingCpp.encodeAsText(
|
||||
content, format, -1, ecLevel
|
||||
)
|
||||
}
|
||||
|
||||
private fun Bitmap.saveAsPng(outputStream: OutputStream, quality: Int = 90) =
|
||||
compress(Bitmap.CompressFormat.PNG, quality, outputStream)
|
||||
|
||||
private val fileNameCharacters = "[^A-Za-z0-9]".toRegex()
|
||||
private fun encodeFileName(name: String): String =
|
||||
fileNameCharacters.replace(name, "_").take(16).trim('_').toLowerCase(Locale.getDefault())
|
||||
fileNameCharacters.replace(name, "_").take(16).trim('_').lowercase(Locale.getDefault())
|
||||
|
||||
@@ -5,39 +5,52 @@ import android.os.Bundle
|
||||
import android.support.design.widget.FloatingActionButton
|
||||
import android.support.v4.app.Fragment
|
||||
import android.text.Editable
|
||||
import android.text.Html
|
||||
import android.text.TextWatcher
|
||||
import android.text.method.LinkMovementMethod
|
||||
import android.view.*
|
||||
import android.widget.EditText
|
||||
import android.widget.TableLayout
|
||||
import android.widget.TableRow
|
||||
import android.widget.TextView
|
||||
import android.widget.*
|
||||
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.actions.wifi.WifiConnector
|
||||
import de.markusfisch.android.binaryeye.activity.MainActivity
|
||||
import de.markusfisch.android.binaryeye.adapter.prettifyFormatName
|
||||
import de.markusfisch.android.binaryeye.app.*
|
||||
import de.markusfisch.android.binaryeye.content.copyToClipboard
|
||||
import de.markusfisch.android.binaryeye.content.shareText
|
||||
import de.markusfisch.android.binaryeye.content.toHexString
|
||||
import de.markusfisch.android.binaryeye.database.Scan
|
||||
import de.markusfisch.android.binaryeye.database.recreate
|
||||
import de.markusfisch.android.binaryeye.io.askForFileName
|
||||
import de.markusfisch.android.binaryeye.io.toSaveResult
|
||||
import de.markusfisch.android.binaryeye.io.writeExternalFile
|
||||
import de.markusfisch.android.binaryeye.view.setPaddingFromWindowInsets
|
||||
import de.markusfisch.android.binaryeye.widget.toast
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.*
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
class DecodeFragment : Fragment() {
|
||||
private lateinit var contentView: EditText
|
||||
private lateinit var formatView: TextView
|
||||
private lateinit var dataView: TableLayout
|
||||
private lateinit var metaView: TableLayout
|
||||
private lateinit var hexView: TextView
|
||||
private lateinit var format: String
|
||||
private lateinit var stampView: TextView
|
||||
private lateinit var fab: FloatingActionButton
|
||||
|
||||
private val parentJob = Job()
|
||||
private val scope: CoroutineScope = CoroutineScope(Dispatchers.Main + parentJob)
|
||||
private val dp: Float
|
||||
get() = resources.displayMetrics.density
|
||||
private val content: String
|
||||
get() = contentView.text.toString()
|
||||
|
||||
private var closeAutomatically = false
|
||||
private var action = ActionRegistry.DEFAULT_ACTION
|
||||
private var isBinary = false
|
||||
private var raw: ByteArray = ByteArray(0)
|
||||
private var id = 0L
|
||||
|
||||
override fun onCreate(state: Bundle?) {
|
||||
@@ -58,18 +71,24 @@ class DecodeFragment : Fragment() {
|
||||
false
|
||||
)
|
||||
|
||||
val scan = arguments?.getParcelable(SCAN) as Scan?
|
||||
?: throw IllegalArgumentException("DecodeFragment needs a Scan")
|
||||
closeAutomatically = prefs.closeAutomatically &&
|
||||
activity?.intent?.hasExtra(MainActivity.DECODED) == true
|
||||
|
||||
val scan = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
arguments?.getParcelable(SCAN, Scan::class.java)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
arguments?.getParcelable(SCAN) as Scan?
|
||||
} ?: throw IllegalArgumentException("DecodeFragment needs a Scan")
|
||||
id = scan.id
|
||||
|
||||
val inputContent = scan.content
|
||||
isBinary = hasNonPrintableCharacters(
|
||||
inputContent
|
||||
) or inputContent.isEmpty()
|
||||
val raw = scan.raw ?: inputContent.toByteArray()
|
||||
isBinary = scan.raw != null
|
||||
raw = scan.raw ?: inputContent.toByteArray()
|
||||
format = scan.format
|
||||
|
||||
contentView = view.findViewById(R.id.content)
|
||||
stampView = view.findViewById(R.id.stamp)
|
||||
fab = view.findViewById(R.id.open)
|
||||
|
||||
if (!isBinary) {
|
||||
@@ -102,7 +121,7 @@ class DecodeFragment : Fragment() {
|
||||
executeAction(content.toByteArray())
|
||||
}
|
||||
} else {
|
||||
contentView.setText(R.string.binary_data)
|
||||
contentView.setText(String(raw).foldNonAlNum())
|
||||
contentView.isEnabled = false
|
||||
fab.setImageResource(R.drawable.ic_action_save)
|
||||
fab.setOnClickListener {
|
||||
@@ -110,25 +129,56 @@ class DecodeFragment : Fragment() {
|
||||
}
|
||||
}
|
||||
|
||||
val trackingLink = generateDpTrackingLink(raw, scan.format)
|
||||
if (trackingLink != null) {
|
||||
stampView.apply {
|
||||
text = trackingLink.fromHtml()
|
||||
isClickable = true
|
||||
movementMethod = LinkMovementMethod.getInstance()
|
||||
}
|
||||
} else {
|
||||
stampView.visibility = View.GONE
|
||||
}
|
||||
|
||||
formatView = view.findViewById(R.id.format)
|
||||
dataView = view.findViewById(R.id.data)
|
||||
metaView = view.findViewById(R.id.meta)
|
||||
hexView = view.findViewById(R.id.hex)
|
||||
|
||||
updateViewsAndAction(raw)
|
||||
|
||||
if (!isBinary) {
|
||||
fillDataView(dataView, inputContent)
|
||||
}
|
||||
|
||||
if (prefs.showMetaData) {
|
||||
fillMetaView(metaView, scan)
|
||||
}
|
||||
|
||||
view.findViewById<ImageView>(R.id.recreation).apply {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
val bitmap = if (prefs.showRecreation) {
|
||||
scan.recreate((200f * dp).roundToInt())
|
||||
} else null
|
||||
withContext(Dispatchers.Main) {
|
||||
if (bitmap != null) {
|
||||
setImageBitmap(bitmap)
|
||||
} else {
|
||||
visibility = View.GONE
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
(view.findViewById(R.id.inset_layout) as View).setPaddingFromWindowInsets()
|
||||
(view.findViewById(R.id.scroll_view) as View).setPaddingFromWindowInsets()
|
||||
|
||||
return view
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
parentJob.cancel()
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
private fun updateViewsAndAction(bytes: ByteArray) {
|
||||
@@ -139,7 +189,7 @@ class DecodeFragment : Fragment() {
|
||||
formatView.text = resources.getQuantityString(
|
||||
R.plurals.barcode_info,
|
||||
bytes.size,
|
||||
format,
|
||||
prettifyFormatName(format),
|
||||
bytes.size
|
||||
)
|
||||
hexView.text = if (prefs.showHexDump) hexDump(bytes) else ""
|
||||
@@ -156,36 +206,82 @@ class DecodeFragment : Fragment() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun fillDataView(tableLayout: TableLayout, content: String) {
|
||||
val items = LinkedHashMap<Int, String?>()
|
||||
if (action is WifiAction) {
|
||||
WifiConnector.parseMap(content)?.let { wifiData ->
|
||||
items.putAll(
|
||||
linkedMapOf(
|
||||
R.string.entry_type to getString(R.string.wifi_network),
|
||||
R.string.wifi_ssid to wifiData["S"],
|
||||
R.string.wifi_password to wifiData["P"],
|
||||
R.string.wifi_type to wifiData["T"],
|
||||
R.string.wifi_hidden to wifiData["H"],
|
||||
R.string.wifi_eap to wifiData["E"],
|
||||
R.string.wifi_identity to wifiData["I"],
|
||||
R.string.wifi_anonymous_identity to wifiData["A"],
|
||||
R.string.wifi_phase2 to wifiData["PH2"]
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
fillDataTable(tableLayout, items)
|
||||
}
|
||||
|
||||
private fun fillMetaView(tableLayout: TableLayout, scan: Scan) {
|
||||
val ctx = tableLayout.context
|
||||
val spaceBetween = (16f * ctx.resources.displayMetrics.density).toInt()
|
||||
var hasMeta = false
|
||||
sortedMapOf(
|
||||
val items = linkedMapOf(
|
||||
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 {
|
||||
R.string.sequence_size to scan.sequenceSize.positiveToString(),
|
||||
R.string.sequence_index to scan.sequenceIndex.positiveToString(),
|
||||
R.string.sequence_id to scan.sequenceId,
|
||||
R.string.gtin_country to scan.country,
|
||||
R.string.gtin_add_on to scan.addOn,
|
||||
R.string.gtin_price to scan.price,
|
||||
R.string.gtin_issue_number to scan.issueNumber,
|
||||
)
|
||||
if (scan.versionNumber > 0) {
|
||||
val versionString = if (scan.format == "QR_CODE") {
|
||||
getString(
|
||||
R.string.qr_version_and_modules,
|
||||
scan.versionNumber,
|
||||
scan.versionNumber * 4 + 17
|
||||
)
|
||||
} else {
|
||||
scan.versionNumber.toString()
|
||||
}
|
||||
items.putAll(
|
||||
linkedMapOf(
|
||||
R.string.barcode_version_number to versionString
|
||||
)
|
||||
)
|
||||
}
|
||||
fillDataTable(tableLayout, items)
|
||||
}
|
||||
|
||||
private fun fillDataTable(
|
||||
tableLayout: TableLayout,
|
||||
items: LinkedHashMap<Int, String?>
|
||||
) {
|
||||
if (items.isEmpty()) {
|
||||
tableLayout.visibility = View.GONE
|
||||
return
|
||||
}
|
||||
val ctx = tableLayout.context
|
||||
val spaceBetween = (16f * dp).roundToInt()
|
||||
items.forEach { item ->
|
||||
val text = item.value
|
||||
if (!text.isNullOrBlank()) {
|
||||
val tr = TableRow(ctx)
|
||||
val keyView = TextView(ctx)
|
||||
keyView.setText(item.key)
|
||||
val valueView = TextView(ctx)
|
||||
valueView.setPadding(spaceBetween, 0, 0, 0)
|
||||
valueView.text = it
|
||||
valueView.text = text
|
||||
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) {
|
||||
@@ -206,19 +302,23 @@ class DecodeFragment : Fragment() {
|
||||
return when (item.itemId) {
|
||||
R.id.copy_password -> {
|
||||
copyPasswordToClipboard()
|
||||
maybeBackOrFinish()
|
||||
true
|
||||
}
|
||||
R.id.copy_to_clipboard -> {
|
||||
copyToClipboard(content)
|
||||
copyToClipboard(textOrHex())
|
||||
maybeBackOrFinish()
|
||||
true
|
||||
}
|
||||
R.id.share -> {
|
||||
context?.also { shareText(it, content) }
|
||||
context?.apply {
|
||||
shareText(textOrHex())
|
||||
maybeBackOrFinish()
|
||||
}
|
||||
true
|
||||
}
|
||||
R.id.create -> {
|
||||
addFragment(
|
||||
fragmentManager,
|
||||
fragmentManager?.addFragment(
|
||||
EncodeFragment.newInstance(content, format)
|
||||
)
|
||||
true
|
||||
@@ -232,25 +332,37 @@ class DecodeFragment : Fragment() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun textOrHex() = if (isBinary) {
|
||||
raw.toHexString()
|
||||
} else {
|
||||
content
|
||||
}
|
||||
|
||||
private fun copyPasswordToClipboard() {
|
||||
val ac = action
|
||||
if (ac is WifiAction) {
|
||||
ac.password?.let { password ->
|
||||
activity?.apply {
|
||||
copyToClipboard(password)
|
||||
copyToClipboard(password, isSensitive = true)
|
||||
toast(R.string.copied_password_to_clipboard)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun copyToClipboard(text: String) {
|
||||
private fun copyToClipboard(text: String, isSensitive: Boolean = false) {
|
||||
activity?.apply {
|
||||
copyToClipboard(text)
|
||||
copyToClipboard(text, isSensitive)
|
||||
toast(R.string.copied_to_clipboard)
|
||||
}
|
||||
}
|
||||
|
||||
private fun maybeBackOrFinish() {
|
||||
if (closeAutomatically) {
|
||||
backOrFinish()
|
||||
}
|
||||
}
|
||||
|
||||
private fun backOrFinish() {
|
||||
val fm = fragmentManager
|
||||
if (fm != null && fm.backStackEntryCount > 0) {
|
||||
@@ -265,31 +377,38 @@ class DecodeFragment : Fragment() {
|
||||
if (content.isNotEmpty()) {
|
||||
if (action is WifiAction &&
|
||||
Build.VERSION.SDK_INT < Build.VERSION_CODES.Q &&
|
||||
!hasLocationPermission(ac)
|
||||
!ac.hasLocationPermission { executeAction(content) }
|
||||
) {
|
||||
return
|
||||
}
|
||||
scope.launch {
|
||||
action.execute(ac, content)
|
||||
maybeBackOrFinish()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun askForFileNameAndSave(raw: ByteArray) {
|
||||
val ac = activity ?: return
|
||||
if (!hasWritePermission(ac)) {
|
||||
// Write permission is only required before Android Q.
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q &&
|
||||
!ac.hasWritePermission { askForFileNameAndSave(raw) }
|
||||
) {
|
||||
return
|
||||
}
|
||||
scope.launch(Dispatchers.Main) {
|
||||
val name = ac.askForFileName() ?: return@launch
|
||||
val message = writeExternalFile(
|
||||
ac,
|
||||
name,
|
||||
"application/octet-stream"
|
||||
) {
|
||||
it.write(raw)
|
||||
}.toSaveResult()
|
||||
ac.toast(message)
|
||||
withContext(Dispatchers.IO) {
|
||||
val message = ac.writeExternalFile(
|
||||
name,
|
||||
"application/octet-stream"
|
||||
) {
|
||||
it.write(raw)
|
||||
}.toSaveResult()
|
||||
withContext(Dispatchers.Main) {
|
||||
ac.toast(message)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -306,6 +425,13 @@ class DecodeFragment : Fragment() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun Int.positiveToString() = if (this > -1) this.toString() else ""
|
||||
|
||||
private val nonAlNum = "[^a-zA-Z0-9]".toRegex()
|
||||
private val multipleDots = "[…]+".toRegex()
|
||||
private fun String.foldNonAlNum() = replace(nonAlNum, "…")
|
||||
.replace(multipleDots, "…")
|
||||
|
||||
private fun hexDump(bytes: ByteArray, charsPerLine: Int = 33): String {
|
||||
if (charsPerLine < 4 || bytes.isEmpty()) {
|
||||
return ""
|
||||
@@ -319,7 +445,7 @@ private fun hexDump(bytes: ByteArray, charsPerLine: Int = 33): String {
|
||||
while (true) {
|
||||
val ord = bytes[i]
|
||||
hex.append(String.format("%02X ", ord))
|
||||
ascii.append(if (ord > 31) ord.toChar() else " ")
|
||||
ascii.append(if (ord > 31) ord.toInt().toChar() else " ")
|
||||
++i
|
||||
val posInLine = i % itemsPerLine
|
||||
val atEnd = i >= len
|
||||
@@ -344,3 +470,79 @@ private fun hexDump(bytes: ByteArray, charsPerLine: Int = 33): String {
|
||||
}
|
||||
return dump.toString()
|
||||
}
|
||||
|
||||
private fun generateDpTrackingLink(raw: ByteArray, format: String): String? {
|
||||
// Check for Deutsche Post Matrixcode stamp.
|
||||
var isStamp = false
|
||||
var rawData = raw
|
||||
if (format == "DATA_MATRIX" &&
|
||||
raw.toString(Charsets.ISO_8859_1).startsWith("DEA5")
|
||||
) {
|
||||
if (raw.size == 47) {
|
||||
isStamp = true
|
||||
} else if (raw.size > 47) {
|
||||
// Transform back to original data.
|
||||
rawData = raw.toString(Charsets.UTF_8).toByteArray(
|
||||
Charsets.ISO_8859_1
|
||||
)
|
||||
if (rawData.size == 47) {
|
||||
isStamp = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!isStamp) {
|
||||
return null
|
||||
}
|
||||
|
||||
val hex = StringBuilder()
|
||||
hex.append(String.format("%02X", rawData[9]))
|
||||
hex.append(String.format("%02X", rawData[10]))
|
||||
hex.append(String.format("%02X", rawData[11]))
|
||||
hex.append(String.format("%02X", rawData[12]))
|
||||
hex.append(String.format("%02X", rawData[13]))
|
||||
hex.append(String.format("%X", (rawData[4].toInt() and 0x0f).toByte()))
|
||||
hex.append(String.format("%02X", rawData[5]))
|
||||
hex.append(String.format("%02X", rawData[6]))
|
||||
hex.append(String.format("%02X", rawData[7]))
|
||||
hex.append(String.format("%02X", rawData[8]))
|
||||
val hexString = hex.toString()
|
||||
val trackingNumber = hexString + String.format(
|
||||
"%X",
|
||||
crc4(hexString.toByteArray(Charsets.ISO_8859_1))
|
||||
)
|
||||
return "<a href=\"https://www.deutschepost.de/de/s/sendungsverfolgung/verfolgen.html?piececode=$trackingNumber\">Deutsche Post: $trackingNumber</a>"
|
||||
}
|
||||
|
||||
// CRC-4 with polynomial x^4 + x + 1.
|
||||
private fun crc4(input: ByteArray): Int {
|
||||
var crc = 0
|
||||
var i = 0
|
||||
while (i < input.size) {
|
||||
val c = input[i].toInt()
|
||||
var j = 0x80
|
||||
while (j != 0) {
|
||||
var bit = crc and 0x8
|
||||
crc = crc shl 1
|
||||
if (c and j != 0) {
|
||||
bit = bit xor 0x8
|
||||
}
|
||||
if (bit != 0) {
|
||||
crc = crc xor 0x3
|
||||
}
|
||||
j = j ushr 1
|
||||
}
|
||||
++i
|
||||
}
|
||||
crc = crc and 0xF
|
||||
return crc
|
||||
}
|
||||
|
||||
private fun String.fromHtml() = if (
|
||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.N
|
||||
) {
|
||||
Html.fromHtml(this, Html.FROM_HTML_MODE_LEGACY)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
Html.fromHtml(this)
|
||||
}
|
||||
|
||||
@@ -6,31 +6,34 @@ import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.*
|
||||
import com.google.zxing.BarcodeFormat
|
||||
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.hideSoftKeyboard
|
||||
import de.markusfisch.android.binaryeye.view.setPaddingFromWindowInsets
|
||||
import de.markusfisch.android.zxingcpp.ZxingCpp.Format
|
||||
|
||||
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,
|
||||
BarcodeFormat.CODE_39,
|
||||
BarcodeFormat.CODE_128,
|
||||
BarcodeFormat.DATA_MATRIX,
|
||||
BarcodeFormat.EAN_8,
|
||||
BarcodeFormat.EAN_13,
|
||||
BarcodeFormat.ITF,
|
||||
BarcodeFormat.PDF_417,
|
||||
BarcodeFormat.QR_CODE,
|
||||
BarcodeFormat.UPC_A
|
||||
Format.AZTEC,
|
||||
Format.CODABAR,
|
||||
Format.CODE_39,
|
||||
Format.CODE_128,
|
||||
Format.DATA_MATRIX,
|
||||
Format.EAN_8,
|
||||
Format.EAN_13,
|
||||
Format.ITF,
|
||||
Format.PDF_417,
|
||||
Format.QR_CODE,
|
||||
Format.UPC_A
|
||||
)
|
||||
|
||||
override fun onCreateView(
|
||||
@@ -48,11 +51,38 @@ class EncodeFragment : Fragment() {
|
||||
)
|
||||
|
||||
formatView = view.findViewById(R.id.format)
|
||||
formatView.adapter = ArrayAdapter(
|
||||
val formatAdapter = ArrayAdapter(
|
||||
ac,
|
||||
android.R.layout.simple_list_item_1,
|
||||
writers.map { it.name }
|
||||
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] == Format.QR_CODE
|
||||
) {
|
||||
View.VISIBLE
|
||||
} else {
|
||||
View.GONE
|
||||
}
|
||||
errorCorrectionLabel.visibility = visibility
|
||||
errorCorrectionLevel.visibility = visibility
|
||||
}
|
||||
|
||||
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)
|
||||
@@ -61,14 +91,14 @@ class EncodeFragment : Fragment() {
|
||||
contentView = view.findViewById(R.id.content)
|
||||
|
||||
val args = arguments
|
||||
args?.also {
|
||||
contentView.setText(it.getString(CONTENT))
|
||||
args?.getString(CONTENT)?.let {
|
||||
contentView.setText(it)
|
||||
}
|
||||
|
||||
val barcodeFormat = args?.getString(FORMAT)
|
||||
if (barcodeFormat != null) {
|
||||
formatView.setSelection(
|
||||
writers.indexOf(resolveFormat(barcodeFormat))
|
||||
writers.indexOf(barcodeFormat.toFormat())
|
||||
)
|
||||
} else if (state == null) {
|
||||
formatView.post {
|
||||
@@ -92,8 +122,6 @@ class EncodeFragment : Fragment() {
|
||||
}
|
||||
|
||||
private fun encode() {
|
||||
val format = writers[formatView.selectedItemPosition]
|
||||
val size = getSize(sizeBarView.progress)
|
||||
val content = contentView.text.toString()
|
||||
if (content.isEmpty()) {
|
||||
Toast.makeText(
|
||||
@@ -103,9 +131,13 @@ class EncodeFragment : Fragment() {
|
||||
).show()
|
||||
} else {
|
||||
activity?.hideSoftKeyboard(contentView)
|
||||
addFragment(
|
||||
fragmentManager,
|
||||
BarcodeFragment.newInstance(content, format, size)
|
||||
fragmentManager?.addFragment(
|
||||
BarcodeFragment.newInstance(
|
||||
content,
|
||||
writers[formatView.selectedItemPosition],
|
||||
getSize(sizeBarView.progress),
|
||||
errorCorrectionLevel.selectedItemPosition
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -140,11 +172,11 @@ class EncodeFragment : Fragment() {
|
||||
private const val FORMAT = "format"
|
||||
|
||||
fun newInstance(
|
||||
content: String,
|
||||
content: String? = null,
|
||||
format: String? = null
|
||||
): Fragment {
|
||||
val args = Bundle()
|
||||
args.putString(CONTENT, content)
|
||||
content?.let { args.putString(CONTENT, content) }
|
||||
format?.let { args.putString(FORMAT, it) }
|
||||
val fragment = EncodeFragment()
|
||||
fragment.arguments = args
|
||||
@@ -153,8 +185,8 @@ class EncodeFragment : Fragment() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun resolveFormat(name: String): BarcodeFormat = try {
|
||||
BarcodeFormat.valueOf(name)
|
||||
private fun String.toFormat(default: Format = Format.QR_CODE): Format = try {
|
||||
Format.valueOf(this)
|
||||
} catch (_: IllegalArgumentException) {
|
||||
BarcodeFormat.QR_CODE
|
||||
default
|
||||
}
|
||||
|
||||
@@ -21,12 +21,12 @@ 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.content.copyToClipboard
|
||||
import de.markusfisch.android.binaryeye.content.shareText
|
||||
import de.markusfisch.android.binaryeye.database.*
|
||||
import de.markusfisch.android.binaryeye.io.askForFileName
|
||||
import de.markusfisch.android.binaryeye.io.toSaveResult
|
||||
import de.markusfisch.android.binaryeye.view.*
|
||||
import de.markusfisch.android.binaryeye.widget.toast
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
@@ -72,7 +72,7 @@ class HistoryFragment : Fragment() {
|
||||
val ac = activity ?: return false
|
||||
return when (item.itemId) {
|
||||
R.id.copy_scan -> {
|
||||
scansAdapter?.getSelectedContent()?.let {
|
||||
scansAdapter?.getSelectedContent("\n")?.let {
|
||||
ac.copyToClipboard(it)
|
||||
ac.toast(R.string.copied_to_clipboard)
|
||||
}
|
||||
@@ -80,19 +80,21 @@ class HistoryFragment : Fragment() {
|
||||
true
|
||||
}
|
||||
R.id.edit_scan -> {
|
||||
scansAdapter?.let {
|
||||
askForName(
|
||||
ac,
|
||||
it.selectedScanId,
|
||||
getScanName(it.selectedScanPosition)
|
||||
scansAdapter?.forSelection { id, position ->
|
||||
ac.askForName(
|
||||
id,
|
||||
scansAdapter?.getName(position),
|
||||
scansAdapter?.getContent(position)
|
||||
)
|
||||
}
|
||||
closeActionMode()
|
||||
true
|
||||
}
|
||||
R.id.remove_scan -> {
|
||||
scansAdapter?.let {
|
||||
askToRemoveScan(ac, it.selectedScanId)
|
||||
scansAdapter?.getSelectedIds()?.let {
|
||||
if (it.isNotEmpty()) {
|
||||
ac.askToRemoveScans(it)
|
||||
}
|
||||
}
|
||||
closeActionMode()
|
||||
true
|
||||
@@ -142,8 +144,7 @@ class HistoryFragment : Fragment() {
|
||||
showScan(id)
|
||||
}
|
||||
listView.setOnItemLongClickListener { _, v, position, id ->
|
||||
v.isSelected = true
|
||||
scansAdapter?.select(id, position)
|
||||
scansAdapter?.select(v, id, position)
|
||||
if (actionMode == null && ac is AppCompatActivity) {
|
||||
actionMode = ac.delegate.startSupportActionMode(
|
||||
actionModeCallback
|
||||
@@ -155,7 +156,7 @@ class HistoryFragment : Fragment() {
|
||||
|
||||
fab = view.findViewById(R.id.share)
|
||||
fab.setOnClickListener { v ->
|
||||
pickListSeparatorAndShare(v.context)
|
||||
v.context.pickListSeparatorAndShare()
|
||||
}
|
||||
|
||||
progressView = view.findViewById(R.id.progress_view)
|
||||
@@ -217,11 +218,11 @@ class HistoryFragment : Fragment() {
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
return when (item.itemId) {
|
||||
R.id.clear -> {
|
||||
askToRemoveScans(context)
|
||||
context.askToRemoveScans()
|
||||
true
|
||||
}
|
||||
R.id.export_history -> {
|
||||
askToExportToFile(context)
|
||||
askToExportToFile()
|
||||
true
|
||||
}
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
@@ -262,7 +263,7 @@ class HistoryFragment : Fragment() {
|
||||
View.GONE
|
||||
}
|
||||
cursor?.let { cursor ->
|
||||
// close previous cursor
|
||||
// Close previous cursor.
|
||||
scansAdapter?.also { it.changeCursor(null) }
|
||||
scansAdapter = ScansAdapter(ac, cursor)
|
||||
listView.adapter = scansAdapter
|
||||
@@ -290,29 +291,28 @@ class HistoryFragment : Fragment() {
|
||||
private fun showScan(id: Long) = db.getScan(id)?.also { scan ->
|
||||
closeActionMode()
|
||||
try {
|
||||
addFragment(
|
||||
fragmentManager,
|
||||
DecodeFragment.newInstance(scan)
|
||||
)
|
||||
fragmentManager?.addFragment(DecodeFragment.newInstance(scan))
|
||||
} catch (e: IllegalArgumentException) {
|
||||
// ignore, can never happen
|
||||
// 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
|
||||
// Dialogs don't have a parent layout.
|
||||
@SuppressLint("InflateParams")
|
||||
private fun askForName(context: Context, id: Long, text: String?) {
|
||||
val view = LayoutInflater.from(context).inflate(
|
||||
private fun Context.askForName(id: Long, text: String?, content: String?) {
|
||||
val view = LayoutInflater.from(this).inflate(
|
||||
R.layout.dialog_enter_name, null
|
||||
)
|
||||
val nameView = view.findViewById<EditText>(R.id.name)
|
||||
nameView.setText(text)
|
||||
AlertDialog.Builder(context)
|
||||
AlertDialog.Builder(this)
|
||||
.setTitle(
|
||||
if (content.isNullOrEmpty()) {
|
||||
getString(R.string.binary_data)
|
||||
} else {
|
||||
content
|
||||
}
|
||||
)
|
||||
.setView(view)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
val name = nameView.text.toString()
|
||||
@@ -323,11 +323,17 @@ class HistoryFragment : Fragment() {
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun askToRemoveScan(context: Context, id: Long) {
|
||||
AlertDialog.Builder(context)
|
||||
.setMessage(R.string.really_remove_scan)
|
||||
private fun Context.askToRemoveScans(ids: List<Long>) {
|
||||
AlertDialog.Builder(this)
|
||||
.setMessage(
|
||||
if (ids.size > 1) {
|
||||
R.string.really_remove_selected_scans
|
||||
} else {
|
||||
R.string.really_remove_scan
|
||||
}
|
||||
)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
db.removeScan(id)
|
||||
ids.forEach { db.removeScan(it) }
|
||||
if (scansAdapter?.count == 1) {
|
||||
updateAndClearFilter()
|
||||
} else {
|
||||
@@ -339,8 +345,8 @@ class HistoryFragment : Fragment() {
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun askToRemoveScans(context: Context) {
|
||||
AlertDialog.Builder(context)
|
||||
private fun Context.askToRemoveScans() {
|
||||
AlertDialog.Builder(this)
|
||||
.setMessage(
|
||||
if (filter == null) {
|
||||
R.string.really_remove_all_scans
|
||||
@@ -357,50 +363,55 @@ class HistoryFragment : Fragment() {
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun askToExportToFile(context: Context) = scope.launch {
|
||||
val ac = activity ?: return@launch
|
||||
progressView.useVisibility {
|
||||
if (!hasWritePermission(ac)) {
|
||||
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])
|
||||
private fun askToExportToFile() {
|
||||
scope.launch {
|
||||
val ac = activity ?: return@launch
|
||||
progressView.useVisibility {
|
||||
// Write permission is only required before Android Q.
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q &&
|
||||
!ac.hasWritePermission { askToExportToFile() }
|
||||
) {
|
||||
return@useVisibility
|
||||
}
|
||||
} ?: return@useVisibility
|
||||
val name = withContext(Dispatchers.Main) {
|
||||
ac.askForFileName(
|
||||
when (delimiter) {
|
||||
"db" -> ".db"
|
||||
"json" -> ".json"
|
||||
else -> ".csv"
|
||||
}
|
||||
val options = ac.resources.getStringArray(
|
||||
R.array.export_options_values
|
||||
)
|
||||
} ?: 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)
|
||||
val delimiter = alertDialog<String>(ac) { resume ->
|
||||
setTitle(R.string.export_as)
|
||||
setItems(R.array.export_options_names) { _, which ->
|
||||
resume(options[which])
|
||||
}
|
||||
} ?: false
|
||||
}.toSaveResult()
|
||||
withContext(Dispatchers.Main) {
|
||||
ac.toast(message)
|
||||
} ?: return@useVisibility
|
||||
val name = withContext(Dispatchers.Main) {
|
||||
ac.askForFileName(
|
||||
when (delimiter) {
|
||||
"db" -> ".db"
|
||||
"json" -> ".json"
|
||||
else -> ".csv"
|
||||
}
|
||||
)
|
||||
} ?: return@useVisibility
|
||||
val message = when (delimiter) {
|
||||
"db" -> ac.exportDatabase(name)
|
||||
else -> db.getScansDetailed(filter)?.use {
|
||||
when (delimiter) {
|
||||
"json" -> ac.exportJson(name, it)
|
||||
else -> ac.exportCsv(name, it, delimiter)
|
||||
}
|
||||
} ?: false
|
||||
}.toSaveResult()
|
||||
withContext(Dispatchers.Main) {
|
||||
ac.toast(message)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun pickListSeparatorAndShare(context: Context) {
|
||||
val separators = context.resources.getStringArray(
|
||||
private fun Context.pickListSeparatorAndShare() {
|
||||
val separators = resources.getStringArray(
|
||||
R.array.list_separators_values
|
||||
)
|
||||
AlertDialog.Builder(context)
|
||||
AlertDialog.Builder(this)
|
||||
.setTitle(R.string.pick_list_separator)
|
||||
.setItems(R.array.list_separators_names) { _, which ->
|
||||
shareScans(separators[which])
|
||||
@@ -414,31 +425,31 @@ class HistoryFragment : Fragment() {
|
||||
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" -> cursor.exportText(details[1])
|
||||
"csv" -> cursor.exportCsv(details[1])
|
||||
else -> cursor.exportJson()
|
||||
}
|
||||
}
|
||||
text?.let {
|
||||
withContext(Dispatchers.Main) {
|
||||
shareText(context, it)
|
||||
context.shareText(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun exportText(cursor: Cursor, separator: String): String {
|
||||
private fun Cursor.exportText(separator: String): String {
|
||||
val sb = StringBuilder()
|
||||
val contentIndex = cursor.getColumnIndex(Database.SCANS_CONTENT)
|
||||
if (cursor.moveToFirst()) {
|
||||
val contentIndex = getColumnIndex(Database.SCANS_CONTENT)
|
||||
if (contentIndex > -1 && moveToFirst()) {
|
||||
do {
|
||||
val content = cursor.getString(contentIndex)
|
||||
val content = getString(contentIndex)
|
||||
if (content?.isNotEmpty() == true) {
|
||||
sb.append(content)
|
||||
sb.append(separator)
|
||||
}
|
||||
} while (cursor.moveToNext())
|
||||
} while (moveToNext())
|
||||
}
|
||||
return sb.toString()
|
||||
}
|
||||
|
||||
@@ -1,17 +1,28 @@
|
||||
package de.markusfisch.android.binaryeye.fragment
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.AlertDialog
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.SharedPreferences
|
||||
import android.content.SharedPreferences.OnSharedPreferenceChangeListener
|
||||
import android.net.wifi.WifiManager
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.support.annotation.RequiresApi
|
||||
import android.support.v14.preference.MultiSelectListPreference
|
||||
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.actions.wifi.removeNetworkSuggestions
|
||||
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
|
||||
import de.markusfisch.android.binaryeye.view.systemBarRecyclerViewScrollListener
|
||||
import de.markusfisch.android.binaryeye.widget.toast
|
||||
|
||||
class PreferencesFragment : PreferenceFragmentCompat() {
|
||||
private val changeListener = object : OnSharedPreferenceChangeListener {
|
||||
@@ -21,13 +32,54 @@ class PreferencesFragment : PreferenceFragmentCompat() {
|
||||
) {
|
||||
val preference = findPreference(key) ?: return
|
||||
prefs.update()
|
||||
setSummary(preference)
|
||||
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)
|
||||
wireClearNetworkPreferences()
|
||||
}
|
||||
|
||||
private fun wireClearNetworkPreferences() {
|
||||
val pref = findPreference("clear_network_suggestions") ?: return
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
||||
pref.isVisible = false
|
||||
} else {
|
||||
pref.setOnPreferenceClickListener {
|
||||
context.askToClearNetworkSuggestions()
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.Q)
|
||||
private fun Context.askToClearNetworkSuggestions() {
|
||||
AlertDialog.Builder(this)
|
||||
.setMessage(R.string.really_remove_all_networks)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
clearNetworkSuggestions()
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
||||
.show()
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.Q)
|
||||
private fun Context.clearNetworkSuggestions() {
|
||||
toast(
|
||||
if (
|
||||
removeNetworkSuggestions(this) == WifiManager.STATUS_NETWORK_SUGGESTIONS_SUCCESS
|
||||
) {
|
||||
R.string.clear_network_suggestions_success
|
||||
} else {
|
||||
R.string.clear_network_suggestions_nothing_to_remove
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
@@ -73,9 +125,30 @@ class PreferencesFragment : PreferenceFragmentCompat() {
|
||||
is ListPreference -> {
|
||||
preference.setSummary(preference.entry)
|
||||
}
|
||||
is MultiSelectListPreference -> {
|
||||
preference.setSummary(
|
||||
preference.values.joinToString(", ") {
|
||||
it.replace(Regex("_"), " ")
|
||||
}
|
||||
)
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -5,25 +5,61 @@ 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(urlView?.text.toString())
|
||||
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"
|
||||
).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()
|
||||
@@ -34,3 +70,19 @@ class UrlDialogFragment : PreferenceDialogFragmentCompat() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -7,11 +7,13 @@ 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 {
|
||||
fun ContentResolver.loadImageUri(uri: Uri): Bitmap? = try {
|
||||
val options = BitmapFactory.Options()
|
||||
cr.openInputStream(uri)?.use {
|
||||
openInputStream(uri)?.use {
|
||||
options.inJustDecodeBounds = true
|
||||
BitmapFactory.decodeStream(it, null, options)
|
||||
options.inSampleSize = calculateInSampleSize(
|
||||
@@ -20,7 +22,7 @@ fun loadImageUri(cr: ContentResolver, uri: Uri): Bitmap? = try {
|
||||
)
|
||||
options.inJustDecodeBounds = false
|
||||
}
|
||||
cr.openInputStream(uri)?.use {
|
||||
openInputStream(uri)?.use {
|
||||
BitmapFactory.decodeStream(it, null, options)
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
@@ -47,16 +49,23 @@ private fun calculateInSampleSize(
|
||||
return inSampleSize
|
||||
}
|
||||
|
||||
fun crop(bitmap: Bitmap, rect: RectF, rotation: Float) = try {
|
||||
val erected = erect(bitmap, rotation)
|
||||
fun Bitmap.crop(
|
||||
rect: RectF,
|
||||
rotation: Float,
|
||||
pivotX: Float,
|
||||
pivotY: Float
|
||||
) = try {
|
||||
val erected = erect(rotation, pivotX, pivotY)
|
||||
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,
|
||||
(rect.left * w).roundToInt(),
|
||||
(rect.top * h).roundToInt(),
|
||||
(rect.width() * w).roundToInt(),
|
||||
(rect.height() * h).roundToInt()
|
||||
x,
|
||||
y,
|
||||
min(w - x, (rect.right * w).roundToInt() - x),
|
||||
min(h - y, (rect.bottom * h).roundToInt() - y)
|
||||
)
|
||||
} catch (e: OutOfMemoryError) {
|
||||
null
|
||||
@@ -64,20 +73,24 @@ fun crop(bitmap: Bitmap, rect: RectF, rotation: Float) = try {
|
||||
null
|
||||
}
|
||||
|
||||
private fun erect(bitmap: Bitmap, rotation: Float): Bitmap = if (
|
||||
private fun Bitmap.erect(
|
||||
rotation: Float,
|
||||
pivotX: Float,
|
||||
pivotY: Float
|
||||
): Bitmap = if (
|
||||
rotation % 360f != 0f
|
||||
) {
|
||||
val matrix = Matrix()
|
||||
matrix.setRotate(rotation)
|
||||
Bitmap.createBitmap(
|
||||
bitmap,
|
||||
this,
|
||||
0,
|
||||
0,
|
||||
bitmap.width,
|
||||
bitmap.height,
|
||||
matrix,
|
||||
width,
|
||||
height,
|
||||
Matrix().apply {
|
||||
setRotate(rotation, pivotX, pivotY)
|
||||
},
|
||||
true
|
||||
)
|
||||
} else {
|
||||
bitmap
|
||||
this
|
||||
}
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,85 +1,87 @@
|
||||
package de.markusfisch.android.binaryeye.graphics
|
||||
|
||||
import android.graphics.Point
|
||||
import android.graphics.Matrix
|
||||
import android.graphics.Rect
|
||||
import com.google.zxing.Result
|
||||
import com.google.zxing.ResultPoint
|
||||
import android.graphics.RectF
|
||||
import de.markusfisch.android.zxingcpp.ZxingCpp.Position
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
fun mapResult(
|
||||
frameWidth: Int,
|
||||
frameHeight: Int,
|
||||
frameOrientation: Int,
|
||||
data class FrameMetrics(
|
||||
var width: Int = 0,
|
||||
var height: Int = 0,
|
||||
var orientation: Int = 0
|
||||
)
|
||||
|
||||
fun Rect.setFrameRoi(
|
||||
frameMetrics: FrameMetrics,
|
||||
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
|
||||
viewRoi: Rect
|
||||
) {
|
||||
fun map(resultPoint: ResultPoint): Point {
|
||||
val point = Point(
|
||||
resultPoint.x.roundToInt(),
|
||||
resultPoint.y.roundToInt()
|
||||
Matrix().apply {
|
||||
// Map ROI from view coordinates to frame coordinates.
|
||||
setScale(1f / viewRect.width(), 1f / viewRect.height())
|
||||
postRotate(-frameMetrics.orientation.toFloat(), .5f, .5f)
|
||||
postScale(frameMetrics.width.toFloat(), frameMetrics.height.toFloat())
|
||||
postTranslate(-viewRect.left.toFloat(), -viewRect.top.toFloat())
|
||||
val frameRoiF = RectF()
|
||||
val viewRoiF = RectF(
|
||||
viewRoi.left.toFloat(),
|
||||
viewRoi.top.toFloat(),
|
||||
viewRoi.right.toFloat(),
|
||||
viewRoi.bottom.toFloat()
|
||||
)
|
||||
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
|
||||
mapRect(frameRoiF, viewRoiF)
|
||||
set(
|
||||
frameRoiF.left.roundToInt(),
|
||||
frameRoiF.top.roundToInt(),
|
||||
frameRoiF.right.roundToInt(),
|
||||
frameRoiF.bottom.roundToInt()
|
||||
)
|
||||
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 -> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun Matrix.setFrameToView(
|
||||
frameMetrics: FrameMetrics,
|
||||
viewRect: Rect,
|
||||
viewRoi: Rect? = null
|
||||
) {
|
||||
// Configure this matrix to map points in frame coordinates to
|
||||
// view coordinates.
|
||||
val uprightWidth: Int
|
||||
val uprightHeight: Int
|
||||
when (frameMetrics.orientation) {
|
||||
90, 270 -> {
|
||||
uprightWidth = frameMetrics.height
|
||||
uprightHeight = frameMetrics.width
|
||||
}
|
||||
else -> {
|
||||
uprightWidth = frameMetrics.width
|
||||
uprightHeight = frameMetrics.height
|
||||
}
|
||||
}
|
||||
setScale(
|
||||
viewRect.width().toFloat() / uprightWidth,
|
||||
viewRect.height().toFloat() / uprightHeight
|
||||
)
|
||||
viewRoi?.let {
|
||||
postTranslate(viewRoi.left.toFloat(), viewRoi.top.toFloat())
|
||||
}
|
||||
}
|
||||
|
||||
fun Matrix.mapPosition(
|
||||
position: Position,
|
||||
coords: FloatArray
|
||||
): Int {
|
||||
var i = 0
|
||||
setOf(
|
||||
position.topLeft,
|
||||
position.topRight,
|
||||
position.bottomRight,
|
||||
position.bottomLeft
|
||||
).forEach {
|
||||
coords[i++] = it.x.toFloat()
|
||||
coords[i++] = it.y.toFloat()
|
||||
}
|
||||
mapPoints(coords)
|
||||
return i
|
||||
}
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
package de.markusfisch.android.binaryeye.graphics
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Canvas
|
||||
|
||||
fun Bitmap.fixTransparency(): Bitmap {
|
||||
val result = analyzeTransparency()
|
||||
return if (result.hasTransparentPixels) {
|
||||
val copy = Bitmap.createBitmap(
|
||||
width,
|
||||
height,
|
||||
config
|
||||
)
|
||||
Canvas(copy).apply {
|
||||
drawColor(result.backgroundColor)
|
||||
drawBitmap(this@fixTransparency, 0f, 0f, null)
|
||||
}
|
||||
copy
|
||||
} else {
|
||||
this
|
||||
}
|
||||
}
|
||||
|
||||
private fun Bitmap.analyzeTransparency(): Result {
|
||||
val pixels = IntArray(width * height)
|
||||
getPixels(pixels, 0, width, 0, 0, width, height)
|
||||
var transparent = 0
|
||||
var visible = 0
|
||||
var bright = 0
|
||||
for (pixel in pixels) {
|
||||
val alpha = (pixel shr 24) and 0xff
|
||||
if (alpha < 0xff) {
|
||||
++transparent
|
||||
}
|
||||
if (alpha > 0) {
|
||||
++visible
|
||||
if (
|
||||
((pixel shr 16) and 0xff) +
|
||||
((pixel shr 8) and 0xff) +
|
||||
(pixel and 0xff) > 128
|
||||
) ++bright
|
||||
}
|
||||
}
|
||||
return Result(
|
||||
transparent > 0,
|
||||
if (bright > 0 && visible / bright < 2) {
|
||||
0xff000000.toInt()
|
||||
} else {
|
||||
0xffffffff.toInt()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private data class Result(
|
||||
val hasTransparentPixels: Boolean,
|
||||
val backgroundColor: Int
|
||||
)
|
||||
@@ -1,4 +1,4 @@
|
||||
package de.markusfisch.android.binaryeye.app
|
||||
package de.markusfisch.android.binaryeye.io
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.ContentValues
|
||||
@@ -9,6 +9,7 @@ import android.provider.MediaStore
|
||||
import android.support.annotation.MainThread
|
||||
import android.widget.EditText
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.app.alertDialog
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
@@ -45,20 +46,18 @@ fun Boolean.toSaveResult() = if (this) {
|
||||
R.string.error_saving_file
|
||||
}
|
||||
|
||||
fun writeExternalFile(
|
||||
context: Context,
|
||||
fun Context.writeExternalFile(
|
||||
fileName: String,
|
||||
mimeType: String,
|
||||
write: (outputStream: OutputStream) -> Unit
|
||||
): Boolean = try {
|
||||
openExternalOutputStream(context, fileName, mimeType).use { write(it) }
|
||||
openExternalOutputStream(fileName, mimeType).use { write(it) }
|
||||
true
|
||||
} catch (e: IOException) {
|
||||
false
|
||||
}
|
||||
|
||||
private fun openExternalOutputStream(
|
||||
context: Context,
|
||||
private fun Context.openExternalOutputStream(
|
||||
fileName: String,
|
||||
mimeType: String
|
||||
): OutputStream = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
||||
@@ -74,7 +73,7 @@ private fun openExternalOutputStream(
|
||||
}
|
||||
FileOutputStream(file)
|
||||
} else {
|
||||
val resolver = context.contentResolver
|
||||
val resolver = contentResolver
|
||||
val uri = resolver.insert(
|
||||
MediaStore.Downloads.EXTERNAL_CONTENT_URI,
|
||||
ContentValues().apply {
|
||||
@@ -1,32 +1,34 @@
|
||||
package de.markusfisch.android.binaryeye.net
|
||||
|
||||
import android.content.Context
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.app.toHexString
|
||||
import de.markusfisch.android.binaryeye.content.toHexString
|
||||
import de.markusfisch.android.binaryeye.database.Scan
|
||||
import de.markusfisch.android.binaryeye.widget.toast
|
||||
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(context: Context, url: String, type: String) {
|
||||
fun Scan.sendAsync(
|
||||
url: String,
|
||||
type: String,
|
||||
callback: (Int?, String?) -> Unit
|
||||
) {
|
||||
CoroutineScope(Dispatchers.IO).launch(Dispatchers.IO) {
|
||||
val code = send(url, type)
|
||||
val response = send(url, type)
|
||||
withContext(Dispatchers.Main) {
|
||||
if (code < 200 || code > 299) {
|
||||
context.toast(R.string.background_request_failed)
|
||||
}
|
||||
callback(response.code, response.body)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun Scan.send(url: String, type: String): Int {
|
||||
private fun Scan.send(url: String, type: String): Response {
|
||||
return when (type) {
|
||||
"1" -> request(
|
||||
url + "?" + asUrlArguments()
|
||||
@@ -39,7 +41,7 @@ private fun Scan.send(url: String, type: String): Int {
|
||||
close()
|
||||
}
|
||||
}
|
||||
"3" -> request(url) { con ->
|
||||
"3" -> request(url) { con ->
|
||||
con.requestMethod = "POST"
|
||||
con.setRequestProperty("Content-Type", "application/json")
|
||||
con.outputStream.apply {
|
||||
@@ -48,34 +50,33 @@ private fun Scan.send(url: String, type: String): Int {
|
||||
}
|
||||
}
|
||||
else -> request(
|
||||
url + urlEncode(content)
|
||||
url + content.urlEncode()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun Scan.asUrlArguments(): String = getMap().map { (k, v) ->
|
||||
"${k}=${urlEncode(v)}"
|
||||
"${k}=${v.urlEncode()}"
|
||||
}.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,
|
||||
"versionNumber" to versionNumber.toString(),
|
||||
"sequenceSize" to sequenceSize.toString(),
|
||||
"sequenceIndex" to sequenceIndex.toString(),
|
||||
"sequenceId" to sequenceId,
|
||||
"country" to country,
|
||||
"addOn" to addOn,
|
||||
"price" to price,
|
||||
"issueNumber" to issueNumber,
|
||||
"orientation" to orientation,
|
||||
"otherMetaData" to otherMetaData,
|
||||
"pdf417ExtraMetaData" to pdf417ExtraMetaData,
|
||||
"possibleCountry" to possibleCountry,
|
||||
"suggestedPrice" to suggestedPrice,
|
||||
"upcEanExtension" to upcEanExtension,
|
||||
"timestamp" to timestamp
|
||||
"timestamp" to dateTime
|
||||
).filterNullValues()
|
||||
|
||||
private fun Map<String, String?>.filterNullValues() =
|
||||
@@ -84,15 +85,40 @@ private fun Map<String, String?>.filterNullValues() =
|
||||
private fun request(
|
||||
url: String,
|
||||
writer: ((HttpURLConnection) -> Any)? = null
|
||||
): Int {
|
||||
): Response {
|
||||
var con: HttpURLConnection? = null
|
||||
return try {
|
||||
con = URL(url).openConnection() as HttpURLConnection
|
||||
con.connectTimeout = 5000
|
||||
writer?.invoke(con)
|
||||
con.responseCode
|
||||
val body = con.inputStream.readHead()
|
||||
Response(con.responseCode, body)
|
||||
} catch (e: ProtocolException) {
|
||||
Response(null, e.message)
|
||||
} catch (e: IOException) {
|
||||
-1
|
||||
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,10 @@
|
||||
package de.markusfisch.android.binaryeye.net
|
||||
|
||||
import java.io.UnsupportedEncodingException
|
||||
import java.net.URLEncoder
|
||||
|
||||
fun String.urlEncode(): String = try {
|
||||
URLEncoder.encode(this, "UTF-8")
|
||||
} catch (e: UnsupportedEncodingException) {
|
||||
this
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package de.markusfisch.android.binaryeye.os
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.os.VibrationEffect
|
||||
import android.os.Vibrator
|
||||
import de.markusfisch.android.binaryeye.app.prefs
|
||||
|
||||
private const val DURATION_IN_MS = 100L
|
||||
|
||||
private val beforeO = Build.VERSION.SDK_INT < Build.VERSION_CODES.O
|
||||
private val errorPatternTimings = longArrayOf(
|
||||
50L, 100L, 50L, 100L, 50L
|
||||
)
|
||||
private val errorPatternAmplitudes = intArrayOf(
|
||||
255, 0, 255, 0, 255
|
||||
)
|
||||
|
||||
// On SDK 31 and better, we should ask the `VIBRATOR_MANAGER_SERVICE`
|
||||
// for the default vibrator. This was implemented here before:
|
||||
/*fun Context.getVibrator(): Vibrator = if (
|
||||
Build.VERSION.SDK_INT < Build.VERSION_CODES.S
|
||||
) {
|
||||
@Suppress("DEPRECATION")
|
||||
getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
|
||||
} else {
|
||||
(getSystemService(
|
||||
Context.VIBRATOR_MANAGER_SERVICE
|
||||
) as VibratorManager).defaultVibrator
|
||||
}*/
|
||||
// But for reasons beyond my interests, using `VIBRATOR_MANAGER_SERVICE`
|
||||
// leads to a very cryptic `java.lang.VerifyError: r0/b` on Android 4.
|
||||
// This is probably a Proguard/R8 issue, but I don't want to waste more
|
||||
// time just to get rid of the deprecation warning. So let's use the old
|
||||
// `VIBRATOR_SERVICE` and wait for Google to fix their stuff.
|
||||
fun Context.getVibrator(): Vibrator = getSystemService(
|
||||
Context.VIBRATOR_SERVICE
|
||||
) as Vibrator
|
||||
|
||||
fun Vibrator.vibrate() {
|
||||
if (!prefs.vibrate) {
|
||||
return
|
||||
} else if (beforeO) {
|
||||
@Suppress("DEPRECATION")
|
||||
vibrate(DURATION_IN_MS)
|
||||
} else {
|
||||
vibrate(
|
||||
VibrationEffect.createOneShot(
|
||||
DURATION_IN_MS,
|
||||
VibrationEffect.DEFAULT_AMPLITUDE
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun Vibrator.error() {
|
||||
if (!prefs.vibrate) {
|
||||
return
|
||||
} else if (beforeO) {
|
||||
@Suppress("DEPRECATION")
|
||||
vibrate(errorPatternTimings, -1)
|
||||
} else {
|
||||
vibrate(
|
||||
VibrationEffect.createWaveform(
|
||||
errorPatternTimings,
|
||||
errorPatternAmplitudes,
|
||||
-1
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -2,17 +2,44 @@ package de.markusfisch.android.binaryeye.preference
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import android.os.Build
|
||||
import android.preference.PreferenceManager
|
||||
import android.support.annotation.RequiresApi
|
||||
import de.markusfisch.android.zxingcpp.ZxingCpp.Format
|
||||
|
||||
class Preferences {
|
||||
lateinit var preferences: SharedPreferences
|
||||
|
||||
var cropHandleX = -1
|
||||
var barcodeFormats = setOf(
|
||||
Format.AZTEC.name,
|
||||
Format.CODABAR.name,
|
||||
Format.CODE_39.name,
|
||||
Format.CODE_93.name,
|
||||
Format.CODE_128.name,
|
||||
Format.DATA_BAR.name,
|
||||
Format.DATA_BAR_EXPANDED.name,
|
||||
Format.DATA_MATRIX.name,
|
||||
Format.EAN_8.name,
|
||||
Format.EAN_13.name,
|
||||
Format.ITF.name,
|
||||
Format.MAXICODE.name,
|
||||
Format.PDF_417.name,
|
||||
Format.QR_CODE.name,
|
||||
Format.MICRO_QR_CODE.name,
|
||||
Format.UPC_A.name,
|
||||
Format.UPC_E.name,
|
||||
)
|
||||
@RequiresApi(Build.VERSION_CODES.HONEYCOMB)
|
||||
set(value) {
|
||||
apply(BARCODE_FORMATS, value)
|
||||
field = value
|
||||
}
|
||||
var cropHandleX = -2 // -2 means set default roi.
|
||||
set(value) {
|
||||
apply(CROP_HANDLE_X, value)
|
||||
field = value
|
||||
}
|
||||
var cropHandleY = -1
|
||||
var cropHandleY = -2
|
||||
set(value) {
|
||||
apply(CROP_HANDLE_Y, value)
|
||||
field = value
|
||||
@@ -32,20 +59,41 @@ class Preferences {
|
||||
apply(ZOOM_BY_SWIPING, value)
|
||||
field = value
|
||||
}
|
||||
var autoRotate = false
|
||||
var autoRotate = true
|
||||
set(value) {
|
||||
// immediately save this setting before it shouldn't change
|
||||
// on the fly while scanning
|
||||
// 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
|
||||
// Immediately save this setting because it's only ever read
|
||||
// before the camera is opened.
|
||||
commit(TRY_HARDER, value)
|
||||
field = value
|
||||
}
|
||||
var bulkMode = false
|
||||
set(value) {
|
||||
// Immediately save this setting because it's only ever read
|
||||
// before the camera is opened.
|
||||
commit(BULK_MODE, value)
|
||||
field = value
|
||||
}
|
||||
var bulkModeDelay = "500"
|
||||
set(value) {
|
||||
// Immediately save this setting because it's only ever read
|
||||
// before the camera is opened.
|
||||
commit(BULK_MODE_DELAY, value)
|
||||
field = value
|
||||
}
|
||||
var showToastInBulkMode = true
|
||||
set(value) {
|
||||
// Immediately save this setting because it's only ever read
|
||||
// before the camera is opened.
|
||||
commit(SHOW_TOAST_IN_BULK_MODE, value)
|
||||
field = value
|
||||
}
|
||||
var vibrate = true
|
||||
set(value) {
|
||||
apply(VIBRATE, value)
|
||||
@@ -81,11 +129,31 @@ class Preferences {
|
||||
apply(SHOW_HEX_DUMP, value)
|
||||
field = value
|
||||
}
|
||||
var showRecreation = true
|
||||
set(value) {
|
||||
apply(SHOW_RECREATION, value)
|
||||
field = value
|
||||
}
|
||||
var closeAutomatically = false
|
||||
set(value) {
|
||||
apply(CLOSE_AUTOMATICALLY, value)
|
||||
field = value
|
||||
}
|
||||
var defaultSearchUrl = ""
|
||||
set(value) {
|
||||
apply(DEFAULT_SEARCH_URL, value)
|
||||
field = value
|
||||
}
|
||||
var openWithUrl: String = ""
|
||||
set(value) {
|
||||
apply(OPEN_WITH_URL, value)
|
||||
field = value
|
||||
}
|
||||
var sendScanActive = true
|
||||
set(value) {
|
||||
apply(SEND_SCAN_ACTIVE, value)
|
||||
field = value
|
||||
}
|
||||
var sendScanUrl: String = ""
|
||||
set(value) {
|
||||
apply(SEND_SCAN_URL, value)
|
||||
@@ -96,15 +164,20 @@ class Preferences {
|
||||
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
|
||||
var forceCompat = false
|
||||
set(value) {
|
||||
// since the app may be about to crash when forceCompat is set,
|
||||
// it's necessary to `commit()` this synchronously
|
||||
// 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
|
||||
}
|
||||
@@ -115,16 +188,33 @@ class Preferences {
|
||||
}
|
||||
|
||||
fun update() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
|
||||
preferences.getStringSet(BARCODE_FORMATS, barcodeFormats)?.let {
|
||||
barcodeFormats = it
|
||||
}
|
||||
}
|
||||
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)
|
||||
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)
|
||||
bulkMode = preferences.getBoolean(BULK_MODE, bulkMode)
|
||||
bulkModeDelay = preferences.getString(
|
||||
BULK_MODE_DELAY,
|
||||
bulkModeDelay
|
||||
) ?: bulkModeDelay
|
||||
showToastInBulkMode = preferences.getBoolean(
|
||||
SHOW_TOAST_IN_BULK_MODE,
|
||||
showToastInBulkMode
|
||||
)
|
||||
vibrate = preferences.getBoolean(VIBRATE, vibrate)
|
||||
useHistory = preferences.getBoolean(USE_HISTORY, useHistory)
|
||||
ignoreConsecutiveDuplicates = preferences.getBoolean(
|
||||
@@ -141,15 +231,30 @@ class Preferences {
|
||||
)
|
||||
showMetaData = preferences.getBoolean(SHOW_META_DATA, showMetaData)
|
||||
showHexDump = preferences.getBoolean(SHOW_HEX_DUMP, showHexDump)
|
||||
showRecreation = preferences.getBoolean(SHOW_RECREATION, showRecreation)
|
||||
closeAutomatically = preferences.getBoolean(
|
||||
CLOSE_AUTOMATICALLY,
|
||||
closeAutomatically
|
||||
)
|
||||
preferences.getString(DEFAULT_SEARCH_URL, defaultSearchUrl)?.also {
|
||||
defaultSearchUrl = it
|
||||
}
|
||||
preferences.getString(OPEN_WITH_URL, openWithUrl)?.also {
|
||||
openWithUrl = it
|
||||
}
|
||||
sendScanActive = preferences.getBoolean(
|
||||
SEND_SCAN_ACTIVE,
|
||||
sendScanActive
|
||||
)
|
||||
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
|
||||
@@ -160,10 +265,17 @@ class Preferences {
|
||||
private fun put(label: String, value: Boolean) =
|
||||
preferences.edit().putBoolean(label, value)
|
||||
|
||||
private fun put(label: String, value: String) =
|
||||
preferences.edit().putString(label, value)
|
||||
|
||||
private fun commit(label: String, value: Boolean) {
|
||||
put(label, value).commit()
|
||||
}
|
||||
|
||||
private fun commit(label: String, value: String) {
|
||||
put(label, value).commit()
|
||||
}
|
||||
|
||||
private fun apply(label: String, value: Boolean) {
|
||||
put(label, value).apply()
|
||||
}
|
||||
@@ -176,25 +288,39 @@ class Preferences {
|
||||
preferences.edit().putInt(label, value).apply()
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.HONEYCOMB)
|
||||
private fun apply(label: String, value: Set<String>) {
|
||||
preferences.edit().putStringSet(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_HEX_DUMP = "show_hex_dump"
|
||||
const val SHOW_META_DATA = "show_meta_data"
|
||||
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 INDEX_OF_LAST_SELECTED_FORMAT = "index_of_last_selected_format"
|
||||
const val FORCE_COMPAT = "force_compat"
|
||||
private const val BARCODE_FORMATS = "formats"
|
||||
private const val CROP_HANDLE_X = "crop_handle_x"
|
||||
private const val CROP_HANDLE_Y = "crop_handle_y"
|
||||
private const val CROP_HANDLE_ORIENTATION = "crop_handle_orientation"
|
||||
private const val SHOW_CROP_HANDLE = "show_crop_handle"
|
||||
private const val ZOOM_BY_SWIPING = "zoom_by_swiping"
|
||||
private const val AUTO_ROTATE = "auto_rotate"
|
||||
private const val TRY_HARDER = "try_harder"
|
||||
private const val BULK_MODE = "bulk_mode"
|
||||
private const val BULK_MODE_DELAY = "bulk_mode_delay"
|
||||
private const val SHOW_TOAST_IN_BULK_MODE = "show_toast_in_bulk_mode"
|
||||
private const val VIBRATE = "vibrate"
|
||||
private const val USE_HISTORY = "use_history"
|
||||
private const val IGNORE_CONSECUTIVE_DUPLICATES = "ignore_consecutive_duplicates"
|
||||
private const val OPEN_IMMEDIATELY = "open_immediately"
|
||||
private const val COPY_IMMEDIATELY = "copy_immediately"
|
||||
private const val SHOW_META_DATA = "show_meta_data"
|
||||
private const val SHOW_HEX_DUMP = "show_hex_dump"
|
||||
private const val SHOW_RECREATION = "show_recreation"
|
||||
private const val CLOSE_AUTOMATICALLY = "close_automatically"
|
||||
private const val DEFAULT_SEARCH_URL = "default_search_url"
|
||||
private const val OPEN_WITH_URL = "open_with_url"
|
||||
private const val SEND_SCAN_ACTIVE = "send_scan_active"
|
||||
private const val SEND_SCAN_URL = "send_scan_url"
|
||||
private const val SEND_SCAN_TYPE = "send_scan_type"
|
||||
private const val CUSTOM_LOCALE = "custom_locale"
|
||||
private const val INDEX_OF_LAST_SELECTED_FORMAT = "index_of_last_selected_format"
|
||||
private const val FORCE_COMPAT = "force_compat"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,159 +0,0 @@
|
||||
package de.markusfisch.android.binaryeye.rs
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Rect
|
||||
import android.support.v8.renderscript.*
|
||||
import kotlin.math.max
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
private const val SCALE_FACTOR = .75f
|
||||
|
||||
class Preprocessor(
|
||||
context: Context,
|
||||
width: Int,
|
||||
height: 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 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
|
||||
private var rotatedAlloc: Allocation? = null
|
||||
|
||||
init {
|
||||
yuvType = Type.createXY(
|
||||
rs,
|
||||
Element.U8(rs),
|
||||
width,
|
||||
height // use only grayscale part
|
||||
)
|
||||
yuvAlloc = Allocation.createTyped(
|
||||
rs,
|
||||
yuvType,
|
||||
Allocation.USAGE_SCRIPT
|
||||
)
|
||||
|
||||
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),
|
||||
outWidth,
|
||||
outHeight
|
||||
)
|
||||
resizedAlloc = Allocation.createTyped(
|
||||
rs,
|
||||
resizedType,
|
||||
Allocation.USAGE_SCRIPT
|
||||
)
|
||||
|
||||
rotatedType = Type.createXY(
|
||||
rs,
|
||||
Element.U8(rs),
|
||||
outHeight,
|
||||
outWidth
|
||||
)
|
||||
rotatedAlloc = Allocation.createTyped(
|
||||
rs,
|
||||
rotatedType,
|
||||
Allocation.USAGE_SCRIPT
|
||||
)
|
||||
}
|
||||
|
||||
fun destroy() {
|
||||
yuvType?.destroy()
|
||||
yuvType = null
|
||||
yuvAlloc?.destroy()
|
||||
yuvAlloc = null
|
||||
roiType?.destroy()
|
||||
roiType = null
|
||||
roiAlloc?.destroy()
|
||||
roiAlloc = null
|
||||
resizedType?.destroy()
|
||||
resizedType = null
|
||||
resizedAlloc?.destroy()
|
||||
resizedAlloc = null
|
||||
rotatedType?.destroy()
|
||||
rotatedType = null
|
||||
rotatedAlloc?.destroy()
|
||||
rotatedAlloc = null
|
||||
resizeScript.destroy()
|
||||
rotateScript.destroy()
|
||||
rs.destroy()
|
||||
}
|
||||
|
||||
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(
|
||||
if (roi != null) {
|
||||
roiAlloc?.copy2DRangeFrom(
|
||||
0, 0,
|
||||
roi.width(), roi.height(),
|
||||
yuvAlloc,
|
||||
max(0, roi.left), max(0, roi.top)
|
||||
)
|
||||
roiAlloc
|
||||
} else {
|
||||
yuvAlloc
|
||||
}
|
||||
)
|
||||
resizeScript.forEach_bicubic(resizedAlloc)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package de.markusfisch.android.binaryeye.service
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.service.quicksettings.TileService
|
||||
import android.support.annotation.RequiresApi
|
||||
import de.markusfisch.android.binaryeye.activity.CameraActivity
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.N)
|
||||
class ScanTileService : TileService() {
|
||||
override fun onClick() {
|
||||
super.onClick()
|
||||
val intent = Intent(applicationContext, CameraActivity::class.java)
|
||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
startActivityAndCollapse(intent)
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package de.markusfisch.android.binaryeye.app
|
||||
package de.markusfisch.android.binaryeye.view
|
||||
|
||||
import android.content.Context
|
||||
import android.view.View
|
||||
@@ -1,4 +1,4 @@
|
||||
package de.markusfisch.android.binaryeye.app
|
||||
package de.markusfisch.android.binaryeye.view
|
||||
|
||||
import android.content.Context
|
||||
import android.content.ContextWrapper
|
||||
@@ -20,8 +20,8 @@ val systemBarListViewScrollListener = object : AbsListView.OnScrollListener {
|
||||
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
|
||||
// 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))
|
||||
@@ -59,7 +59,7 @@ private fun lastChildOutOfView(listView: AbsListView): Boolean {
|
||||
|
||||
fun initSystemBars(activity: AppCompatActivity) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
// keeps the soft keyboard from repositioning the layout
|
||||
// Keeps the soft keyboard from repositioning the layout.
|
||||
val window = activity.window
|
||||
window.setSoftInputMode(
|
||||
WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE
|
||||
@@ -109,11 +109,11 @@ fun colorSystemAndToolBars(
|
||||
}
|
||||
activity.supportActionBar?.setBackgroundDrawable(
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
|
||||
// avoid allocation on Honeycomb and better
|
||||
// Avoid allocation on Honeycomb and better.
|
||||
actionBarBackground.color = topColor
|
||||
actionBarBackground
|
||||
} else {
|
||||
// ColorDrawable.setColor() doesn't exist pre Honeycomb
|
||||
// ColorDrawable.setColor() doesn't exist pre Honeycomb.
|
||||
ColorDrawable(topColor)
|
||||
}
|
||||
)
|
||||
@@ -29,10 +29,10 @@ fun View.doOnApplyWindowInsets(f: (View, Rect) -> Unit) {
|
||||
f(v, insetsWithToolbar(insets))
|
||||
insets
|
||||
}
|
||||
// it's important to explicitly request the insets (again) in
|
||||
// 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
|
||||
// isn't attached.
|
||||
requestApplyInsetsWhenAttached()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ 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.binaryeye.view.colorSystemAndToolBars
|
||||
import de.markusfisch.android.scalingimageview.widget.ScalingImageView
|
||||
|
||||
open class ConfinedScalingImageView : ScalingImageView {
|
||||
|
||||
@@ -3,7 +3,7 @@ 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
|
||||
import de.markusfisch.android.binaryeye.view.colorSystemAndToolBars
|
||||
|
||||
class ConfinedScrollView : ScrollView {
|
||||
private var scrollable = false
|
||||
@@ -23,8 +23,8 @@ class ConfinedScrollView : ScrollView {
|
||||
) {
|
||||
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
|
||||
// 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
|
||||
|
||||
@@ -2,53 +2,18 @@ package de.markusfisch.android.binaryeye.widget
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Point
|
||||
import android.graphics.Rect
|
||||
import android.graphics.RectF
|
||||
import android.util.AttributeSet
|
||||
import de.markusfisch.android.binaryeye.graphics.Dots
|
||||
import de.markusfisch.android.binaryeye.graphics.getDashedBorderPaint
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
class CropImageView(context: Context, attr: AttributeSet) :
|
||||
ConfinedScalingImageView(context, attr) {
|
||||
val windowInsets = Rect()
|
||||
|
||||
var onScan: (() -> Unit)? = null
|
||||
|
||||
private val dots = Dots(context)
|
||||
private val boundsPaint = context.getDashedBorderPaint()
|
||||
private val lastMappedRect = RectF()
|
||||
private val padding: Int = (24f * context.resources.displayMetrics.density).roundToInt()
|
||||
private val onScanRunnable = Runnable { onScan?.invoke() }
|
||||
|
||||
private var resultPoints: List<Point>? = null
|
||||
|
||||
init {
|
||||
scaleType = ScaleType.CENTER_CROP
|
||||
}
|
||||
|
||||
fun updateResultPoints(points: List<Point>) {
|
||||
resultPoints = points
|
||||
invalidate()
|
||||
}
|
||||
|
||||
override fun onLayout(
|
||||
changed: Boolean,
|
||||
left: Int,
|
||||
top: Int,
|
||||
right: Int,
|
||||
bottom: Int
|
||||
) {
|
||||
super.onLayout(changed, left, top, right, bottom)
|
||||
setBoundsWithPadding(
|
||||
left + windowInsets.left,
|
||||
top + windowInsets.top,
|
||||
right - windowInsets.right,
|
||||
bottom - windowInsets.bottom
|
||||
)
|
||||
}
|
||||
|
||||
fun getBoundsRect() = Rect(
|
||||
bounds.left.roundToInt(),
|
||||
bounds.top.roundToInt(),
|
||||
@@ -56,40 +21,12 @@ class CropImageView(context: Context, attr: AttributeSet) :
|
||||
bounds.bottom.roundToInt()
|
||||
)
|
||||
|
||||
private fun setBoundsWithPadding(
|
||||
left: Int,
|
||||
top: Int,
|
||||
right: Int,
|
||||
bottom: Int
|
||||
) {
|
||||
val width = right - left
|
||||
val height = bottom - top
|
||||
val size = if (width < height)
|
||||
width - padding * 2
|
||||
else
|
||||
height - padding * 2
|
||||
val hpad = (width - size) / 2
|
||||
val vpad = (height - size) / 2
|
||||
setBounds(
|
||||
left.toFloat() + hpad,
|
||||
top.toFloat() + vpad,
|
||||
right.toFloat() - hpad,
|
||||
bottom.toFloat() - vpad
|
||||
)
|
||||
center(bounds)
|
||||
}
|
||||
|
||||
override fun onDraw(canvas: Canvas) {
|
||||
super.onDraw(canvas)
|
||||
canvas.drawRect(bounds, boundsPaint)
|
||||
resultPoints?.let {
|
||||
dots.draw(canvas, it)
|
||||
}
|
||||
resultPoints = null
|
||||
val mr = mappedRect ?: return
|
||||
if (mr != lastMappedRect) {
|
||||
removeCallbacks(onScanRunnable)
|
||||
postDelayed(onScanRunnable, 500)
|
||||
postDelayed(onScanRunnable, 300)
|
||||
lastMappedRect.set(mr)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,12 +6,12 @@ import android.graphics.*
|
||||
import android.os.Build
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
import android.support.v4.content.ContextCompat
|
||||
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
|
||||
@@ -20,18 +20,23 @@ import kotlin.math.min
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
class DetectorView : View {
|
||||
val currentOrientation = resources.configuration.orientation
|
||||
val coordinates = FloatArray(32)
|
||||
val roi = Rect()
|
||||
|
||||
var onRoiChange: (() -> Unit)? = null
|
||||
var onRoiChanged: (() -> Unit)? = null
|
||||
|
||||
private val dots = Dots(context)
|
||||
private val currentOrientation = resources.configuration.orientation
|
||||
private val invalidateRunnable: Runnable = Runnable {
|
||||
marks = null
|
||||
coordinatesLast = 0
|
||||
invalidate()
|
||||
}
|
||||
private val roiPaint = context.getDashedBorderPaint()
|
||||
private val dotPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
color = ContextCompat.getColor(context, R.color.dot)
|
||||
style = Paint.Style.FILL
|
||||
}
|
||||
private val dotRadius: Float
|
||||
private val handleBitmap = resources.getBitmapFromDrawable(
|
||||
R.drawable.button_crop
|
||||
)
|
||||
@@ -47,7 +52,7 @@ class DetectorView : View {
|
||||
private val fabHeight: Int
|
||||
private val padding: Int
|
||||
|
||||
private var marks: List<Point>? = null
|
||||
private var coordinatesLast = 0
|
||||
private var handleGrabbed = false
|
||||
private var handleActive = false
|
||||
private var minY = 0
|
||||
@@ -57,6 +62,7 @@ class DetectorView : View {
|
||||
|
||||
init {
|
||||
val dp = context.resources.displayMetrics.density
|
||||
dotRadius = 8f * dp
|
||||
distToFull = (24f * dp).roundToInt()
|
||||
val minMoveThreshold = (8f * dp).roundToInt()
|
||||
minMoveThresholdSq = minMoveThreshold * minMoveThreshold
|
||||
@@ -72,25 +78,41 @@ class DetectorView : View {
|
||||
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 saveCropHandlePos() {
|
||||
val pos = getCropHandlePos()
|
||||
prefs.cropHandleX = pos.x
|
||||
prefs.cropHandleY = pos.y
|
||||
prefs.cropHandleOrientation = currentOrientation
|
||||
}
|
||||
|
||||
fun getCropHandlePos() = if (handleActive) {
|
||||
private fun getCropHandlePos() = if (handleActive) {
|
||||
handlePos
|
||||
} else {
|
||||
Point(-1, -1)
|
||||
}
|
||||
|
||||
fun mark(points: List<Point>) {
|
||||
marks = points
|
||||
fun restoreCropHandlePos() {
|
||||
setCropHandlePos(
|
||||
prefs.cropHandleX,
|
||||
prefs.cropHandleY,
|
||||
prefs.cropHandleOrientation
|
||||
)
|
||||
}
|
||||
|
||||
private fun setCropHandlePos(x: Int, y: Int, orientation: Int) {
|
||||
// Always set handlePos even if it's invalid because
|
||||
// handlePos.x may be -2 which is a signal to set the
|
||||
// default ROI.
|
||||
if (orientation == currentOrientation) {
|
||||
handlePos.set(x, y)
|
||||
} else {
|
||||
handlePos.set(y, x)
|
||||
}
|
||||
handleActive = handlePos.x > -1
|
||||
}
|
||||
|
||||
fun update(numberOfCoordinates: Int) {
|
||||
coordinatesLast = numberOfCoordinates
|
||||
invalidate()
|
||||
removeCallbacks(invalidateRunnable)
|
||||
postDelayed(invalidateRunnable, 500)
|
||||
@@ -143,11 +165,12 @@ class DetectorView : View {
|
||||
MotionEvent.ACTION_MOVE -> {
|
||||
if (handleGrabbed) {
|
||||
handlePos.set(x, y)
|
||||
if (distSq(handlePos, touchDown) > minMoveThresholdSq) {
|
||||
handleActive = true
|
||||
handleActive = handleActive ||
|
||||
distSq(handlePos, touchDown) > minMoveThresholdSq
|
||||
if (handleActive) {
|
||||
updateClipRect()
|
||||
invalidate()
|
||||
}
|
||||
updateClipRect()
|
||||
invalidate()
|
||||
true
|
||||
} else {
|
||||
false
|
||||
@@ -163,17 +186,14 @@ class DetectorView : View {
|
||||
MotionEvent.ACTION_UP -> {
|
||||
if (handleGrabbed) {
|
||||
if (!handleActive) {
|
||||
val mn = min(center.x, center.y) * .8f
|
||||
handlePos.set(
|
||||
(center.x + mn).roundToInt(),
|
||||
(center.y + mn).roundToInt()
|
||||
)
|
||||
handleActive = true
|
||||
invalidate()
|
||||
setHandleToDefaultRoi()
|
||||
} else {
|
||||
snap(x, y)
|
||||
}
|
||||
updateClipRect()
|
||||
if (handleActive) {
|
||||
updateClipRect()
|
||||
invalidate()
|
||||
}
|
||||
onRoiChanged?.invoke()
|
||||
handleGrabbed = false
|
||||
}
|
||||
@@ -183,15 +203,24 @@ class DetectorView : View {
|
||||
}
|
||||
}
|
||||
|
||||
private fun setHandleToDefaultRoi() {
|
||||
val mn = min(center.x, center.y) * .8f
|
||||
handlePos.set(
|
||||
(center.x + mn).roundToInt(),
|
||||
(center.y + mn).roundToInt()
|
||||
)
|
||||
handleActive = true
|
||||
}
|
||||
|
||||
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
|
||||
// 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
|
||||
// Check if handle is close to a screen corner.
|
||||
((abs(cy - minY) < distToFull || abs(maxY - cy) < distToFull) &&
|
||||
abs(dx - center.x) < distToFull)
|
||||
) {
|
||||
@@ -220,6 +249,9 @@ class DetectorView : View {
|
||||
width - handleXRadius - paddingRight - padding,
|
||||
height - handleYRadius - paddingBottom - fabHeight
|
||||
)
|
||||
if (handlePos.x == -2) {
|
||||
setHandleToDefaultRoi()
|
||||
}
|
||||
if (handleActive) {
|
||||
updateClipRect()
|
||||
} else {
|
||||
@@ -228,32 +260,36 @@ class DetectorView : View {
|
||||
}
|
||||
|
||||
override fun onDraw(canvas: Canvas) {
|
||||
canvas.drawColor(0, PorterDuff.Mode.CLEAR)
|
||||
if (handleActive) {
|
||||
drawClip(canvas)
|
||||
}
|
||||
marks?.let {
|
||||
dots.draw(canvas, it)
|
||||
canvas.drawClip()
|
||||
}
|
||||
canvas.drawDots()
|
||||
if (prefs.showCropHandle) {
|
||||
canvas.drawBitmap(
|
||||
handleBitmap,
|
||||
(handlePos.x - handleXRadius).toFloat(),
|
||||
(handlePos.y - handleYRadius).toFloat(),
|
||||
null
|
||||
canvas.drawHandle()
|
||||
}
|
||||
}
|
||||
|
||||
private fun Canvas.drawDots() {
|
||||
var i = 0
|
||||
while (i < coordinatesLast) {
|
||||
drawCircle(
|
||||
coordinates[i++],
|
||||
coordinates[i++],
|
||||
dotRadius,
|
||||
dotPaint
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun drawClip(canvas: Canvas) {
|
||||
private fun Canvas.drawClip() {
|
||||
if (minDist < 1) {
|
||||
return
|
||||
}
|
||||
// canvas.clipRect() doesn't work reliably below KITKAT
|
||||
// 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(
|
||||
save()
|
||||
clipOutPathCompat(
|
||||
calculateRoundedRectPath(
|
||||
roi.left.toFloat(),
|
||||
roi.top.toFloat(),
|
||||
@@ -263,13 +299,22 @@ class DetectorView : View {
|
||||
radius
|
||||
)
|
||||
)
|
||||
canvas.drawColor(shadeColor, PorterDuff.Mode.SRC)
|
||||
canvas.restore()
|
||||
drawColor(shadeColor)
|
||||
restore()
|
||||
} else {
|
||||
canvas.drawRect(roi, roiPaint)
|
||||
drawRect(roi, roiPaint)
|
||||
}
|
||||
}
|
||||
|
||||
private fun Canvas.drawHandle() {
|
||||
drawBitmap(
|
||||
handleBitmap,
|
||||
(handlePos.x - handleXRadius).toFloat(),
|
||||
(handlePos.y - handleYRadius).toFloat(),
|
||||
null
|
||||
)
|
||||
}
|
||||
|
||||
private fun updateClipRect() {
|
||||
clampHandlePos()
|
||||
val dx = abs(handlePos.x - center.x)
|
||||
|
||||
@@ -4,13 +4,19 @@ import android.content.Context
|
||||
import android.widget.Toast
|
||||
|
||||
fun Context.toast(message: Int) = Toast.makeText(
|
||||
this,
|
||||
applicationContext,
|
||||
message,
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
|
||||
fun Context.toast(message: String) = Toast.makeText(
|
||||
this,
|
||||
message,
|
||||
applicationContext,
|
||||
message.ellipsize(128),
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
|
||||
private fun String.ellipsize(max: Int) = if (length < max) {
|
||||
this
|
||||
} else {
|
||||
"${take(max)}…"
|
||||
}
|
||||
|
||||
@@ -1,197 +0,0 @@
|
||||
package de.markusfisch.android.binaryeye.zxing
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import com.google.zxing.*
|
||||
import com.google.zxing.common.HybridBinarizer
|
||||
import java.util.*
|
||||
|
||||
class Zxing(possibleResultPoint: ResultPointCallback? = null) {
|
||||
private val hints = EnumMap<DecodeHintType, Any>(DecodeHintType::class.java)
|
||||
private val multiFormatReader = MultiFormatReader()
|
||||
|
||||
init {
|
||||
val decodeFormats = EnumSet.noneOf<BarcodeFormat>(
|
||||
BarcodeFormat::class.java
|
||||
)
|
||||
decodeFormats.addAll(
|
||||
EnumSet.copyOf(
|
||||
listOf(
|
||||
BarcodeFormat.AZTEC,
|
||||
BarcodeFormat.CODABAR,
|
||||
BarcodeFormat.CODE_39,
|
||||
BarcodeFormat.CODE_93,
|
||||
BarcodeFormat.CODE_128,
|
||||
BarcodeFormat.DATA_MATRIX,
|
||||
BarcodeFormat.EAN_8,
|
||||
BarcodeFormat.EAN_13,
|
||||
BarcodeFormat.ITF,
|
||||
BarcodeFormat.MAXICODE,
|
||||
BarcodeFormat.PDF_417,
|
||||
BarcodeFormat.QR_CODE,
|
||||
BarcodeFormat.RSS_14,
|
||||
BarcodeFormat.RSS_EXPANDED,
|
||||
BarcodeFormat.UPC_A,
|
||||
BarcodeFormat.UPC_E,
|
||||
BarcodeFormat.UPC_EAN_EXTENSION
|
||||
)
|
||||
)
|
||||
)
|
||||
hints[DecodeHintType.POSSIBLE_FORMATS] = decodeFormats
|
||||
possibleResultPoint?.let {
|
||||
hints[DecodeHintType.NEED_RESULT_POINT_CALLBACK] = it
|
||||
}
|
||||
}
|
||||
|
||||
fun updateHints(tryHarder: Boolean) {
|
||||
if (tryHarder) {
|
||||
hints[DecodeHintType.TRY_HARDER] = java.lang.Boolean.TRUE
|
||||
} else {
|
||||
hints.remove(DecodeHintType.TRY_HARDER)
|
||||
}
|
||||
}
|
||||
|
||||
fun decode(
|
||||
yuvData: ByteArray,
|
||||
width: Int,
|
||||
height: Int,
|
||||
invert: Boolean = false
|
||||
): Result? {
|
||||
val source = PlanarYUVLuminanceSource(
|
||||
yuvData,
|
||||
width,
|
||||
height,
|
||||
0,
|
||||
0,
|
||||
width,
|
||||
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()
|
||||
} else {
|
||||
source
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun decodeLuminanceSource(source: LuminanceSource): Result? {
|
||||
val bitmap = BinaryBitmap(HybridBinarizer(source))
|
||||
return try {
|
||||
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()
|
||||
|
||||
fun encodeAsBitmap(
|
||||
text: String,
|
||||
format: BarcodeFormat,
|
||||
width: Int,
|
||||
height: Int
|
||||
): Bitmap? {
|
||||
val hints = EnumMap<EncodeHintType, Any>(EncodeHintType::class.java)
|
||||
hints[EncodeHintType.CHARACTER_SET] = "utf-8"
|
||||
val result = MultiFormatWriter().encode(
|
||||
text,
|
||||
format,
|
||||
width,
|
||||
height,
|
||||
hints
|
||||
)
|
||||
val w = result.width
|
||||
val h = result.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 (result.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,
|
||||
width: Int,
|
||||
height: Int
|
||||
): String {
|
||||
val hints = EnumMap<EncodeHintType, Any>(EncodeHintType::class.java)
|
||||
hints[EncodeHintType.CHARACTER_SET] = "utf-8"
|
||||
val result = MultiFormatWriter().encode(
|
||||
text,
|
||||
format,
|
||||
0,
|
||||
0,
|
||||
hints
|
||||
)
|
||||
val sb = StringBuilder()
|
||||
sb.append("<svg width=\"$width\" height=\"$height\"")
|
||||
sb.append(" viewBox=\"0 0 $width $height\"")
|
||||
sb.append(" xmlns=\"http://www.w3.org/2000/svg\">\n")
|
||||
val w = result.width
|
||||
val h = result.height
|
||||
val xf = width.toFloat() / w
|
||||
val yf = height.toFloat() / h
|
||||
for (y in 0 until h) {
|
||||
for (x in 0 until w) {
|
||||
if (result.get(x, y)) {
|
||||
val ox = x * xf
|
||||
val oy = y * yf
|
||||
sb.append("<rect x=\"$ox\" y=\"$oy\"")
|
||||
sb.append(" width=\"$xf\" height=\"$yf\"/>\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
sb.append("</svg>\n")
|
||||
return sb.toString()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,4 @@
|
||||
<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="M48.281 55.787a5.719 5.719 0 1 1 11.438 0 5.719 5.719 0 1 1-11.438 0" android:fillColor="#ffffff"/>
|
||||
<path android:pathData="M48.639 37.916l-3.271 3.575h-5.665c-1.965 0-3.574 1.608-3.574 3.574v21.444c0 1.966 1.609 3.575 3.574 3.575h28.594c1.965 0 3.574-1.609 3.574-3.575V45.065c0-1.966-1.609-3.574-3.574-3.574h-5.665l-3.271-3.575zM54 64.722c-4.932 0-8.935-4.003-8.935-8.935s4.003-8.935 8.935-8.935 8.935 4.003 8.935 8.935-4.003 8.935-8.935 8.935z" 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,3 @@
|
||||
<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.25h-13.5v13.5h-4.5v-13.5h-13.5v-4.5h13.5v-13.5h4.5v13.5h13.5z" 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,3 @@
|
||||
<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.114c0.09-0.675 0.135-1.372 0.135-2.114 0-0.719-0.045-1.439-0.158-2.114l4.566-3.553c0.405-0.315 0.517-0.922 0.27-1.372l-4.318-7.466C70.282 39 69.72 38.843 69.225 39l-5.375 2.159c-1.124-0.854-2.316-1.574-3.643-2.114l-0.81-5.712c-0.09-0.54-0.539-0.922-1.079-0.922h-8.636c-0.54 0-0.967 0.382-1.057 0.922l-0.809 5.712c-1.327 0.54-2.542 1.282-3.644 2.114L38.798 39c-0.495-0.18-1.057 0-1.327 0.495l-4.296 7.466c-0.269 0.472-0.179 1.057 0.27 1.372l4.566 3.553c-0.113 0.675-0.203 1.417-0.203 2.114 0 0.697 0.045 1.439 0.158 2.114L33.4 59.667c-0.404 0.315-0.517 0.922-0.27 1.372l4.318 7.466C37.718 69 38.28 69.158 38.775 69l5.375-2.159c1.124 0.855 2.316 1.574 3.643 2.114l0.81 5.712c0.112 0.54 0.539 0.922 1.079 0.922h8.636c0.54 0 0.989-0.382 1.057-0.922l0.809-5.712c1.327-0.54 2.542-1.259 3.644-2.114L69.202 69c0.495 0.18 1.057 0 1.327-0.495l4.318-7.466c0.27-0.495 0.158-1.057-0.27-1.372zM54 62.096c-4.453 0-8.096-3.643-8.096-8.096s3.643-8.096 8.096-8.096 8.096 3.643 8.096 8.096-3.643 8.096-8.096 8.096z" android:fillColor="#ffffff"/>
|
||||
</vector>
|
||||
|
Before Width: | Height: | Size: 5.8 KiB After Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 7.8 KiB After Width: | Height: | Size: 7.8 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 16 KiB |
@@ -1,3 +1,3 @@
|
||||
<vector android:height="24dp" android:tint="#FFFFFF" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="@android:color/white" android:pathData="M12.65 10C11.83 7.67 9.61 6 7 6c-3.31 0-6 2.69-6 6s2.69 6 6 6c2.61 0 4.83-1.67 5.65-4H17v4h4v-4h2v-4H12.65zM7 14c-1.1 0-2-0.9-2-2s0.9-2 2-2 2 0.9 2 2-0.9 2-2 2z"/>
|
||||
<path android:fillColor="#FFFFFF" android:pathData="M12.65 10C11.83 7.67 9.61 6 7 6c-3.31 0-6 2.69-6 6s2.69 6 6 6c2.61 0 4.83-1.67 5.65-4H17v4h4v-4h2v-4H12.65zM7 14c-1.1 0-2-0.9-2-2s0.9-2 2-2 2 0.9 2 2-0.9 2-2 2z"/>
|
||||
</vector>
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
<vector android:height="24dp" android:tint="#FFFFFF" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#FF000000" android:pathData="M3 11h8V3H3v8zm2-6h4v4H5V5zM3 21h8v-8H3v8zm2-6h4v4H5v-4zm8-12v8h8V3h-8zm6 6h-4V5h4v4zm0 10h2v2h-2zm-6-6h2v2h-2zm2 2h2v2h-2zm-2 2h2v2h-2zm2 2h2v2h-2zm2-2h2v2h-2zm0-4h2v2h-2zm2 2h2v2h-2z"/>
|
||||
</vector>
|
||||
@@ -1,3 +0,0 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24">
|
||||
<path android:fillColor="#FFFFFFFF" android:pathData="M4.413 15.067h-0.88v1.93h-1.37v-5.69h2.27c0.433 0 0.817 0.08 1.15 0.24 0.34 0.16 0.6 0.39 0.78 0.69 0.187 0.293 0.28 0.627 0.28 1 0 0.553-0.2 0.997-0.6 1.33-0.4 0.333-0.943 0.5-1.63 0.5zm-0.88-2.7v1.64h0.9c0.267 0 0.47-0.067 0.61-0.2s0.21-0.32 0.21-0.56c0-0.267-0.073-0.48-0.22-0.64-0.14-0.16-0.337-0.24-0.59-0.24h-0.91zm8.587-1.06v5.69h-1.36l-2.02-3.54v3.54H7.37v-5.69h1.37l2.01 3.54v-3.54h1.37zm5.527 2.65v2.34c-0.213 0.233-0.523 0.42-0.93 0.56-0.407 0.147-0.853 0.22-1.34 0.22-0.74 0-1.333-0.23-1.78-0.69-0.447-0.453-0.687-1.087-0.72-1.9v-0.49c0-0.56 0.1-1.05 0.3-1.47 0.193-0.413 0.477-0.733 0.85-0.96 0.367-0.227 0.793-0.34 1.28-0.34 0.707 0 1.257 0.163 1.65 0.49 0.387 0.32 0.613 0.803 0.68 1.45h-1.32c-0.047-0.32-0.147-0.547-0.3-0.68-0.16-0.133-0.383-0.2-0.67-0.2-0.34 0-0.603 0.143-0.79 0.43-0.193 0.293-0.29 0.71-0.29 1.25v0.35c0 0.567 0.097 0.993 0.29 1.28 0.193 0.28 0.5 0.42 0.92 0.42 0.353 0 0.617-0.077 0.79-0.23v-0.89h-0.95v-0.94h2.33zm4.733-6.991L18.481 6.96l1.463 1.464-2.065 2.066 0.975 0.975L20.921 9.4l1.463 1.464-0.005-3.898z"/>
|
||||
</vector>
|
||||
@@ -1,3 +0,0 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24">
|
||||
<path android:fillColor="#FFFFFFFF" android:pathData="M5.45 15.487c0-0.2-0.07-0.357-0.21-0.47-0.14-0.113-0.39-0.23-0.75-0.35-0.353-0.12-0.643-0.237-0.87-0.35-0.74-0.36-1.11-0.857-1.11-1.49 0-0.313 0.093-0.59 0.28-0.83 0.18-0.24 0.44-0.427 0.78-0.56 0.333-0.14 0.71-0.21 1.13-0.21 0.407 0 0.773 0.073 1.1 0.22 0.327 0.147 0.577 0.357 0.75 0.63 0.18 0.267 0.27 0.573 0.27 0.92H5.46c0-0.233-0.073-0.413-0.22-0.54-0.14-0.127-0.33-0.19-0.57-0.19-0.247 0-0.44 0.053-0.58 0.16-0.14 0.107-0.21 0.243-0.21 0.41 0 0.147 0.077 0.28 0.23 0.4 0.153 0.12 0.427 0.243 0.82 0.37 0.393 0.127 0.717 0.263 0.97 0.41 0.613 0.353 0.92 0.84 0.92 1.46 0 0.5-0.187 0.89-0.56 1.17-0.373 0.287-0.887 0.43-1.54 0.43-0.46 0-0.877-0.083-1.25-0.25-0.373-0.167-0.657-0.393-0.85-0.68-0.187-0.287-0.28-0.617-0.28-0.99h1.38c0 0.3 0.077 0.523 0.23 0.67 0.16 0.147 0.417 0.22 0.77 0.22 0.227 0 0.407-0.05 0.54-0.15 0.127-0.1 0.19-0.237 0.19-0.41zm3.158-4.18l1.13 4.14 1.13-4.14h1.53l-1.91 5.69h-1.5l-1.9-5.69h1.52zm8.814 2.65v2.34c-0.213 0.233-0.523 0.42-0.93 0.56-0.407 0.147-0.853 0.22-1.34 0.22-0.74 0-1.333-0.23-1.78-0.69-0.447-0.453-0.687-1.087-0.72-1.9v-0.49c0-0.56 0.1-1.05 0.3-1.47 0.193-0.413 0.477-0.733 0.85-0.96 0.367-0.227 0.793-0.34 1.28-0.34 0.707 0 1.257 0.163 1.65 0.49 0.387 0.32 0.613 0.803 0.68 1.45h-1.32c-0.047-0.32-0.147-0.547-0.3-0.68-0.16-0.133-0.383-0.2-0.67-0.2-0.34 0-0.603 0.143-0.79 0.43-0.193 0.293-0.29 0.71-0.29 1.25v0.35c0 0.567 0.097 0.993 0.29 1.28 0.193 0.28 0.5 0.42 0.92 0.42 0.353 0 0.617-0.077 0.79-0.23v-0.89h-0.95v-0.94h2.33zm4.164-7.051L17.69 6.9l1.463 1.464-2.065 2.066 0.975 0.975 2.065-2.066 1.464 1.464-0.005-3.898z"/>
|
||||
</vector>
|
||||