Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ecccf34418 | ||
|
|
4027ce2cc6 | ||
|
|
209e6c5c30 | ||
|
|
5e29f3fbe9 | ||
|
|
ba3d63277a | ||
|
|
fe34b7209d | ||
|
|
0efdca7b4a | ||
|
|
827fbedc1f | ||
|
|
107b5bafd4 | ||
|
|
19a71480fa | ||
|
|
909e97a591 | ||
|
|
bc2be96871 | ||
|
|
d834621d99 | ||
|
|
ad83be1a60 | ||
|
|
d65ea02b49 | ||
|
|
c8d1357ceb | ||
|
|
4c05d046e6 | ||
|
|
4423ee62c8 | ||
|
|
bc69f99f8b | ||
|
|
6b088031fd | ||
|
|
b27d03e9c0 | ||
|
|
9a13838f28 | ||
|
|
cce2a460f6 | ||
|
|
36352ecde2 | ||
|
|
9a0bc324cb | ||
|
|
6dabafd6a3 | ||
|
|
dde4b1e0bd | ||
|
|
8a8c7bd1ff | ||
|
|
a2aed42e25 | ||
|
|
a38b7b647f | ||
|
|
76e82ed842 | ||
|
|
fd54bc604e | ||
|
|
59b62ecbc4 | ||
|
|
de6a2863f3 | ||
|
|
4b2eb80612 | ||
|
|
2de40d2277 | ||
|
|
7673b7cb0d | ||
|
|
097c5e4453 | ||
|
|
1fab30089e | ||
|
|
f9bf16dd47 | ||
|
|
7a59126312 | ||
|
|
694c854650 | ||
|
|
b1cd12ec77 | ||
|
|
0c27b15025 | ||
|
|
5de950bdbf | ||
|
|
ae9e304ef6 | ||
|
|
c4cc14554b | ||
|
|
d6afccf562 | ||
|
|
a90bdad117 | ||
|
|
52d4f082f2 | ||
|
|
1c63f5b3ac | ||
|
|
21985b4533 | ||
|
|
012ac644aa | ||
|
|
19f19739fd | ||
|
|
f01f9fcd91 | ||
|
|
4177b248c3 | ||
|
|
e4182a265b | ||
|
|
98df5a447c | ||
|
|
a0a1405359 | ||
|
|
76922c421c | ||
|
|
cac9dd16da | ||
|
|
a1f9ba1056 | ||
|
|
0b67c272b6 | ||
|
|
9b8be5305d | ||
|
|
a026ade13b | ||
|
|
b1771bc5ea | ||
|
|
5199460669 | ||
|
|
fd040e1660 | ||
|
|
2e1528364e | ||
|
|
d21af142bb | ||
|
|
8395e135f9 | ||
|
|
c0caa0ce44 | ||
|
|
96927ef9b2 | ||
|
|
84d9a3b55a | ||
|
|
ffe8e4c5ba | ||
|
|
bc7c5dca97 | ||
|
|
d9737f4df1 | ||
|
|
0e8147713a | ||
|
|
3d425a097a | ||
|
|
c26765cc63 | ||
|
|
fb6dc8ba55 | ||
|
|
70f4d166a0 | ||
|
|
6c136ce719 | ||
|
|
c6dee80c0a | ||
|
|
1f43dd4827 | ||
|
|
a366157977 | ||
|
|
6bb72672a3 | ||
|
|
04774527a0 | ||
|
|
6f3e644aa8 | ||
|
|
5e3176b703 | ||
|
|
0f0aaff284 | ||
|
|
f0d09e81b4 | ||
|
|
771258cb5c | ||
|
|
9482086666 | ||
|
|
bcad50c3e8 | ||
|
|
dbd055699e | ||
|
|
d67f8273d6 | ||
|
|
649be0ceb6 | ||
|
|
f5eb5dd8d2 | ||
|
|
eb805dad2f | ||
|
|
202bb32b29 | ||
|
|
50b8a4d329 | ||
|
|
fa15478fb4 | ||
|
|
20c2a5a8aa | ||
|
|
00b95678ed | ||
|
|
5df3517563 | ||
|
|
fd6946d2c2 | ||
|
|
c74d229e1f | ||
|
|
fa99a19db5 | ||
|
|
b083fcf213 | ||
|
|
46930ddea6 | ||
|
|
7bef77b96e | ||
|
|
c5f4c088b4 | ||
|
|
73f7210e18 | ||
|
|
469429e663 | ||
|
|
1a7ac59740 | ||
|
|
c13c59e728 | ||
|
|
bc17e25717 | ||
|
|
5017bd4c8d | ||
|
|
a44a913eca | ||
|
|
007e83e243 | ||
|
|
de8f97c246 | ||
|
|
1287591e53 | ||
|
|
8c17238572 | ||
|
|
5dbb36e6a9 | ||
|
|
ca7dfb3a33 | ||
|
|
419b7d5765 | ||
|
|
fa38866669 | ||
|
|
19ca521f59 | ||
|
|
be657d86cf | ||
|
|
5199a4a526 | ||
|
|
ff48a543c9 | ||
|
|
2eadd01a87 | ||
|
|
dd2abe7d89 | ||
|
|
8e05caac70 | ||
|
|
46aeced09f | ||
|
|
3681c5d011 | ||
|
|
20b4f7aac5 | ||
|
|
bc6e350271 | ||
|
|
c9c1b2f5bf | ||
|
|
1dbc0f0fa1 | ||
|
|
ab8170ee72 | ||
|
|
551464dc6f | ||
|
|
c3aac0a1e1 | ||
|
|
d678e1cfd0 | ||
|
|
a445ade877 | ||
|
|
1fd0b7637f | ||
|
|
fdc88df897 | ||
|
|
d0f133ae00 | ||
|
|
67646261e7 | ||
|
|
faeae9e904 | ||
|
|
77b21a9e83 | ||
|
|
cbe7860a3d | ||
|
|
fc29267d3d | ||
|
|
890506328b | ||
|
|
04036b6cd6 | ||
|
|
2602021c1c | ||
|
|
17a0bdc0a9 | ||
|
|
d71a220eca | ||
|
|
0c2e711d79 | ||
|
|
ca342a83d3 | ||
|
|
3c44ff56eb | ||
|
|
bcae64c0d0 | ||
|
|
c2f673729b | ||
|
|
2043e0f349 | ||
|
|
b93eb5963e | ||
|
|
5189fc9c5a | ||
|
|
2d311ff92a | ||
|
|
14bb08003d | ||
|
|
42f9887030 | ||
|
|
a59d241b50 | ||
|
|
9f31160c7b | ||
|
|
2eb4e51c17 | ||
|
|
53a554e7a2 | ||
|
|
7f6887716a | ||
|
|
5ef93802ce | ||
|
|
c1eff08bb9 | ||
|
|
8798d44ec7 | ||
|
|
d4e31ff97e | ||
|
|
ae8b08a85a | ||
|
|
09430d776e | ||
|
|
ef88c3179d | ||
|
|
c732ec726b | ||
|
|
801c768df4 | ||
|
|
552a2f6d57 | ||
|
|
7fa8810197 | ||
|
|
254af25bbb | ||
|
|
a27656c174 | ||
|
|
2ca55ebccc | ||
|
|
ec1d40942d | ||
|
|
7852e5ce9d | ||
|
|
f36a81d4a0 | ||
|
|
d86ea4073c | ||
|
|
a69530621d | ||
|
|
3c0f29a768 | ||
|
|
ccf5157fa7 | ||
|
|
c1ebb1c154 | ||
|
|
a4778bcc02 | ||
|
|
24d2c6d8d6 | ||
|
|
0f3a07f602 | ||
|
|
751d8932f8 | ||
|
|
9c2d063c24 | ||
|
|
0f9bbf5b2f | ||
|
|
530280981e | ||
|
|
ddfbb305c7 | ||
|
|
8d1a29922b | ||
|
|
c710f7c6ab | ||
|
|
111c587eef | ||
|
|
264e494487 | ||
|
|
8135b2a3dc | ||
|
|
dce62b70d3 | ||
|
|
533812c638 | ||
|
|
5dc5bed281 | ||
|
|
2a13561962 | ||
|
|
1476f41cda | ||
|
|
0da1ffbe02 | ||
|
|
980679b6a3 | ||
|
|
28cbb55299 | ||
|
|
1a4e7d100a | ||
|
|
265e43b439 | ||
|
|
03a58c3412 | ||
|
|
327cad48f0 | ||
|
|
8e64fe2f56 | ||
|
|
368d4dbb66 | ||
|
|
c8c3c094db | ||
|
|
aa4aefe35f | ||
|
|
536b3d77c1 | ||
|
|
bbf2a80981 | ||
|
|
ae99214d6c | ||
|
|
73b5a0969c | ||
|
|
bf5dc96ac9 | ||
|
|
301d9c2513 | ||
|
|
0a11611b8f | ||
|
|
249e310e43 | ||
|
|
6279c5731a | ||
|
|
df98637951 | ||
|
|
08b3558e46 | ||
|
|
648ef6fdf8 | ||
|
|
807f075a51 | ||
|
|
27789beeed | ||
|
|
97bee27827 | ||
|
|
3fee49301e | ||
|
|
8a6073291b | ||
|
|
47151de4a4 | ||
|
|
248c4edca1 | ||
|
|
2c7386d536 | ||
|
|
e19cad4c87 | ||
|
|
585e0f3372 | ||
|
|
2b7f611774 | ||
|
|
05b2a0d9c9 | ||
|
|
e7287eb088 | ||
|
|
4b45ad3fe7 | ||
|
|
449d8fdbbd | ||
|
|
f44cff9912 | ||
|
|
24730dfde3 | ||
|
|
be6539d41f | ||
|
|
60dd40554e | ||
|
|
837119b6ec | ||
|
|
abe11b82e5 | ||
|
|
4fc1e36d04 | ||
|
|
54aa15e1be | ||
|
|
876d8fc7b3 | ||
|
|
7bdcdaaa5d | ||
|
|
fd44728e89 | ||
|
|
8242520a0b | ||
|
|
2e7e9a942b | ||
|
|
dcc9d5b3ff | ||
|
|
607aab854c | ||
|
|
cc2b2f076b | ||
|
|
90e9723de5 | ||
|
|
ab6787e60b | ||
|
|
698686a45a | ||
|
|
95bdaa1252 | ||
|
|
f0c02e5156 | ||
|
|
a8c50308d0 | ||
|
|
6b359d4c65 | ||
|
|
863caed387 | ||
|
|
e4f17b9bdf | ||
|
|
c2071d8d81 | ||
|
|
4758c2d337 | ||
|
|
91336fe15c | ||
|
|
2c437f3771 | ||
|
|
cb082b20fe | ||
|
|
bce3630686 | ||
|
|
4feb4d066c | ||
|
|
e7d84cd916 | ||
|
|
84e2917694 | ||
|
|
b87464ec40 | ||
|
|
269e0f41a6 | ||
|
|
aefbc4a296 | ||
|
|
c64984dc6a | ||
|
|
24c7ad0ba4 | ||
|
|
643a935852 | ||
|
|
4fcb836b53 | ||
|
|
48ed5f1a2c | ||
|
|
3aa2371eeb | ||
|
|
88f92943c0 | ||
|
|
39710d4ab9 | ||
|
|
93f7cd3cb8 | ||
|
|
69baa3b824 | ||
|
|
bb05e38278 | ||
|
|
554f5a5e73 | ||
|
|
2dcf7772f2 | ||
|
|
3c7012e3c4 | ||
|
|
08e11c3c40 | ||
|
|
31854dab0c | ||
|
|
4f12c3e270 | ||
|
|
3ec43dabce | ||
|
|
0d4f536f71 | ||
|
|
b064a293fb | ||
|
|
d124c29919 | ||
|
|
2e89f1530a | ||
|
|
ef7e2a4b0d | ||
|
|
f680533819 | ||
|
|
a8c977c40e | ||
|
|
1e89b22953 | ||
|
|
11f028e0f3 | ||
|
|
1f7f4a23b5 | ||
|
|
41581d07ec | ||
|
|
dc819a6e1a | ||
|
|
5b51072f1a | ||
|
|
1b10e82cb2 | ||
|
|
eeae1eb0ad | ||
|
|
21c29ca373 | ||
|
|
8bcb72e91b | ||
|
|
8f345fbfbe | ||
|
|
2c88dbca02 | ||
|
|
bd1c629c85 | ||
|
|
f89e6612c4 | ||
|
|
7a0cd0ce19 | ||
|
|
8544affa1e | ||
|
|
48d3588341 | ||
|
|
783f6e4892 | ||
|
|
032cd64daf | ||
|
|
96626e6b00 | ||
|
|
11ae65ec3e | ||
|
|
91e3acb647 | ||
|
|
40c2f5947b | ||
|
|
58b9466cfc | ||
|
|
12210e1768 | ||
|
|
ad2ac457c9 | ||
|
|
634b6e8d2b | ||
|
|
6dd47f4dd0 | ||
|
|
8426f66cfb | ||
|
|
3dfa84a33e | ||
|
|
38f658e253 | ||
|
|
287d99e2b4 | ||
|
|
5cfcde9d4a | ||
|
|
b2730259fc | ||
|
|
61a2d3023c | ||
|
|
239ac7bd1a | ||
|
|
4048314cf7 | ||
|
|
3dbd772847 | ||
|
|
3a08465c35 |
+176
@@ -1,5 +1,181 @@
|
||||
# Change Log
|
||||
|
||||
## 1.68.7
|
||||
* Add icons to preferences screen
|
||||
* Update history listing on resume
|
||||
* Fix quiet zone for recreated barcodes
|
||||
* Fix automatic screen brightening
|
||||
* Migrate to CameraX
|
||||
|
||||
## 1.68.6
|
||||
* Update ZXingCpp
|
||||
* Ask for confirmation before deleting a scan from the result screen
|
||||
|
||||
## 1.68.5
|
||||
* Downgrade ZXingCpp for compatibilty
|
||||
|
||||
## 1.68.4
|
||||
* Fix generating 1D barcodes
|
||||
|
||||
## 1.68.3
|
||||
* Stability improvement
|
||||
|
||||
## 1.68.2
|
||||
* Fix barcode regeneration
|
||||
|
||||
## 1.68.1
|
||||
* Improve UI of search barcode dialog
|
||||
* Stability improvements
|
||||
* Update ZXingCpp
|
||||
* Update Russian translation
|
||||
|
||||
## 1.68.0
|
||||
* Add automated actions
|
||||
* Improve managing profiles
|
||||
|
||||
## 1.67.1
|
||||
* Fix opening file:// Uris
|
||||
* Fix auto-return/immediate opening
|
||||
* Improve Bluetooth stability
|
||||
|
||||
## 1.67.0
|
||||
* Add preference profiles
|
||||
* Support regex when searching for a barcode
|
||||
* Improve feedback when searching for a barcode
|
||||
* Update translations
|
||||
|
||||
## 1.66.2
|
||||
* Remove forcing `?content=` on forwarding URL
|
||||
* Update Spanish translation
|
||||
|
||||
## 1.66.1
|
||||
* Show elements of invalid Wifi configurations too
|
||||
* Update translations
|
||||
|
||||
## 1.66.0
|
||||
* Support deep links to encode content via web links
|
||||
* Parse International Driver Licenses (IDL) in PDF417 barcodes
|
||||
* Add WiFis from system settings
|
||||
* Show parsed data items
|
||||
* Update translations
|
||||
|
||||
## 1.65.2
|
||||
* Sanity check ROI metrics
|
||||
|
||||
## 1.65.1
|
||||
* Improve simple mode
|
||||
* Remember quiet zone settings
|
||||
* Reduce height for 1D barcodes
|
||||
* Add execute argument for binaryeye://encode
|
||||
* Update Russian translation
|
||||
|
||||
## 1.65.0
|
||||
* Add a welcome screen to choose a mode
|
||||
* Add a deep link to encode a barcode
|
||||
* Remember zoom level of generated barcodes
|
||||
* Remove Human Readable Interpretation (HRI) decoration
|
||||
* Update Italian translation
|
||||
|
||||
## 1.64.2
|
||||
* Separate crop window settings for image scanning
|
||||
* Fix initializing dataMask
|
||||
* Update Russian translation
|
||||
|
||||
## 1.64.1
|
||||
* Handle empty input for recreation
|
||||
* Fix setting up database
|
||||
|
||||
## 1.64.0
|
||||
* Find barcode by content
|
||||
* Immediately replace toasts
|
||||
* Use detected barcode for recreation
|
||||
* Show data mask of QR Codes
|
||||
* Fix deep linking
|
||||
* Replace margin with quiet zone switch when generating barcodes
|
||||
* Update Russian translation
|
||||
* Update Portuguese (Brazil) translation
|
||||
|
||||
## 1.63.13
|
||||
* Fix UTF-8 conversion of binary escape sequences when creating barcodes
|
||||
* Fix scan order for intervals under one second
|
||||
* Add arabic translation
|
||||
* Update French translation
|
||||
|
||||
## 1.63.12
|
||||
* Fix magnifying barcode recreation
|
||||
|
||||
## 1.63.11
|
||||
* Update parcel tracking URL
|
||||
* Update multiple translations
|
||||
* Only ask for barcode size when exporting/sharing
|
||||
|
||||
## 1.63.10
|
||||
* Use maximum brightness when showing a generated barcode
|
||||
* Allow file input from share menu
|
||||
* Update multiple translations
|
||||
|
||||
## 1.63.9
|
||||
* Export/share as JPEG
|
||||
* Ignore "URL:" before URLs in barcode content
|
||||
* Fix exporting/sharing multiple history items
|
||||
* Update multiple translations
|
||||
|
||||
## 1.63.8
|
||||
* Add an option to show a checksum for the decoded content
|
||||
* Add support for encoding escape sequences for non-printable characters
|
||||
* Update Italian translation
|
||||
|
||||
## 1.63.7
|
||||
* Update ZXingCpp for Aztec Runes
|
||||
* Update Italian translation
|
||||
|
||||
## 1.63.6
|
||||
* Add history shortcut
|
||||
* Return Codabar start/stop codes
|
||||
* Update French, Russian, Spanish and Ukrainian translations
|
||||
|
||||
## 1.63.5
|
||||
* Add an option to ignore any duplicates
|
||||
* Fix invalid bluetooth connection
|
||||
* Update Russian translation
|
||||
* Update Ukrainian translation
|
||||
|
||||
## 1.63.4
|
||||
* Update Chinese translation
|
||||
* Improve support for old devices
|
||||
|
||||
## 1.63.3
|
||||
* Fix SCAN Intent
|
||||
|
||||
## 1.63.2
|
||||
* Add DX Film Edge barcode format
|
||||
* Update Turkish translation
|
||||
* Update Ukrainian translation
|
||||
|
||||
## 1.63.1
|
||||
* Include scan label in exports/shares
|
||||
* Update Russian translation
|
||||
|
||||
## 1.63.0
|
||||
* Edit scan label in decoding screen
|
||||
* Add a button to add generated codes to the history
|
||||
* Improve scanning from images
|
||||
* Improve asking for camera permission
|
||||
* Fix margin for recreated barcodes
|
||||
* Limit duration of error beep to 1s
|
||||
* Update Ukrainian translation
|
||||
* Update Spanish translation
|
||||
|
||||
## 1.62.3
|
||||
* Extend selection with single taps in history listing
|
||||
* Update Brazilian Portuguese translation
|
||||
|
||||
## 1.62.2
|
||||
* Include rMQR Code for updates
|
||||
|
||||
## 1.62.1
|
||||
* Make rMQR Code selectable
|
||||
|
||||
## 1.62.0
|
||||
* Add support for rMQR (rectangular Micro QR) Code
|
||||
* Share history as a file
|
||||
|
||||
@@ -16,5 +16,10 @@ to squash when merging into master:
|
||||
Then write a [good commit message][commit_messages] to keep the history
|
||||
meaningful and useful. One feature, one commit.
|
||||
|
||||
## Translations
|
||||
|
||||
For translations use [Toolate][toolate].
|
||||
|
||||
[android_best_practices]: https://developer.android.com/distribute/best-practices/develop/
|
||||
[commit_messages]: https://git-scm.com/book/en/v2/Distributed-Git-Contributing-to-a-Project#_commit_guidelines
|
||||
[toolate]: https://toolate.othing.xyz/projects/binaryeye/
|
||||
|
||||
@@ -28,6 +28,26 @@ testscan:
|
||||
-c android.intent.category.BROWSABLE \
|
||||
-d 'binaryeye://scan/?ret=http%3A%2F%2Fmarkusfisch.de%2F%3Fresult%3D{RESULT}'
|
||||
|
||||
testencode:
|
||||
adb shell am start -a android.intent.action.VIEW \
|
||||
-c android.intent.category.BROWSABLE \
|
||||
-d 'binaryeye://encode?content=Test\&format=QR_CODE'
|
||||
|
||||
testencodenotqr:
|
||||
adb shell am start -a android.intent.action.VIEW \
|
||||
-c android.intent.category.BROWSABLE \
|
||||
-d 'binaryeye://encode?content=Test\&format=DATA_MATRIX'
|
||||
|
||||
testencodeexec:
|
||||
adb shell am start -a android.intent.action.VIEW \
|
||||
-c android.intent.category.BROWSABLE \
|
||||
-d 'binaryeye://encode?content=Test\&format=QR_CODE\&execute'
|
||||
|
||||
testencodeurl:
|
||||
adb shell am start -a android.intent.action.VIEW \
|
||||
-c android.intent.category.BROWSABLE \
|
||||
-d 'http://markusfisch.de/encode?content=Test\&format=QR_CODE'
|
||||
|
||||
testurl:
|
||||
adb shell am start -a android.intent.action.VIEW \
|
||||
-c android.intent.category.BROWSABLE \
|
||||
|
||||
+1
-1
@@ -15,7 +15,7 @@ Before Android Q, this feature requires using
|
||||
[WifiManager.getConfiguredNetworks][getConfiguredNetworks]
|
||||
which requires `ACCESS_FINE_LOCATION`.
|
||||
|
||||
On Android Q an better, this permission is not requested nor required.
|
||||
On Android Q and better, this permission is not requested nor required.
|
||||
|
||||
### [BLUETOOTH][BLUETOOTH], [BLUETOOTH_ADMIN][BLUETOOTH_ADMIN] and [BLUETOOTH_CONNECT][BLUETOOTH_CONNECT]
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
Yet another barcode scanner for Android. As if there weren't [enough][play].
|
||||
|
||||
This one is free, without any ads and open source.
|
||||
This one is free, without any ads, and open source.
|
||||
|
||||
Works in portrait and landscape orientation, can read inverted codes,
|
||||
comes in Material Design and can also generate barcodes.
|
||||
@@ -10,6 +10,23 @@ comes in Material Design and can also generate barcodes.
|
||||
Binary Eye uses the [ZXing-C++][zxing_cpp] ("Zebra Crossing") barcode
|
||||
scanning library.
|
||||
|
||||
If you find this app useful and wish to support its continued development,
|
||||
you can [buy me a coffee](https://www.buymeacoffee.com/markusfisch) or
|
||||
send some Bitcoin decimals to `bc1q2guk2rpll587aymrfadkdtpq32448x5khk5j8z`.
|
||||
|
||||
<a href="https://www.buymeacoffee.com/markusfisch" target="_blank"><img
|
||||
src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png"
|
||||
alt="Buy Me A Coffee"
|
||||
style="height: 60px !important;width: 217px !important;"/></a> <a
|
||||
href="https://liberapay.com/markusfisch/" target="_blank"><img
|
||||
src="https://liberapay.com/assets/widgets/donate.svg"
|
||||
alt="Liberapay"
|
||||
style="height: 60px !important"/></a> <a
|
||||
href="https://ko-fi.com/markusfisch" target="_blank"><img
|
||||
src="https://storage.ko-fi.com/cdn/brandasset/kofi_s_tag_dark.png"
|
||||
alt="Ko-fi"
|
||||
style="height: 60px !important"/></a>
|
||||
|
||||
## Screenshots
|
||||
|
||||
<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/screencap-scanning.png"
|
||||
@@ -36,6 +53,7 @@ ZXing can read the following barcode formats:
|
||||
* [CODE 93][code_93]
|
||||
* [CODE 128][code_128]
|
||||
* [DATA MATRIX][data_matrix]
|
||||
* [DX FILM EDGE][dx_film_edge]
|
||||
* [EAN 8][ean_8]
|
||||
* [EAN 13][ean_13]
|
||||
* [ITF][itf]
|
||||
@@ -43,6 +61,7 @@ ZXing can read the following barcode formats:
|
||||
* [PDF417][pdf417]
|
||||
* [QR CODE][qr_code]
|
||||
* [Micro QR Code][micro_qr_code]
|
||||
* [rMQR Code][rmqr_code]
|
||||
* [RSS 14][rss]
|
||||
* [RSS EXPANDED][rss]
|
||||
* [UPC A][upc_a]
|
||||
@@ -64,14 +83,14 @@ ZXing can generate the following barcode formats:
|
||||
* [QR CODE][qr_code]
|
||||
* [UPC A][upc_a]
|
||||
|
||||
## Deep Links and Intents
|
||||
|
||||
### Deep Links
|
||||
## Deep Links
|
||||
|
||||
You can invoke Binary Eye with a web URI intent from anything that can
|
||||
open URIs. There are two options:
|
||||
open URIs.
|
||||
|
||||
1. [binaryeye://scan](binaryeye://scan)
|
||||
### Decoding
|
||||
|
||||
1. [binaryeye://scan](binaryeye://scan) (note: GitHub does not render this as clickable link)
|
||||
2. [http(s)://markusfisch.de/BinaryEye](http://markusfisch.de/BinaryEye)
|
||||
|
||||
If you want to get the scanned contents, you can add a `ret` query
|
||||
@@ -85,10 +104,32 @@ Supported symbols are:
|
||||
* `RESULT_BYTES` - raw result as a hex string
|
||||
* `FORMAT` - barcode format
|
||||
|
||||
### Encoding
|
||||
|
||||
1. [binaryeye://encode](binaryeye://encode)
|
||||
|
||||
You can use the URL arguments `content` and `format` to automatically
|
||||
preset this data. For example:
|
||||
|
||||
1. [binaryeye://encode?content=Test&format=QR_CODE](binaryeye://encode?content=Test&format=QR_CODE)
|
||||
2. [http(s)://markusfisch.de/encode?content=Test&format=QR_CODE](http://markusfisch.de/encode?content=Test&format=QR_CODE)
|
||||
3. [http(s)://markusfisch.de/encode?content=Test&format=DATA_MATRIX](http://markusfisch.de/encode?content=Test2&format=DATA_MATRIX&execute)
|
||||
|
||||
If you want the code to be generated immediately, add `execute=true` (or
|
||||
just `execute`).
|
||||
|
||||
## Intents
|
||||
|
||||
You can also use Binary Eye from other apps by using an [Intent][intent].
|
||||
|
||||
If you prefer to integrate a barcode scanner into your app, take a look at
|
||||
[BarcodeScannerView][barcode_scanner_view] (if you also want to use
|
||||
[ZXing-C++][zxing_cpp]) or read how to
|
||||
[scan barcodes with ML Kit on Android][mlkit_barcode_scanning].
|
||||
|
||||
### SCAN Intent
|
||||
|
||||
You can also use Binary Eye from other apps by using the
|
||||
`com.google.zxing.client.android.SCAN` Intent with
|
||||
Use the `com.google.zxing.client.android.SCAN` `Intent` with
|
||||
[startActivityForResult()][start_activity] like this:
|
||||
|
||||
```kotlin
|
||||
@@ -120,10 +161,12 @@ 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")
|
||||
class YourActivity : AppCompatActivity() {
|
||||
private val resultLauncher = registerForActivityResult(
|
||||
ActivityResultContracts.StartActivityForResult()
|
||||
) { result ->
|
||||
if (result.resultCode == RESULT_OK) {
|
||||
val content = result.data?.getStringExtra("SCAN_RESULT")
|
||||
…
|
||||
}
|
||||
}
|
||||
@@ -134,6 +177,21 @@ class YourActivity : Activity() {
|
||||
}
|
||||
```
|
||||
|
||||
## Forwarding scans via Bluetooth
|
||||
|
||||
**Note:** The companion apps aren't developed nor maintained by the
|
||||
author of Binary Eye.
|
||||
|
||||
In order to set up Bluetooth forwarding to a computer:
|
||||
|
||||
1. Pair your devices (phone and pc) if not paired already
|
||||
2. Download and run a companion app:
|
||||
- Windows: https://github.com/KamaleiZestri/BinaryReceptorWindows
|
||||
- Linux: https://github.com/sean666888/bin_eye_bt_receiver
|
||||
3. In the settings of Binary Eye, enable "Forward scans with Bluetooth"
|
||||
and select the appropriate target host device that is running the
|
||||
companion application.
|
||||
|
||||
[play]: https://play.google.com/store/search?q=barcode%20scanner&c=apps
|
||||
[zxing_cpp]: https://github.com/zxing-cpp/zxing-cpp
|
||||
[kotlin]: http://kotlinlang.org/
|
||||
@@ -143,6 +201,7 @@ class YourActivity : Activity() {
|
||||
[code_93]: https://en.wikipedia.org/wiki/Code_93
|
||||
[code_128]: https://en.wikipedia.org/wiki/Code_128
|
||||
[data_matrix]: https://en.wikipedia.org/wiki/Data_Matrix
|
||||
[dx_film_edge]: https://en.wikipedia.org/wiki/DX_encoding
|
||||
[ean_8]: https://en.wikipedia.org/wiki/EAN-8
|
||||
[ean_13]: https://en.wikipedia.org/wiki/International_Article_Number
|
||||
[itf]: https://en.wikipedia.org/wiki/Interleaved_2_of_5
|
||||
@@ -150,10 +209,14 @@ class YourActivity : Activity() {
|
||||
[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
|
||||
[rmqr_code]: https://www.qrcode.com/en/codes/rmqr.html
|
||||
[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
|
||||
[intent]: https://developer.android.com/reference/android/content/Intent
|
||||
[barcode_scanner_view]: https://github.com/markusfisch/BarcodeScannerView
|
||||
[mlkit_barcode_scanning]: https://developers.google.com/ml-kit/vision/barcode-scanning/android
|
||||
[start_activity]: https://developer.android.com/reference/android/app/Activity#startActivityForResult(android.content.Intent,%20int)
|
||||
[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
|
||||
|
||||
+36
-23
@@ -1,15 +1,15 @@
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
|
||||
android {
|
||||
namespace 'de.markusfisch.android.binaryeye'
|
||||
compileSdkVersion sdk_version
|
||||
defaultConfig {
|
||||
minSdkVersion 9
|
||||
targetSdkVersion sdk_version
|
||||
namespace "de.markusfisch.android.binaryeye"
|
||||
compileSdk sdk_version
|
||||
|
||||
versionCode 124
|
||||
versionName '1.62.0'
|
||||
defaultConfig {
|
||||
minSdk 21
|
||||
targetSdk sdk_version
|
||||
|
||||
versionCode 161
|
||||
versionName '1.68.7'
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
@@ -34,13 +34,17 @@ android {
|
||||
}
|
||||
|
||||
release {
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||
minifyEnabled true
|
||||
shrinkResources true
|
||||
signingConfig signingConfigs.release
|
||||
}
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
buildConfig true
|
||||
}
|
||||
|
||||
bundle {
|
||||
language {
|
||||
// To make the app bundle contain all language resources
|
||||
@@ -53,24 +57,33 @@ android {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Required since Gradle 8.
|
||||
compileOptions {
|
||||
// Required for Gradle 8.
|
||||
sourceCompatibility = 17
|
||||
targetCompatibility = 17
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
targetCompatibility = JavaVersion.VERSION_1_8
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
testImplementation 'junit:junit:4.13.2'
|
||||
|
||||
// Kotlin
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4"
|
||||
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"
|
||||
implementation 'com.github.markusfisch:CameraView:1.9.2'
|
||||
implementation 'com.github.markusfisch:ScalingImageView:1.4.1'
|
||||
implementation 'com.github.markusfisch:zxing-cpp:v2.2.0.1'
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.10.2"
|
||||
implementation "androidx.core:core-ktx:1.17.0"
|
||||
|
||||
// Support
|
||||
implementation "androidx.appcompat:appcompat:1.7.1"
|
||||
implementation "com.google.android.material:material:1.13.0"
|
||||
implementation "androidx.preference:preference:1.2.1"
|
||||
|
||||
// External
|
||||
implementation 'androidx.camera:camera-core:1.4.2'
|
||||
implementation 'androidx.camera:camera-camera2:1.4.2'
|
||||
implementation 'androidx.camera:camera-lifecycle:1.4.2'
|
||||
implementation 'androidx.camera:camera-view:1.4.2'
|
||||
implementation 'com.github.markusfisch:ScalingImageView:1.4.3'
|
||||
implementation 'com.github.markusfisch:zxing-cpp:v3.0.2.1'
|
||||
|
||||
// Testing
|
||||
testImplementation 'junit:junit:4.13.2'
|
||||
testImplementation 'org.robolectric:robolectric:4.16.1'
|
||||
}
|
||||
|
||||
Binary file not shown.
@@ -19,8 +19,9 @@
|
||||
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
|
||||
<uses-feature android:name="android.hardware.camera"/>
|
||||
<uses-feature android:name="android.hardware.camera.autofocus"/>
|
||||
<uses-sdk tools:overrideLibrary="android.support.v14.preference"/>
|
||||
<uses-feature android:name="android.hardware.camera.autofocus"
|
||||
android:required="false" />
|
||||
<uses-sdk tools:overrideLibrary="androidx.preference"/>
|
||||
<queries>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
@@ -62,7 +63,7 @@
|
||||
<intent-filter android:label="@string/compose_barcode">
|
||||
<action android:name="android.intent.action.SEND"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<data android:mimeType="text/plain"/>
|
||||
<data android:mimeType="text/*"/>
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="com.google.zxing.client.android.SCAN"/>
|
||||
@@ -75,6 +76,15 @@
|
||||
<data android:host="details" android:scheme="market"
|
||||
android:pathSuffix="id=com.xiaomi.scanner"/>
|
||||
</intent-filter>
|
||||
<intent-filter android:autoVerify="true">
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<category android:name="android.intent.category.BROWSABLE"/>
|
||||
<data android:host="markusfisch.de"/>
|
||||
<data android:pathPrefix="/BinaryEye"/>
|
||||
<data android:scheme="http"/>
|
||||
<data android:scheme="https"/>
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
@@ -99,20 +109,61 @@
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".activity.MainActivity"
|
||||
android:name=".activity.WelcomeActivity"
|
||||
android:exported="true"/>
|
||||
<activity
|
||||
android:name=".activity.PreferencesActivity"
|
||||
android:exported="true"/>
|
||||
<activity
|
||||
android:name=".activity.HistoryActivity"
|
||||
android:exported="true"/>
|
||||
<activity
|
||||
android:name=".activity.EncodeActivity"
|
||||
android:exported="true">
|
||||
<intent-filter android:autoVerify="true">
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<category android:name="android.intent.category.BROWSABLE"/>
|
||||
<data android:host="markusfisch.de"/>
|
||||
<data android:pathPrefix="/encode"/>
|
||||
<data android:scheme="http"/>
|
||||
<data android:scheme="https"/>
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<category android:name="android.intent.category.BROWSABLE"/>
|
||||
<data android:scheme="binaryeye" android:host="encode"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".activity.DecodeActivity"
|
||||
android:exported="false"/>
|
||||
<activity
|
||||
android:name=".activity.BarcodeActivity"
|
||||
android:exported="false"/>
|
||||
<activity
|
||||
android:name=".activity.ProfilesActivity"
|
||||
android:exported="false"/>
|
||||
<activity
|
||||
android:name=".activity.AutomatedActionsActivity"
|
||||
android:exported="false"/>
|
||||
<activity
|
||||
android:name=".activity.NetworkSuggestionsActivity"
|
||||
android:exported="false"/>
|
||||
<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">
|
||||
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE"
|
||||
tools:targetApi="n">
|
||||
<intent-filter>
|
||||
<action android:name="android.service.quicksettings.action.QS_TILE"/>
|
||||
</intent-filter>
|
||||
</service>
|
||||
<provider
|
||||
android:name="android.support.v4.content.FileProvider"
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="${applicationId}.provider"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true">
|
||||
|
||||
+11
-9
@@ -6,15 +6,17 @@ import de.markusfisch.android.binaryeye.content.execShareIntent
|
||||
import de.markusfisch.android.binaryeye.content.openUrl
|
||||
import de.markusfisch.android.binaryeye.widget.toast
|
||||
|
||||
interface IAction {
|
||||
val iconResId: Int
|
||||
val titleResId: Int
|
||||
abstract class Action {
|
||||
abstract val iconResId: Int
|
||||
abstract val titleResId: Int
|
||||
|
||||
fun canExecuteOn(data: ByteArray): Boolean
|
||||
suspend fun execute(context: Context, data: ByteArray)
|
||||
var fired: Boolean = false
|
||||
|
||||
abstract fun canExecuteOn(data: ByteArray): Boolean
|
||||
abstract suspend fun execute(context: Context, data: ByteArray)
|
||||
}
|
||||
|
||||
abstract class IntentAction : IAction {
|
||||
abstract class IntentAction : Action() {
|
||||
abstract val errorMsg: Int
|
||||
|
||||
final override suspend fun execute(context: Context, data: ByteArray) {
|
||||
@@ -22,7 +24,7 @@ abstract class IntentAction : IAction {
|
||||
if (intent == null) {
|
||||
context.toast(errorMsg)
|
||||
} else {
|
||||
context.execShareIntent(intent)
|
||||
fired = context.execShareIntent(intent)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,7 +34,7 @@ abstract class IntentAction : IAction {
|
||||
): Intent?
|
||||
}
|
||||
|
||||
abstract class SchemeAction : IAction {
|
||||
abstract class SchemeAction : Action() {
|
||||
abstract val scheme: String
|
||||
open val buildRegex: Boolean = false
|
||||
|
||||
@@ -50,6 +52,6 @@ abstract class SchemeAction : IAction {
|
||||
}
|
||||
|
||||
final override suspend fun execute(context: Context, data: ByteArray) {
|
||||
context.openUrl(String(data))
|
||||
fired = context.openUrl(String(data))
|
||||
}
|
||||
}
|
||||
@@ -12,9 +12,9 @@ import de.markusfisch.android.binaryeye.actions.web.WebAction
|
||||
import de.markusfisch.android.binaryeye.actions.wifi.WifiAction
|
||||
|
||||
object ActionRegistry {
|
||||
val DEFAULT_ACTION: IAction = OpenOrSearchAction
|
||||
val DEFAULT_ACTION: Action = OpenOrSearchAction
|
||||
|
||||
private val REGISTRY: Set<IAction> = setOf(
|
||||
private val REGISTRY: Set<Action> = setOf(
|
||||
MailAction,
|
||||
MatMsgAction,
|
||||
OtpauthAction,
|
||||
@@ -28,7 +28,7 @@ object ActionRegistry {
|
||||
WebAction
|
||||
)
|
||||
|
||||
fun getAction(data: ByteArray): IAction = REGISTRY.find {
|
||||
fun getAction(data: ByteArray): Action = REGISTRY.find {
|
||||
it.canExecuteOn(data)
|
||||
} ?: DEFAULT_ACTION
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ package de.markusfisch.android.binaryeye.actions.mail
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import androidx.core.net.toUri
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.actions.IntentAction
|
||||
|
||||
@@ -24,7 +24,7 @@ object MailAction : IntentAction() {
|
||||
)?.groupValues?.get(1) ?: return null
|
||||
return Intent(
|
||||
Intent.ACTION_SENDTO,
|
||||
Uri.parse("mailto:$mailWithMessage")
|
||||
"mailto:$mailWithMessage".toUri()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ package de.markusfisch.android.binaryeye.actions.mail
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import androidx.core.net.toUri
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.actions.IntentAction
|
||||
|
||||
@@ -26,7 +26,7 @@ object MatMsgAction : IntentAction() {
|
||||
}
|
||||
return Intent(
|
||||
Intent.ACTION_SENDTO,
|
||||
Uri.parse("mailto:")
|
||||
"mailto:".toUri()
|
||||
).apply {
|
||||
mm.to?.let {
|
||||
putExtra(Intent.EXTRA_EMAIL, arrayOf(mm.to))
|
||||
|
||||
+49
-46
@@ -2,62 +2,65 @@ package de.markusfisch.android.binaryeye.actions.search
|
||||
|
||||
import android.content.Context
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.actions.IAction
|
||||
import de.markusfisch.android.binaryeye.actions.Action
|
||||
import de.markusfisch.android.binaryeye.app.alertDialog
|
||||
import de.markusfisch.android.binaryeye.app.prefs
|
||||
import de.markusfisch.android.binaryeye.content.openUrl
|
||||
import de.markusfisch.android.binaryeye.net.urlEncode
|
||||
|
||||
object OpenOrSearchAction : IAction {
|
||||
object OpenOrSearchAction : Action() {
|
||||
override val iconResId: Int = R.drawable.ic_action_search
|
||||
override val titleResId: Int = R.string.search_web
|
||||
|
||||
override fun canExecuteOn(data: ByteArray): Boolean = false
|
||||
|
||||
override suspend fun execute(context: Context, data: ByteArray) {
|
||||
view(context, String(data), true)
|
||||
}
|
||||
|
||||
private suspend fun view(
|
||||
context: Context,
|
||||
url: String,
|
||||
search: Boolean
|
||||
) {
|
||||
if (!context.openUrl(url, silent = true) && search) {
|
||||
openSearch(context, url)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
val queryUri = alertDialog<String>(context) { resume ->
|
||||
setTitle(R.string.pick_search_engine)
|
||||
setItems(names.toTypedArray()) { _, which ->
|
||||
resume(urls[which] + query.urlEncode())
|
||||
}
|
||||
} ?: return
|
||||
view(context, queryUri, false)
|
||||
fired = openUrlOrSearch(context, String(data))
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun openUrlOrSearch(
|
||||
context: Context,
|
||||
url: String,
|
||||
search: Boolean = true
|
||||
): Boolean {
|
||||
if (context.openUrl(url, silent = true)) {
|
||||
return true
|
||||
}
|
||||
if (search) {
|
||||
return prependSearchUrl(context, url)?.let {
|
||||
openUrlOrSearch(context, it, false)
|
||||
} ?: false
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private suspend fun prependSearchUrl(
|
||||
context: Context,
|
||||
query: String
|
||||
): String? {
|
||||
val defaultSearchUrl = prefs.defaultSearchUrl
|
||||
if (defaultSearchUrl.isNotEmpty()) {
|
||||
return defaultSearchUrl + query.urlEncode()
|
||||
}
|
||||
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.removeAt(0)
|
||||
urls.removeAt(0)
|
||||
if (prefs.openWithUrl.isNotEmpty()) {
|
||||
names.add(prefs.openWithUrl)
|
||||
urls.add(prefs.openWithUrl)
|
||||
}
|
||||
return alertDialog(context) { resume ->
|
||||
setTitle(R.string.pick_search_engine)
|
||||
setItems(names.toTypedArray()) { _, which ->
|
||||
resume(urls[which] + query.urlEncode())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ package de.markusfisch.android.binaryeye.actions.sms
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import androidx.core.net.toUri
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.actions.IntentAction
|
||||
|
||||
@@ -27,7 +27,7 @@ object SmsAction : IntentAction() {
|
||||
} ?: return null
|
||||
return Intent(
|
||||
Intent.ACTION_SENDTO,
|
||||
Uri.parse("smsto:$number")
|
||||
"smsto:$number".toUri()
|
||||
).apply {
|
||||
if (message.isNotEmpty()) {
|
||||
putExtra("sms_body", message)
|
||||
|
||||
+27
-30
@@ -3,7 +3,6 @@ package de.markusfisch.android.binaryeye.actions.vtype.vcard
|
||||
import android.content.ContentValues
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.provider.ContactsContract
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.actions.IntentAction
|
||||
@@ -98,38 +97,36 @@ object VCardAction : IntentAction() {
|
||||
}
|
||||
putExtra(ContactsContract.Intents.Insert.POSTAL, addr.value.locationFormat)
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
|
||||
val urls = info["URL"]?.mapNotNull {
|
||||
it.value.takeIf { url -> url.isNotBlank() }?.let { url ->
|
||||
ContentValues(2).apply {
|
||||
put(
|
||||
ContactsContract.Data.MIMETYPE,
|
||||
ContactsContract.CommonDataKinds.Website.CONTENT_ITEM_TYPE
|
||||
)
|
||||
put(ContactsContract.CommonDataKinds.Website.URL, url)
|
||||
}
|
||||
val urls = info["URL"]?.mapNotNull {
|
||||
it.value.takeIf { url -> url.isNotBlank() }?.let { url ->
|
||||
ContentValues(2).apply {
|
||||
put(
|
||||
ContactsContract.Data.MIMETYPE,
|
||||
ContactsContract.CommonDataKinds.Website.CONTENT_ITEM_TYPE
|
||||
)
|
||||
put(ContactsContract.CommonDataKinds.Website.URL, url)
|
||||
}
|
||||
}
|
||||
val bDays = info["BDAY"]?.mapNotNull {
|
||||
it.value.takeIf { day -> day.isNotBlank() }?.let { day ->
|
||||
ContentValues(3).apply {
|
||||
put(
|
||||
ContactsContract.Data.MIMETYPE,
|
||||
ContactsContract.CommonDataKinds.Event.CONTENT_ITEM_TYPE
|
||||
)
|
||||
put(
|
||||
ContactsContract.CommonDataKinds.Event.TYPE,
|
||||
ContactsContract.CommonDataKinds.Event.TYPE_BIRTHDAY
|
||||
)
|
||||
put(ContactsContract.CommonDataKinds.Event.START_DATE, day)
|
||||
}
|
||||
}
|
||||
}
|
||||
putParcelableArrayListExtra(
|
||||
ContactsContract.Intents.Insert.DATA,
|
||||
ArrayList(listOfNotNull(urls, bDays).flatten())
|
||||
)
|
||||
}
|
||||
val bDays = info["BDAY"]?.mapNotNull {
|
||||
it.value.takeIf { day -> day.isNotBlank() }?.let { day ->
|
||||
ContentValues(3).apply {
|
||||
put(
|
||||
ContactsContract.Data.MIMETYPE,
|
||||
ContactsContract.CommonDataKinds.Event.CONTENT_ITEM_TYPE
|
||||
)
|
||||
put(
|
||||
ContactsContract.CommonDataKinds.Event.TYPE,
|
||||
ContactsContract.CommonDataKinds.Event.TYPE_BIRTHDAY
|
||||
)
|
||||
put(ContactsContract.CommonDataKinds.Event.START_DATE, day)
|
||||
}
|
||||
}
|
||||
}
|
||||
putParcelableArrayListExtra(
|
||||
ContactsContract.Intents.Insert.DATA,
|
||||
ArrayList(listOfNotNull(urls, bDays).flatten())
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+3
-8
@@ -2,14 +2,13 @@ package de.markusfisch.android.binaryeye.actions.vtype.vevent
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.provider.CalendarContract
|
||||
import android.support.annotation.RequiresApi
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.actions.IntentAction
|
||||
import de.markusfisch.android.binaryeye.actions.vtype.VTypeParser
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
|
||||
object VEventAction : IntentAction() {
|
||||
override val iconResId: Int
|
||||
@@ -20,14 +19,10 @@ object VEventAction : IntentAction() {
|
||||
get() = R.string.vevent_failed
|
||||
|
||||
override fun canExecuteOn(data: ByteArray): Boolean {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
|
||||
return false
|
||||
}
|
||||
val type = VTypeParser.parseVType(String(data))
|
||||
return type == "VEVENT" || type == "VCALENDAR"
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
|
||||
override suspend fun createIntent(context: Context, data: ByteArray): Intent? {
|
||||
val info = VTypeParser.parseMap(String(data))
|
||||
|
||||
@@ -87,6 +82,6 @@ private fun List<String>.simpleFindParse(date: String): Date? {
|
||||
|
||||
private fun SimpleDateFormat.simpleParse(date: String): Date? = try {
|
||||
parse(date)
|
||||
} catch (e: Exception) {
|
||||
} catch (_: Exception) {
|
||||
null
|
||||
}
|
||||
|
||||
@@ -2,12 +2,12 @@ package de.markusfisch.android.binaryeye.actions.web
|
||||
|
||||
import android.content.Context
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.actions.IAction
|
||||
import de.markusfisch.android.binaryeye.actions.Action
|
||||
import de.markusfisch.android.binaryeye.content.openUrl
|
||||
|
||||
object WebAction : IAction {
|
||||
object WebAction : Action() {
|
||||
private val colloquialRegex =
|
||||
"^(http[s]*://)*[A-Za-z0-9]{3,}\\.[A-Za-z]{2,}[^ \t\r\n]*$".toRegex()
|
||||
"^(URL:[ ]*)*(http[s]*://)*[A-Za-z0-9-]{3,}\\.[A-Za-z]{2,}[^ \t\r\n]*$".toRegex()
|
||||
|
||||
override val iconResId: Int = R.drawable.ic_action_open
|
||||
override val titleResId: Int = R.string.open_url
|
||||
@@ -18,9 +18,12 @@ object WebAction : IAction {
|
||||
|
||||
override suspend fun execute(context: Context, data: ByteArray) {
|
||||
var url = String(data).trim()
|
||||
if (url.startsWith("URL:")) {
|
||||
url = url.substring(4).trim()
|
||||
}
|
||||
if (!url.startsWith("http") && !url.startsWith("ftp")) {
|
||||
url = "http://${url}"
|
||||
}
|
||||
context.openUrl(url)
|
||||
fired = context.openUrl(url)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,31 +2,39 @@ package de.markusfisch.android.binaryeye.actions.wifi
|
||||
|
||||
import android.content.Context
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.actions.IAction
|
||||
import de.markusfisch.android.binaryeye.actions.Action
|
||||
import de.markusfisch.android.binaryeye.widget.toast
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
object WifiAction : IAction {
|
||||
object WifiAction : Action() {
|
||||
override val iconResId = R.drawable.ic_action_wifi
|
||||
override val titleResId = R.string.connect_to_wifi
|
||||
|
||||
var password: String? = null
|
||||
|
||||
override fun canExecuteOn(data: ByteArray): Boolean =
|
||||
WifiConnector.parse(String(data)) {
|
||||
password = it
|
||||
} != null
|
||||
override fun canExecuteOn(data: ByteArray): Boolean {
|
||||
// Just check if the data can be parsed as the app should also
|
||||
// show invalid configurations so users can still access them.
|
||||
val inputMap = WifiConnector.parseMap(String(data)) ?: return false
|
||||
password = inputMap["P"]
|
||||
return !inputMap["S"].isNullOrEmpty()
|
||||
}
|
||||
|
||||
override suspend fun execute(
|
||||
context: Context,
|
||||
data: ByteArray
|
||||
) = withContext(Dispatchers.IO) {
|
||||
val message = WifiConnector.parse(String(data))?.let {
|
||||
WifiConnector.addNetwork(context, it)
|
||||
} ?: R.string.wifi_config_failed
|
||||
withContext(Dispatchers.Main) {
|
||||
context.toast(message)
|
||||
override suspend fun execute(context: Context, data: ByteArray) {
|
||||
withContext(Dispatchers.IO) {
|
||||
fired = WifiConnector.parse(String(data))?.let {
|
||||
WifiConnector.addNetwork(context, it)
|
||||
} ?: false
|
||||
withContext(Dispatchers.Main) {
|
||||
context.toast(
|
||||
if (fired) {
|
||||
R.string.wifi_added
|
||||
} else {
|
||||
R.string.wifi_config_failed
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+61
-53
@@ -1,14 +1,15 @@
|
||||
package de.markusfisch.android.binaryeye.actions.wifi
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.wifi.WifiConfiguration
|
||||
import android.net.wifi.WifiEnterpriseConfig
|
||||
import android.net.wifi.WifiManager
|
||||
import android.net.wifi.WifiNetworkSuggestion
|
||||
import android.os.Build
|
||||
import android.support.annotation.RequiresApi
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import java.util.*
|
||||
import android.provider.Settings
|
||||
import androidx.annotation.RequiresApi
|
||||
import java.util.Locale
|
||||
|
||||
/**
|
||||
* Normal:
|
||||
@@ -35,15 +36,9 @@ import java.util.*
|
||||
* The fields can appear in any order. Only "S:" is required.
|
||||
*/
|
||||
object WifiConnector {
|
||||
fun parse(
|
||||
input: String,
|
||||
passwordBlock: ((password: String?) -> Unit)? = null
|
||||
): Any? {
|
||||
fun parse(input: String): Any? {
|
||||
val inputMap = parseMap(input) ?: return null
|
||||
val parsedData = SimpleDataAccessor.of(inputMap) ?: return null
|
||||
passwordBlock?.apply {
|
||||
invoke(parsedData.password)
|
||||
}
|
||||
return try {
|
||||
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q) {
|
||||
// WifiConfiguration is deprecated in Android Q but
|
||||
@@ -54,7 +49,7 @@ object WifiConnector {
|
||||
} else {
|
||||
WifiNetworkSuggestion.Builder().apply(parsedData)
|
||||
}
|
||||
} catch (e: IllegalArgumentException) {
|
||||
} catch (_: IllegalArgumentException) {
|
||||
// Some arguments may be out of range, e.g. SSIDs cannot
|
||||
// be longer than 32 characters in some versions of Android.
|
||||
// See: https://github.com/markusfisch/BinaryEye/issues/402
|
||||
@@ -62,24 +57,26 @@ object WifiConnector {
|
||||
}
|
||||
}
|
||||
|
||||
fun addNetwork(context: Context, config: Any): Int {
|
||||
fun addNetwork(context: Context, config: Any): Boolean {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R &&
|
||||
config is WifiNetworkSuggestion.Builder
|
||||
) {
|
||||
context.suggestNetworkFromBuilder(config)
|
||||
return true
|
||||
}
|
||||
|
||||
val wifiManager = context.applicationContext.getSystemService(
|
||||
Context.WIFI_SERVICE
|
||||
) as WifiManager
|
||||
// WifiConfiguration is deprecated in Android Q.
|
||||
@Suppress("DEPRECATION")
|
||||
return if ((config is WifiConfiguration &&
|
||||
wifiManager.enableWifi() &&
|
||||
wifiManager.removeOldNetwork(config) &&
|
||||
wifiManager.enableNewNetwork(config)) ||
|
||||
(Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q &&
|
||||
config is WifiNetworkSuggestion.Builder &&
|
||||
wifiManager.addNetworkFromBuilder(config))
|
||||
) {
|
||||
R.string.wifi_added
|
||||
} else {
|
||||
R.string.wifi_config_failed
|
||||
}
|
||||
return (config is WifiConfiguration &&
|
||||
wifiManager.enableWifi() &&
|
||||
wifiManager.removeOldNetwork(config) &&
|
||||
wifiManager.enableNewNetwork(config)) ||
|
||||
(Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q &&
|
||||
config is WifiNetworkSuggestion.Builder &&
|
||||
wifiManager.addNetworkFromBuilder(config))
|
||||
}
|
||||
|
||||
internal fun parseMap(input: String): Map<String, String>? {
|
||||
@@ -93,7 +90,7 @@ object WifiConnector {
|
||||
input.trimAndTerminate()
|
||||
)?.groupValues?.get(1)?.let { pairs ->
|
||||
pairRegex.findAll(pairs).map { pair ->
|
||||
pair.groupValues[1].uppercase(Locale.US) to pair.groupValues[2]
|
||||
pair.groupValues[1].uppercase(Locale.US) to pair.groupValues[2].unescape
|
||||
}.toMap()
|
||||
}
|
||||
}
|
||||
@@ -103,47 +100,43 @@ object WifiConnector {
|
||||
) {
|
||||
// This should be private but because of testing it isn't possible.
|
||||
internal val ssid: String
|
||||
get() = inputMap.getValue("S").unescape
|
||||
get() = inputMap.getValue("S")
|
||||
internal val securityType: String
|
||||
get() = inputMap["T"]?.unescape ?: ""
|
||||
get() = inputMap["T"] ?: ""
|
||||
internal val password: String?
|
||||
get() = inputMap["P"]?.unescape
|
||||
get() = inputMap["P"]
|
||||
internal val hidden: Boolean
|
||||
get() = inputMap["H"]?.unescape == "true"
|
||||
get() = inputMap["H"] == "true"
|
||||
internal val anonymousIdentity: String
|
||||
get() = inputMap["AI"]?.unescape ?: inputMap["A"]?.unescape ?: ""
|
||||
get() = inputMap["AI"] ?: inputMap["A"] ?: ""
|
||||
internal val identity: String
|
||||
get() = inputMap["I"]?.unescape ?: ""
|
||||
get() = inputMap["I"] ?: ""
|
||||
internal val eapMethod: Int?
|
||||
get() = if (inputMap["E"].isNullOrEmpty()) {
|
||||
requireSdk(Build.VERSION_CODES.JELLY_BEAN_MR2) {
|
||||
WifiEnterpriseConfig.Eap.NONE
|
||||
}
|
||||
WifiEnterpriseConfig.Eap.NONE
|
||||
} else when (inputMap["E"]?.uppercase()) {
|
||||
"AKA" -> requireSdk(Build.VERSION_CODES.LOLLIPOP) { WifiEnterpriseConfig.Eap.AKA }
|
||||
"AKA" -> WifiEnterpriseConfig.Eap.AKA
|
||||
"AKA_PRIME" -> requireSdk(Build.VERSION_CODES.M) { WifiEnterpriseConfig.Eap.AKA_PRIME }
|
||||
"NONE" -> requireSdk(Build.VERSION_CODES.JELLY_BEAN_MR2) { WifiEnterpriseConfig.Eap.NONE }
|
||||
"PEAP" -> requireSdk(Build.VERSION_CODES.JELLY_BEAN_MR2) { WifiEnterpriseConfig.Eap.PEAP }
|
||||
"PWD" -> requireSdk(Build.VERSION_CODES.JELLY_BEAN_MR2) { WifiEnterpriseConfig.Eap.PWD }
|
||||
"SIM" -> requireSdk(Build.VERSION_CODES.LOLLIPOP) { WifiEnterpriseConfig.Eap.SIM }
|
||||
"TLS" -> requireSdk(Build.VERSION_CODES.JELLY_BEAN_MR2) { WifiEnterpriseConfig.Eap.TLS }
|
||||
"TTLS" -> requireSdk(Build.VERSION_CODES.JELLY_BEAN_MR2) { WifiEnterpriseConfig.Eap.TTLS }
|
||||
"NONE" -> WifiEnterpriseConfig.Eap.NONE
|
||||
"PEAP" -> WifiEnterpriseConfig.Eap.PEAP
|
||||
"PWD" -> WifiEnterpriseConfig.Eap.PWD
|
||||
"SIM" -> WifiEnterpriseConfig.Eap.SIM
|
||||
"TLS" -> WifiEnterpriseConfig.Eap.TLS
|
||||
"TTLS" -> WifiEnterpriseConfig.Eap.TTLS
|
||||
"UNAUTH_TLS" -> requireSdk(Build.VERSION_CODES.N) { WifiEnterpriseConfig.Eap.UNAUTH_TLS }
|
||||
else -> null
|
||||
}
|
||||
internal val phase2Method: Int?
|
||||
get() = if (inputMap["PH2"].isNullOrEmpty()) {
|
||||
requireSdk(Build.VERSION_CODES.JELLY_BEAN_MR2) {
|
||||
WifiEnterpriseConfig.Phase2.NONE
|
||||
}
|
||||
WifiEnterpriseConfig.Phase2.NONE
|
||||
} else when (inputMap["PH2"]?.uppercase()) {
|
||||
"AKA" -> requireSdk(Build.VERSION_CODES.O) { WifiEnterpriseConfig.Phase2.AKA }
|
||||
"AKA_PRIME" -> requireSdk(Build.VERSION_CODES.O) { WifiEnterpriseConfig.Phase2.AKA_PRIME }
|
||||
"GTC" -> requireSdk(Build.VERSION_CODES.JELLY_BEAN_MR2) { WifiEnterpriseConfig.Phase2.GTC }
|
||||
"MSCHAP" -> requireSdk(Build.VERSION_CODES.JELLY_BEAN_MR2) { WifiEnterpriseConfig.Phase2.MSCHAP }
|
||||
"MSCHAPV2" -> requireSdk(Build.VERSION_CODES.JELLY_BEAN_MR2) { WifiEnterpriseConfig.Phase2.MSCHAPV2 }
|
||||
"NONE" -> requireSdk(Build.VERSION_CODES.JELLY_BEAN_MR2) { WifiEnterpriseConfig.Phase2.NONE }
|
||||
"PAP" -> requireSdk(Build.VERSION_CODES.JELLY_BEAN_MR2) { WifiEnterpriseConfig.Phase2.PAP }
|
||||
"GTC" -> WifiEnterpriseConfig.Phase2.GTC
|
||||
"MSCHAP" -> WifiEnterpriseConfig.Phase2.MSCHAP
|
||||
"MSCHAPV2" -> WifiEnterpriseConfig.Phase2.MSCHAPV2
|
||||
"NONE" -> WifiEnterpriseConfig.Phase2.NONE
|
||||
"PAP" -> WifiEnterpriseConfig.Phase2.PAP
|
||||
"SIM" -> requireSdk(Build.VERSION_CODES.O) { WifiEnterpriseConfig.Phase2.SIM }
|
||||
else -> null
|
||||
}
|
||||
@@ -201,7 +194,7 @@ object WifiConnector {
|
||||
})
|
||||
}
|
||||
}
|
||||
} catch (e: IllegalArgumentException) {
|
||||
} catch (_: IllegalArgumentException) {
|
||||
// Don't accept passphrases that aren't ASCII encodeable.
|
||||
return null
|
||||
}
|
||||
@@ -268,7 +261,7 @@ object WifiConnector {
|
||||
allowedGroupCiphers.set(WifiConfiguration.GroupCipher.CCMP)
|
||||
}
|
||||
|
||||
"WPA2-EAP" -> requireSdk(Build.VERSION_CODES.JELLY_BEAN_MR2) {
|
||||
"WPA2-EAP" -> {
|
||||
passwordWithQuotes?.let {
|
||||
preSharedKey = it
|
||||
} ?: return null
|
||||
@@ -288,7 +281,7 @@ object WifiConnector {
|
||||
data.eapMethod ?: return null // non valid eapMethod
|
||||
enterpriseConfig.phase2Method =
|
||||
data.phase2Method ?: return null // non valid phase2Method
|
||||
} ?: return null // api isn't high enough
|
||||
}
|
||||
}
|
||||
return this
|
||||
}
|
||||
@@ -313,6 +306,21 @@ private fun WifiManager.addNetworkFromBuilder(
|
||||
return result == WifiManager.STATUS_NETWORK_SUGGESTIONS_SUCCESS
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.R)
|
||||
private fun Context.suggestNetworkFromBuilder(
|
||||
builder: WifiNetworkSuggestion.Builder
|
||||
) {
|
||||
val suggestion = builder.build()
|
||||
startActivity(
|
||||
Intent(Settings.ACTION_WIFI_ADD_NETWORKS).apply {
|
||||
putParcelableArrayListExtra(
|
||||
Settings.EXTRA_WIFI_NETWORK_LIST,
|
||||
arrayListOf(suggestion)
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// Make sure the string ends with a semicolon.
|
||||
private fun String.trimAndTerminate(): String {
|
||||
var trimmed = trim()
|
||||
@@ -366,7 +374,7 @@ private fun WifiManager.removeOldNetwork(
|
||||
}?.networkId?.also {
|
||||
removeNetwork(it)
|
||||
}
|
||||
} catch (e: SecurityException) {
|
||||
} catch (_: SecurityException) {
|
||||
// The user didn't allow ACCESS_FINE_LOCATION which is
|
||||
// required to access configuredNetworks and that's fine.
|
||||
}
|
||||
|
||||
+271
@@ -0,0 +1,271 @@
|
||||
package de.markusfisch.android.binaryeye.activity
|
||||
|
||||
import android.app.AlertDialog
|
||||
import android.app.Dialog
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.EditText
|
||||
import android.widget.ListView
|
||||
import android.widget.RadioGroup
|
||||
import android.widget.TextView
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.app.prefs
|
||||
import de.markusfisch.android.binaryeye.automation.AutomatedAction
|
||||
import de.markusfisch.android.binaryeye.view.setPaddingFromWindowInsets
|
||||
import de.markusfisch.android.binaryeye.view.systemBarListViewScrollListener
|
||||
|
||||
class AutomatedActionsActivity : ScreenActivity() {
|
||||
private val actions = ArrayList<AutomatedAction>()
|
||||
|
||||
private lateinit var adapter: ActionsAdapter
|
||||
|
||||
override fun onCreate(state: Bundle?) {
|
||||
super.onCreate(state)
|
||||
setTitle(R.string.automated_actions)
|
||||
val frame = findViewById(R.id.content_frame) as ViewGroup
|
||||
val view = layoutInflater.inflate(
|
||||
R.layout.fragment_automated_actions,
|
||||
frame,
|
||||
false
|
||||
)
|
||||
frame.addView(view)
|
||||
|
||||
actions.clear()
|
||||
actions.addAll(prefs.automatedActions)
|
||||
|
||||
val listView = view.findViewById<ListView>(R.id.actions)
|
||||
listView.emptyView = view.findViewById(R.id.no_actions)
|
||||
adapter = ActionsAdapter(this, actions)
|
||||
listView.adapter = adapter
|
||||
listView.setOnScrollListener(systemBarListViewScrollListener)
|
||||
listView.setOnItemClickListener { _, _, position, _ ->
|
||||
editAction(position)
|
||||
}
|
||||
listView.setOnItemLongClickListener { _, _, position, _ ->
|
||||
confirmRemoveAction(position)
|
||||
true
|
||||
}
|
||||
|
||||
view.findViewById<View>(R.id.add).setOnClickListener {
|
||||
editAction(null)
|
||||
}
|
||||
|
||||
view.findViewById<View>(R.id.inset_layout).setPaddingFromWindowInsets()
|
||||
listView.setPaddingFromWindowInsets()
|
||||
}
|
||||
|
||||
private fun editAction(position: Int?) {
|
||||
val action = position?.let { actions[it] }
|
||||
val view = layoutInflater.inflate(
|
||||
R.layout.dialog_automated_action,
|
||||
null
|
||||
)
|
||||
val regexView = view.findViewById<EditText>(R.id.regex)
|
||||
val urlView = view.findViewById<EditText>(R.id.url_template)
|
||||
val typeGroup = view.findViewById<RadioGroup>(R.id.action_type)
|
||||
val urlSection = view.findViewById<View>(R.id.url_section)
|
||||
val intentSection = view.findViewById<View>(R.id.intent_section)
|
||||
val intentUriView = view.findViewById<EditText>(
|
||||
R.id.intent_uri_template
|
||||
)
|
||||
|
||||
if (action != null) {
|
||||
regexView.setText(action.pattern)
|
||||
when (action.type) {
|
||||
AutomatedAction.Type.Intent -> {
|
||||
urlView.setText(action.urlTemplate ?: "")
|
||||
}
|
||||
|
||||
AutomatedAction.Type.CustomIntent -> {
|
||||
intentUriView.setText(action.intentUriTemplate ?: "")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun updateTypeSections(checkedId: Int) {
|
||||
val isIntent = checkedId == R.id.action_type_intent
|
||||
urlSection.visibility = if (isIntent) View.VISIBLE else View.GONE
|
||||
intentSection.visibility = if (isIntent) View.GONE else View.VISIBLE
|
||||
}
|
||||
|
||||
typeGroup.setOnCheckedChangeListener { _, checkedId ->
|
||||
updateTypeSections(checkedId)
|
||||
}
|
||||
val initialTypeId = if (
|
||||
action?.type == AutomatedAction.Type.CustomIntent
|
||||
) {
|
||||
R.id.action_type_custom_intent
|
||||
} else {
|
||||
R.id.action_type_intent
|
||||
}
|
||||
typeGroup.check(initialTypeId)
|
||||
updateTypeSections(initialTypeId)
|
||||
|
||||
val dialog = AlertDialog.Builder(this)
|
||||
.setTitle(
|
||||
if (action == null) {
|
||||
R.string.automated_action_add
|
||||
} else {
|
||||
R.string.automated_action_edit
|
||||
}
|
||||
)
|
||||
.setView(view)
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
||||
.create()
|
||||
|
||||
dialog.setOnShowListener {
|
||||
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener {
|
||||
addOrUpdateAction(
|
||||
position,
|
||||
regexView,
|
||||
urlView,
|
||||
typeGroup,
|
||||
intentUriView,
|
||||
dialog
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
dialog.show()
|
||||
}
|
||||
|
||||
private fun addOrUpdateAction(
|
||||
position: Int?,
|
||||
regexView: EditText,
|
||||
urlView: EditText,
|
||||
typeGroup: RadioGroup,
|
||||
intentUriView: EditText,
|
||||
dialog: Dialog,
|
||||
) {
|
||||
val pattern = regexView.text.toString().trim()
|
||||
if (pattern.isEmpty()) {
|
||||
regexView.error = getString(R.string.automated_action_regex_required)
|
||||
return
|
||||
}
|
||||
try {
|
||||
Regex(pattern)
|
||||
} catch (_: Exception) {
|
||||
regexView.error = getString(R.string.automated_action_regex_invalid)
|
||||
return
|
||||
}
|
||||
val isIntent = typeGroup.checkedRadioButtonId == R.id.action_type_intent
|
||||
val newAction = if (isIntent) {
|
||||
val urlTemplate = urlView.text.toString().trim()
|
||||
if (urlTemplate.isEmpty()) {
|
||||
urlView.error = getString(R.string.automated_action_url_required)
|
||||
return
|
||||
}
|
||||
AutomatedAction(
|
||||
pattern = pattern,
|
||||
type = AutomatedAction.Type.Intent,
|
||||
urlTemplate = urlTemplate
|
||||
)
|
||||
} else {
|
||||
val intentUriTemplate = intentUriView.text.toString().trim()
|
||||
if (intentUriTemplate.isEmpty()) {
|
||||
intentUriView.error = getString(
|
||||
R.string.automated_action_intent_uri_required
|
||||
)
|
||||
return
|
||||
}
|
||||
if (!intentUriTemplate.startsWith(
|
||||
"intent:",
|
||||
ignoreCase = true
|
||||
)
|
||||
) {
|
||||
intentUriView.error = getString(
|
||||
R.string.automated_action_intent_uri_invalid
|
||||
)
|
||||
return
|
||||
}
|
||||
AutomatedAction(
|
||||
pattern = pattern,
|
||||
type = AutomatedAction.Type.CustomIntent,
|
||||
intentUriTemplate = intentUriTemplate
|
||||
)
|
||||
}
|
||||
if (position == null) {
|
||||
actions.add(newAction)
|
||||
} else {
|
||||
actions[position] = newAction
|
||||
}
|
||||
prefs.setAutomatedActions(actions)
|
||||
adapter.notifyDataSetChanged()
|
||||
dialog.dismiss()
|
||||
}
|
||||
|
||||
private fun confirmRemoveAction(position: Int) {
|
||||
AlertDialog.Builder(this)
|
||||
.setMessage(R.string.automated_action_remove_confirm)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
actions.removeAt(position)
|
||||
prefs.setAutomatedActions(actions)
|
||||
adapter.notifyDataSetChanged()
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
||||
.show()
|
||||
}
|
||||
|
||||
private class ActionsAdapter(
|
||||
context: Context,
|
||||
private val items: List<AutomatedAction>
|
||||
) : ArrayAdapter<AutomatedAction>(
|
||||
context,
|
||||
android.R.layout.simple_list_item_2,
|
||||
android.R.id.text1,
|
||||
items
|
||||
) {
|
||||
override fun getView(
|
||||
position: Int,
|
||||
convertView: View?,
|
||||
parent: ViewGroup
|
||||
): View {
|
||||
val view = super.getView(position, convertView, parent)
|
||||
val action = items[position]
|
||||
getViewHolder(view).apply {
|
||||
title.text = action.pattern
|
||||
subtitle.text = when (action.type) {
|
||||
AutomatedAction.Type.Intent -> if (!action.urlTemplate.isNullOrEmpty()) {
|
||||
context.getString(
|
||||
R.string.automated_action_list_intent,
|
||||
action.urlTemplate
|
||||
)
|
||||
} else {
|
||||
context.getString(R.string.automated_action_list_intent_empty)
|
||||
}
|
||||
|
||||
AutomatedAction.Type.CustomIntent ->
|
||||
if (!action.intentUriTemplate.isNullOrEmpty()) {
|
||||
context.getString(
|
||||
R.string.automated_action_list_custom_intent,
|
||||
action.intentUriTemplate
|
||||
)
|
||||
} else {
|
||||
context.getString(
|
||||
R.string.automated_action_list_custom_intent_empty
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
return view
|
||||
}
|
||||
|
||||
private fun getViewHolder(
|
||||
view: View
|
||||
): ViewHolder = view.tag as ViewHolder? ?: ViewHolder(
|
||||
view.findViewById(android.R.id.text1),
|
||||
view.findViewById(android.R.id.text2),
|
||||
).also {
|
||||
view.tag = it
|
||||
}
|
||||
|
||||
private data class ViewHolder(
|
||||
val title: TextView,
|
||||
val subtitle: TextView,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,598 @@
|
||||
package de.markusfisch.android.binaryeye.activity
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.AlertDialog
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Matrix
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.EditText
|
||||
import android.widget.ImageView
|
||||
import android.widget.SeekBar
|
||||
import android.widget.TextView
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.app.db
|
||||
import de.markusfisch.android.binaryeye.app.hasWritePermission
|
||||
import de.markusfisch.android.binaryeye.app.prefs
|
||||
import de.markusfisch.android.binaryeye.content.Barcode
|
||||
import de.markusfisch.android.binaryeye.content.BarcodeColors
|
||||
import de.markusfisch.android.binaryeye.content.BitMatrixBarcode
|
||||
import de.markusfisch.android.binaryeye.content.ContentBarcode
|
||||
import de.markusfisch.android.binaryeye.content.copyToClipboard
|
||||
import de.markusfisch.android.binaryeye.content.shareFile
|
||||
import de.markusfisch.android.binaryeye.content.shareText
|
||||
import de.markusfisch.android.binaryeye.database.toScan
|
||||
import de.markusfisch.android.binaryeye.io.addSuffixIfNotGiven
|
||||
import de.markusfisch.android.binaryeye.io.toSaveResult
|
||||
import de.markusfisch.android.binaryeye.io.writeExternalFile
|
||||
import de.markusfisch.android.binaryeye.net.createEncodeDeeplink
|
||||
import de.markusfisch.android.binaryeye.os.getScreenBrightness
|
||||
import de.markusfisch.android.binaryeye.os.setScreenBrightness
|
||||
import de.markusfisch.android.binaryeye.view.doOnApplyWindowInsets
|
||||
import de.markusfisch.android.binaryeye.view.setPaddingFromWindowInsets
|
||||
import de.markusfisch.android.binaryeye.widget.ConfinedScalingImageView
|
||||
import de.markusfisch.android.binaryeye.widget.toast
|
||||
import de.markusfisch.android.zxingcpp.ZxingCpp
|
||||
import de.markusfisch.android.zxingcpp.ZxingCpp.BarcodeFormat
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
import java.io.OutputStream
|
||||
import java.util.Locale
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
|
||||
class BarcodeActivity : ScreenActivity() {
|
||||
private enum class FileType {
|
||||
PNG, JPG, SVG, TXT
|
||||
}
|
||||
|
||||
private val parentJob = Job()
|
||||
private val scope = CoroutineScope(Dispatchers.IO + parentJob)
|
||||
|
||||
private lateinit var imageView: ConfinedScalingImageView
|
||||
private lateinit var barcode: Barcode<*>
|
||||
|
||||
private var contentView: View? = null
|
||||
private var addToHistoryItem: MenuItem? = null
|
||||
private var brightenScreenItem: MenuItem? = null
|
||||
private var currentBrightness = -1f
|
||||
|
||||
override fun onCreate(state: Bundle?) {
|
||||
super.onCreate(state)
|
||||
setTitle(R.string.view_barcode)
|
||||
val frame = findViewById(R.id.content_frame) as ViewGroup
|
||||
val view = layoutInflater.inflate(
|
||||
R.layout.fragment_barcode,
|
||||
frame,
|
||||
false
|
||||
)
|
||||
frame.addView(view)
|
||||
contentView = view
|
||||
|
||||
// Make `imageView` available for `onPause()`.
|
||||
imageView = view.findViewById(R.id.barcode)
|
||||
|
||||
val bitmap: Bitmap
|
||||
try {
|
||||
barcode = intent?.extras?.toBarcode() ?: throw IllegalArgumentException(
|
||||
"Illegal arguments"
|
||||
)
|
||||
// Catch exceptions from encoding.
|
||||
bitmap = barcode.bitmap()
|
||||
} catch (e: Exception) {
|
||||
var message = e.message
|
||||
if (message.isNullOrEmpty()) {
|
||||
message = getString(R.string.error_encoding_barcode)
|
||||
}
|
||||
toast(message)
|
||||
finish()
|
||||
return
|
||||
}
|
||||
|
||||
imageView.setImageBitmap(bitmap)
|
||||
imageView.runAfterLayout = {
|
||||
val scale = prefs.previewScale
|
||||
if (scale > 0) {
|
||||
imageView.setScale(
|
||||
scale,
|
||||
bitmap.width / 2f,
|
||||
bitmap.height / 2f
|
||||
)
|
||||
}
|
||||
|
||||
imageView.minWidth = min(
|
||||
imageView.minWidth / 2f,
|
||||
max(bitmap.width, bitmap.height).toFloat()
|
||||
)
|
||||
}
|
||||
|
||||
view.findViewById<View>(R.id.share).setOnClickListener {
|
||||
it.context?.apply {
|
||||
pickFileType(R.string.share_as) { fileType ->
|
||||
shareAs(fileType)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
view.findViewById<View>(R.id.inset_layout).setPaddingFromWindowInsets()
|
||||
imageView.doOnApplyWindowInsets { _, insets ->
|
||||
imageView.insets.set(insets)
|
||||
}
|
||||
}
|
||||
|
||||
private fun Bundle.toBarcode(): Barcode<*> {
|
||||
val content =
|
||||
getString(CONTENT_TEXT) ?: getByteArray(CONTENT_RAW) ?: throw IllegalArgumentException(
|
||||
"content cannot be null"
|
||||
)
|
||||
val barcodeFormat = BarcodeFormat.valueOf(
|
||||
getString(FORMAT) ?: throw IllegalArgumentException(
|
||||
"format cannot be null"
|
||||
)
|
||||
)
|
||||
val colors = BarcodeColors.entries[getInt(COLORS)]
|
||||
val data = getByteArray(BIT_MATRIX_DATA)
|
||||
return if (data != null) {
|
||||
BitMatrixBarcode(
|
||||
ZxingCpp.BitMatrix(
|
||||
getInt(BIT_MATRIX_WIDTH),
|
||||
getInt(BIT_MATRIX_HEIGHT),
|
||||
data
|
||||
),
|
||||
content,
|
||||
barcodeFormat,
|
||||
colors
|
||||
)
|
||||
} else {
|
||||
ContentBarcode(
|
||||
content,
|
||||
barcodeFormat,
|
||||
getInt(EC_LEVEL),
|
||||
getInt(MARGIN),
|
||||
colors
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
if (prefs.brightenScreen) {
|
||||
// Post to make sure brightenScreenItem is initialized.
|
||||
contentView?.postDelayed({
|
||||
brightenScreen()
|
||||
}, 500)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
if (currentBrightness > -1f) {
|
||||
restoreScreenBrightness()
|
||||
}
|
||||
if (imageView.isInBounds()) {
|
||||
prefs.previewScale = imageView.getScale()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
parentJob.cancel()
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
inflater.inflate(R.menu.fragment_barcode, menu)
|
||||
addToHistoryItem = menu.findItem(R.id.add_to_history)
|
||||
brightenScreenItem = menu.findItem(R.id.brighten_screen)
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
return when (item.itemId) {
|
||||
R.id.add_to_history -> {
|
||||
readAndAddToHistory(
|
||||
barcode.bitmap(),
|
||||
barcode.format
|
||||
)
|
||||
addToHistoryItem?.isVisible = false
|
||||
toast(R.string.added_to_history)
|
||||
true
|
||||
}
|
||||
|
||||
R.id.copy_to_clipboard -> {
|
||||
copyToClipboard(barcode.text())
|
||||
true
|
||||
}
|
||||
|
||||
R.id.copy_as_deeplink -> {
|
||||
copyToClipboard(deeplinkToCopy())
|
||||
true
|
||||
}
|
||||
|
||||
R.id.export_to_file -> {
|
||||
pickFileType(R.string.export_as) { fileType ->
|
||||
when (fileType) {
|
||||
FileType.PNG,
|
||||
FileType.JPG -> askForSize { size ->
|
||||
askForFileNameAndSave(fileType, size)
|
||||
}
|
||||
|
||||
else -> askForFileNameAndSave(fileType, 0)
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
R.id.brighten_screen -> {
|
||||
prefs.brightenScreen = if (currentBrightness > -1f) {
|
||||
restoreScreenBrightness()
|
||||
false
|
||||
} else {
|
||||
brightenScreen()
|
||||
true
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
}
|
||||
|
||||
private fun copyToClipboard(text: String, isSensitive: Boolean = false) {
|
||||
(this as android.content.Context).copyToClipboard(text, isSensitive)
|
||||
toast(R.string.copied_to_clipboard)
|
||||
}
|
||||
|
||||
private fun deeplinkToCopy() = createEncodeDeeplink(
|
||||
format = barcode.format.name,
|
||||
content = barcode.textOrHex(),
|
||||
)
|
||||
|
||||
private fun Context.pickFileType(
|
||||
title: Int,
|
||||
action: (FileType) -> Unit
|
||||
) {
|
||||
val fileTypes = FileType.entries.toTypedArray()
|
||||
AlertDialog.Builder(this)
|
||||
.setTitle(title)
|
||||
.setItems(fileTypes.map { it.name }.toTypedArray()) { _, which ->
|
||||
action(fileTypes[which])
|
||||
}
|
||||
.show()
|
||||
}
|
||||
|
||||
// Dialogs do not have a parent view.
|
||||
@SuppressLint("InflateParams")
|
||||
private fun askForFileNameAndSave(fileType: FileType, bitmapSize: Int) {
|
||||
// Write permission is only required before Android Q.
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q &&
|
||||
!hasWritePermission {
|
||||
askForFileNameAndSave(fileType, bitmapSize)
|
||||
}
|
||||
) {
|
||||
return
|
||||
}
|
||||
val view = layoutInflater.inflate(R.layout.dialog_save_file, null)
|
||||
val editText = view.findViewById<EditText>(R.id.file_name)
|
||||
editText.setText(
|
||||
encodeFileName("${barcode.format}_${barcode.content}")
|
||||
)
|
||||
AlertDialog.Builder(this)
|
||||
.setView(view)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
val fileName = editText.text.toString()
|
||||
when (fileType) {
|
||||
FileType.PNG -> saveAs(
|
||||
addSuffixIfNotGiven(fileName, ".png"),
|
||||
MIME_PNG
|
||||
) {
|
||||
barcode.bitmap(bitmapSize).saveAsPng(it)
|
||||
}
|
||||
|
||||
FileType.JPG -> saveAs(
|
||||
addSuffixIfNotGiven(fileName, ".jpg"),
|
||||
MIME_JPG
|
||||
) {
|
||||
barcode.bitmap(bitmapSize).saveAsJpg(it)
|
||||
}
|
||||
|
||||
FileType.SVG -> saveAs(
|
||||
addSuffixIfNotGiven(fileName, ".svg"),
|
||||
MIME_SVG
|
||||
) {
|
||||
it.write(barcode.svg().toByteArray())
|
||||
}
|
||||
|
||||
FileType.TXT -> saveAs(
|
||||
addSuffixIfNotGiven(fileName, ".txt"),
|
||||
MIME_TXT
|
||||
) {
|
||||
it.write(barcode.text().toByteArray())
|
||||
}
|
||||
}
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||
}
|
||||
.show()
|
||||
}
|
||||
|
||||
// Dialogs do not have a parent view.
|
||||
@SuppressLint("InflateParams")
|
||||
private fun askForSize(write: (size: Int) -> Unit) {
|
||||
val view = layoutInflater.inflate(R.layout.dialog_size, null)
|
||||
val sizeView = view.findViewById<TextView>(R.id.size_display)
|
||||
val sizeBarView = view.findViewById<SeekBar>(R.id.size_bar)
|
||||
sizeBarView.initSizeBar(sizeView)
|
||||
AlertDialog.Builder(this)
|
||||
.setView(view)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
write(getSize(sizeBarView.progress))
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||
}
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun SeekBar.initSizeBar(sizeView: TextView) {
|
||||
sizeView.updateSize(progress)
|
||||
setOnSeekBarChangeListener(
|
||||
object : SeekBar.OnSeekBarChangeListener {
|
||||
override fun onProgressChanged(
|
||||
seekBar: SeekBar,
|
||||
progressValue: Int,
|
||||
fromUser: Boolean
|
||||
) {
|
||||
sizeView.updateSize(progressValue)
|
||||
}
|
||||
|
||||
override fun onStartTrackingTouch(seekBar: SeekBar) {}
|
||||
|
||||
override fun onStopTrackingTouch(seekBar: SeekBar) {}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun TextView.updateSize(power: Int) {
|
||||
val size = getSize(power)
|
||||
text = if (size > 0) {
|
||||
getString(R.string.size_width_by_height, size, size)
|
||||
} else {
|
||||
getString(R.string.size_no_magnification)
|
||||
}
|
||||
}
|
||||
|
||||
private fun saveAs(
|
||||
fileName: String,
|
||||
mimeType: String,
|
||||
write: (outputStream: OutputStream) -> Unit
|
||||
) {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
val message = writeExternalFile(
|
||||
fileName, mimeType, write
|
||||
).toSaveResult()
|
||||
withContext(Dispatchers.Main) {
|
||||
toast(message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun Context.shareAs(fileType: FileType) {
|
||||
when (fileType) {
|
||||
FileType.PNG -> askForSize {
|
||||
share(barcode.bitmap(it), MIME_PNG, "png")
|
||||
}
|
||||
|
||||
FileType.JPG -> askForSize {
|
||||
share(barcode.bitmap(it), MIME_JPG, "jpg")
|
||||
}
|
||||
|
||||
FileType.SVG -> shareText(barcode.svg(), MIME_SVG)
|
||||
FileType.TXT -> shareText(barcode.text())
|
||||
}
|
||||
}
|
||||
|
||||
private fun Context.share(bitmap: Bitmap, mimeType: String, ext: String) {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
val file = File(
|
||||
externalCacheDir,
|
||||
"shared_barcode.$ext"
|
||||
)
|
||||
val success = try {
|
||||
FileOutputStream(file).use {
|
||||
when (mimeType) {
|
||||
MIME_PNG -> bitmap.saveAsPng(it)
|
||||
MIME_JPG -> bitmap.saveAsJpg(it)
|
||||
else -> throw IllegalArgumentException(
|
||||
"Invalid mime type"
|
||||
)
|
||||
}
|
||||
}
|
||||
true
|
||||
} catch (_: IOException) {
|
||||
false
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
if (success) {
|
||||
shareFile(file, mimeType)
|
||||
} else {
|
||||
toast(R.string.error_saving_file)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun brightenScreen() {
|
||||
if (isFinishing || isDestroyed) {
|
||||
return
|
||||
}
|
||||
currentBrightness = getScreenBrightness()
|
||||
setScreenBrightness(1f)
|
||||
brightenScreenItem?.isChecked = true
|
||||
}
|
||||
|
||||
private fun restoreScreenBrightness() {
|
||||
if (currentBrightness > -1f) {
|
||||
setScreenBrightness(currentBrightness)
|
||||
currentBrightness = -1f
|
||||
brightenScreenItem?.isChecked = false
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val BIT_MATRIX_WIDTH = "bit_matrix_width"
|
||||
private const val BIT_MATRIX_HEIGHT = "bit_matrix_height"
|
||||
private const val BIT_MATRIX_DATA = "bit_matrix_data"
|
||||
private const val CONTENT_TEXT = "content_text"
|
||||
private const val CONTENT_RAW = "content_raw"
|
||||
private const val FORMAT = "format"
|
||||
private const val EC_LEVEL = "ec_level"
|
||||
private const val MARGIN = "margin"
|
||||
private const val COLORS = "colors"
|
||||
private const val MIME_PNG = "image/png"
|
||||
private const val MIME_JPG = "image/jpeg"
|
||||
private const val MIME_SVG = "image/svg+xmg"
|
||||
private const val MIME_TXT = "text/plain"
|
||||
|
||||
fun newIntent(
|
||||
context: Context,
|
||||
barcode: Barcode<*>,
|
||||
colors: Int = 0
|
||||
): Intent {
|
||||
val args = Bundle()
|
||||
when (barcode) {
|
||||
is ContentBarcode -> args.putEcLevelMargin(
|
||||
barcode.ecLevel,
|
||||
barcode.margin
|
||||
)
|
||||
|
||||
is BitMatrixBarcode -> {
|
||||
args.putInt(BIT_MATRIX_WIDTH, barcode.bitMatrix.width)
|
||||
args.putInt(BIT_MATRIX_HEIGHT, barcode.bitMatrix.height)
|
||||
args.putByteArray(BIT_MATRIX_DATA, barcode.bitMatrix.data)
|
||||
}
|
||||
}
|
||||
args.putContentFormatColors(
|
||||
barcode.content,
|
||||
barcode.format,
|
||||
colors
|
||||
)
|
||||
return Intent(context, BarcodeActivity::class.java).apply {
|
||||
putExtras(args)
|
||||
}
|
||||
}
|
||||
|
||||
fun <T> newIntent(
|
||||
context: Context,
|
||||
content: T,
|
||||
format: BarcodeFormat,
|
||||
ecLevel: Int = -1,
|
||||
margin: Int = 0,
|
||||
colors: Int = 0
|
||||
): Intent {
|
||||
val args = Bundle()
|
||||
args.putContentFormatColors(content, format, colors)
|
||||
args.putEcLevelMargin(ecLevel, margin)
|
||||
return Intent(context, BarcodeActivity::class.java).apply {
|
||||
putExtras(args)
|
||||
}
|
||||
}
|
||||
|
||||
private fun <T> Bundle.putContentFormatColors(
|
||||
content: T,
|
||||
format: BarcodeFormat,
|
||||
colors: Int
|
||||
) {
|
||||
when (content) {
|
||||
is String -> {
|
||||
putString(CONTENT_TEXT, content)
|
||||
}
|
||||
|
||||
is ByteArray -> {
|
||||
putByteArray(CONTENT_RAW, content)
|
||||
}
|
||||
|
||||
else -> {
|
||||
throw IllegalArgumentException(
|
||||
"content must be a String of a ByteArray"
|
||||
)
|
||||
}
|
||||
}
|
||||
putString(FORMAT, format.name)
|
||||
putInt(COLORS, colors)
|
||||
}
|
||||
|
||||
private fun Bundle.putEcLevelMargin(
|
||||
ecLevel: Int,
|
||||
margin: Int
|
||||
) {
|
||||
putInt(EC_LEVEL, ecLevel)
|
||||
putInt(MARGIN, margin)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun ImageView.setScale(scale: Float, x: Float, y: Float) {
|
||||
imageMatrix = Matrix().apply {
|
||||
setTranslate(-x, -y)
|
||||
postScale(scale, scale)
|
||||
postTranslate(x, y)
|
||||
}
|
||||
}
|
||||
|
||||
private fun ImageView.getScale(): Float {
|
||||
val values = FloatArray(9)
|
||||
imageMatrix.getValues(values)
|
||||
return values[Matrix.MSCALE_X]
|
||||
}
|
||||
|
||||
private fun Bitmap.saveAsPng(outputStream: OutputStream, quality: Int = 90) =
|
||||
compress(Bitmap.CompressFormat.PNG, quality, outputStream)
|
||||
|
||||
private fun Bitmap.saveAsJpg(outputStream: OutputStream, quality: Int = 90) =
|
||||
compress(Bitmap.CompressFormat.JPEG, quality, outputStream)
|
||||
|
||||
private val fileNameCharacters = "[^A-Za-z0-9]".toRegex()
|
||||
private fun encodeFileName(name: String): String = fileNameCharacters
|
||||
.replace(name, "_")
|
||||
.take(16)
|
||||
.trim('_')
|
||||
.lowercase(Locale.getDefault())
|
||||
|
||||
private fun getSize(step: Int) = when (step) {
|
||||
0 -> 0
|
||||
else -> 128 shl (step - 1)
|
||||
}
|
||||
|
||||
private fun readAndAddToHistory(
|
||||
bitmap: Bitmap,
|
||||
format: BarcodeFormat
|
||||
) {
|
||||
ZxingCpp.readBitmap(
|
||||
bitmap,
|
||||
0, 0,
|
||||
bitmap.width, bitmap.height,
|
||||
0,
|
||||
ZxingCpp.ReaderOptions(
|
||||
tryHarder = true,
|
||||
tryRotate = true,
|
||||
tryInvert = true,
|
||||
tryDownscale = true,
|
||||
maxNumberOfSymbols = 1,
|
||||
formats = setOf(format)
|
||||
)
|
||||
)?.first {
|
||||
db.insertScan(it.toScan())
|
||||
true
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,873 @@
|
||||
package de.markusfisch.android.binaryeye.activity
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.AlertDialog
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Environment
|
||||
import android.provider.DocumentsContract
|
||||
import android.text.Editable
|
||||
import android.text.Html
|
||||
import android.text.Spannable
|
||||
import android.text.SpannableString
|
||||
import android.text.TextWatcher
|
||||
import android.text.method.LinkMovementMethod
|
||||
import android.text.style.TypefaceSpan
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.EditText
|
||||
import android.widget.ImageView
|
||||
import android.widget.TableLayout
|
||||
import android.widget.TableRow
|
||||
import android.widget.TextView
|
||||
import androidx.core.net.toUri
|
||||
import com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.actions.ActionRegistry
|
||||
import de.markusfisch.android.binaryeye.actions.mail.MatMsg
|
||||
import de.markusfisch.android.binaryeye.actions.mail.MatMsgAction
|
||||
import de.markusfisch.android.binaryeye.actions.vtype.VTypeParser
|
||||
import de.markusfisch.android.binaryeye.actions.vtype.vcard.VCardAction
|
||||
import de.markusfisch.android.binaryeye.actions.vtype.vevent.VEventAction
|
||||
import de.markusfisch.android.binaryeye.actions.web.WebAction
|
||||
import de.markusfisch.android.binaryeye.actions.wifi.WifiAction
|
||||
import de.markusfisch.android.binaryeye.actions.wifi.WifiConnector
|
||||
import de.markusfisch.android.binaryeye.adapter.prettifyFormatName
|
||||
import de.markusfisch.android.binaryeye.app.db
|
||||
import de.markusfisch.android.binaryeye.app.hasLocationPermission
|
||||
import de.markusfisch.android.binaryeye.app.hasWritePermission
|
||||
import de.markusfisch.android.binaryeye.app.prefs
|
||||
import de.markusfisch.android.binaryeye.content.ContentBarcode
|
||||
import de.markusfisch.android.binaryeye.content.IdlParser
|
||||
import de.markusfisch.android.binaryeye.content.copyToClipboard
|
||||
import de.markusfisch.android.binaryeye.content.idlToRes
|
||||
import de.markusfisch.android.binaryeye.content.shareAsFile
|
||||
import de.markusfisch.android.binaryeye.content.shareText
|
||||
import de.markusfisch.android.binaryeye.content.toBarcode
|
||||
import de.markusfisch.android.binaryeye.content.toErrorCorrectionInt
|
||||
import de.markusfisch.android.binaryeye.content.wipeLastShareFile
|
||||
import de.markusfisch.android.binaryeye.database.Scan
|
||||
import de.markusfisch.android.binaryeye.database.toBundle
|
||||
import de.markusfisch.android.binaryeye.database.toScan
|
||||
import de.markusfisch.android.binaryeye.io.askForFileName
|
||||
import de.markusfisch.android.binaryeye.io.toSaveResult
|
||||
import de.markusfisch.android.binaryeye.io.writeExternalFile
|
||||
import de.markusfisch.android.binaryeye.net.createEncodeDeeplink
|
||||
import de.markusfisch.android.binaryeye.view.hideSoftKeyboard
|
||||
import de.markusfisch.android.binaryeye.view.setPaddingFromWindowInsets
|
||||
import de.markusfisch.android.binaryeye.widget.toast
|
||||
import de.markusfisch.android.zxingcpp.ZxingCpp
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.File
|
||||
import java.security.MessageDigest
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
class DecodeActivity : ScreenActivity() {
|
||||
private lateinit var contentView: EditText
|
||||
private lateinit var formatView: TextView
|
||||
private lateinit var dataView: TableLayout
|
||||
private lateinit var metaView: TableLayout
|
||||
private lateinit var hexView: TextView
|
||||
private lateinit var stampView: TextView
|
||||
private lateinit var recreationView: ImageView
|
||||
private lateinit var labelView: EditText
|
||||
private lateinit var fab: FloatingActionButton
|
||||
private lateinit var scan: Scan
|
||||
private lateinit var format: String
|
||||
|
||||
private val parentJob = Job()
|
||||
private val scope: CoroutineScope = CoroutineScope(
|
||||
Dispatchers.Main + parentJob
|
||||
)
|
||||
private val dp: Float
|
||||
get() = resources.displayMetrics.density
|
||||
private val content: String
|
||||
get() = contentView.text.toString()
|
||||
|
||||
private var closeAutomatically = false
|
||||
private var action = ActionRegistry.DEFAULT_ACTION
|
||||
private var isBinary = false
|
||||
private var originalBytes: ByteArray = ByteArray(0)
|
||||
private var label: String? = null
|
||||
private var recreationSize = 0
|
||||
|
||||
override fun onActivityResult(
|
||||
requestCode: Int,
|
||||
resultCode: Int,
|
||||
resultData: Intent?
|
||||
) {
|
||||
super.onActivityResult(requestCode, resultCode, resultData)
|
||||
when (requestCode) {
|
||||
OPEN_DOCUMENT -> {
|
||||
if (resultCode == Activity.RESULT_OK) {
|
||||
val uri = resultData?.data ?: return
|
||||
openPickedFile(uri)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate(state: Bundle?) {
|
||||
super.onCreate(state)
|
||||
setTitle(R.string.content)
|
||||
val frame = findViewById(R.id.content_frame) as ViewGroup
|
||||
val view = layoutInflater.inflate(
|
||||
R.layout.fragment_decode,
|
||||
frame,
|
||||
false
|
||||
)
|
||||
frame.addView(view)
|
||||
|
||||
val justScanned = intent.hasExtra(DECODED) || intent.hasExtra(DECODED_SCAN)
|
||||
closeAutomatically = prefs.closeAutomatically && justScanned
|
||||
|
||||
scan = intent?.extras?.getBundle(SCAN_BUNDLE)?.toScan()
|
||||
?: throw IllegalArgumentException("DecodeActivity needs a Scan")
|
||||
|
||||
isBinary = scan.text.isEmpty()
|
||||
originalBytes = scan.raw ?: scan.text.toByteArray()
|
||||
format = scan.format.name
|
||||
recreationSize = (200f * dp).roundToInt()
|
||||
|
||||
contentView = view.findViewById(R.id.content)
|
||||
formatView = view.findViewById(R.id.format)
|
||||
dataView = view.findViewById(R.id.data)
|
||||
metaView = view.findViewById(R.id.meta)
|
||||
hexView = view.findViewById(R.id.hex)
|
||||
stampView = view.findViewById(R.id.stamp)
|
||||
recreationView = view.findViewById(R.id.recreation)
|
||||
labelView = view.findViewById(R.id.label)
|
||||
fab = view.findViewById(R.id.open)
|
||||
|
||||
if (prefs.showMetaData) {
|
||||
metaView.fillMetaView(scan)
|
||||
} else {
|
||||
metaView.visibility = View.GONE
|
||||
}
|
||||
|
||||
if (scan.id > 0) {
|
||||
scan.label?.let {
|
||||
labelView.setText(it)
|
||||
label = it
|
||||
}
|
||||
} else {
|
||||
labelView.visibility = View.GONE
|
||||
}
|
||||
|
||||
view.findViewById<View>(R.id.inset_layout).setPaddingFromWindowInsets()
|
||||
view.findViewById<View>(R.id.scroll_view).setPaddingFromWindowInsets()
|
||||
|
||||
initContentAndFab(justScanned)
|
||||
updateViewsAndFab(scan.text, originalBytes)
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
val id = scan.id
|
||||
if (id > 0) {
|
||||
val newLabel = labelView.text.toString().trim()
|
||||
if (newLabel != (label ?: "")) {
|
||||
db.renameScan(id, newLabel)
|
||||
label = newLabel
|
||||
}
|
||||
}
|
||||
if (action.fired) {
|
||||
maybeBackOrFinish()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
parentJob.cancel()
|
||||
wipeLastShareFile()
|
||||
}
|
||||
|
||||
private fun initContentAndFab(justScanned: Boolean) {
|
||||
if (isBinary) {
|
||||
contentView.setText(String(originalBytes).foldNonAlNum())
|
||||
contentView.isEnabled = false
|
||||
fab.setImageResource(R.drawable.ic_action_save)
|
||||
fab.setOnClickListener {
|
||||
askForFileNameAndSave(originalBytes)
|
||||
}
|
||||
} else {
|
||||
contentView.setText(scan.text)
|
||||
contentView.addTextChangedListener(object : TextWatcher {
|
||||
override fun afterTextChanged(s: Editable?) {
|
||||
updateViewsAndFab(s?.toString() ?: "")
|
||||
}
|
||||
|
||||
override fun beforeTextChanged(
|
||||
s: CharSequence?,
|
||||
start: Int,
|
||||
count: Int,
|
||||
after: Int
|
||||
) = Unit
|
||||
|
||||
override fun onTextChanged(
|
||||
s: CharSequence?,
|
||||
start: Int,
|
||||
before: Int,
|
||||
count: Int
|
||||
) = Unit
|
||||
})
|
||||
fab.setOnClickListener {
|
||||
executeAction(content)
|
||||
}
|
||||
if (justScanned && prefs.openImmediately) {
|
||||
closeAutomatically = true
|
||||
executeAction(content)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateViewsAndFab(text: String, bytes: ByteArray? = null) {
|
||||
val b = bytes ?: text.toByteArray()
|
||||
resolveActionAndUpdateFab(b)
|
||||
updateViews(text, b)
|
||||
}
|
||||
|
||||
private fun resolveActionAndUpdateFab(bytes: ByteArray) {
|
||||
if (!isBinary) {
|
||||
val prevAction = action
|
||||
if (!prevAction.canExecuteOn(bytes)) {
|
||||
action = ActionRegistry.getAction(bytes)
|
||||
}
|
||||
if (prevAction !== action) {
|
||||
fab.setImageResource(action.iconResId)
|
||||
}
|
||||
}
|
||||
// Run this for the initial action too.
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
||||
fab.setOnLongClickListener { v ->
|
||||
v.context.toast(action.titleResId)
|
||||
true
|
||||
}
|
||||
} else {
|
||||
fab.tooltipText = getString(action.titleResId)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateViews(text: String, bytes: ByteArray) {
|
||||
formatView.text = resources.getQuantityString(
|
||||
R.plurals.barcode_info,
|
||||
bytes.size,
|
||||
format.prettifyFormatName(),
|
||||
bytes.size
|
||||
)
|
||||
hexView.showIf(prefs.showHexDump) { v ->
|
||||
v.text = hexDump(bytes)
|
||||
}
|
||||
recreationView.showIf(prefs.showRecreation) { v ->
|
||||
val unmodified: Boolean
|
||||
val content = if (isBinary) {
|
||||
if (bytes.isEmpty()) {
|
||||
clearRecreation()
|
||||
return@showIf
|
||||
}
|
||||
unmodified = bytes.contentEquals(originalBytes)
|
||||
bytes
|
||||
} else {
|
||||
if (text.isEmpty()) {
|
||||
clearRecreation()
|
||||
return@showIf
|
||||
}
|
||||
unmodified = text == scan.text
|
||||
text
|
||||
}
|
||||
val barcode = if (unmodified) {
|
||||
scan.toBarcode()
|
||||
} else {
|
||||
ContentBarcode(
|
||||
content,
|
||||
scan.format,
|
||||
scan.errorCorrectionLevel.toErrorCorrectionInt(),
|
||||
margin = 1
|
||||
)
|
||||
}
|
||||
try {
|
||||
v.setImageBitmap(barcode.bitmap(recreationSize))
|
||||
v.setOnClickListener {
|
||||
v.context.hideSoftKeyboard(contentView)
|
||||
v.post {
|
||||
startActivity(
|
||||
BarcodeActivity.newIntent(this, barcode)
|
||||
)
|
||||
}
|
||||
}
|
||||
} catch (_: RuntimeException) {
|
||||
clearRecreation()
|
||||
}
|
||||
}
|
||||
dataView.fillDataView(text, bytes)
|
||||
stampView.setTrackingLink(bytes, format)
|
||||
}
|
||||
|
||||
private fun clearRecreation() {
|
||||
recreationView.apply {
|
||||
setImageBitmap(null)
|
||||
setOnClickListener(null)
|
||||
}
|
||||
}
|
||||
|
||||
private fun TableLayout.fillDataView(text: String, bytes: ByteArray) {
|
||||
val items = LinkedHashMap<Any, CharSequence?>()
|
||||
when (prefs.showChecksum) {
|
||||
"CRC4" -> items[R.string.crc4] = String.format("%X", crc4(bytes))
|
||||
"MD5" -> items[R.string.md5] = bytes.md5().toHexString().fold()
|
||||
"SHA1" -> items[R.string.sha1] = bytes.sha1().toHexString().fold()
|
||||
"SHA256" -> items[R.string.sha256] =
|
||||
bytes.sha256().toHexString().fold()
|
||||
|
||||
else -> Unit
|
||||
}
|
||||
IdlParser.parse(String(bytes))?.let {
|
||||
items.putAll(mapOf("IIN" to it.iin))
|
||||
items.putAll(
|
||||
it.elements.entries.associate { (id, value) ->
|
||||
context.idlToRes(id) to value
|
||||
}
|
||||
)
|
||||
}
|
||||
when (action) {
|
||||
is MatMsgAction -> items.putAll(
|
||||
MatMsg(text).run {
|
||||
mapOf(
|
||||
R.string.email_to to to,
|
||||
R.string.email_subject to sub,
|
||||
R.string.email_body to body
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
is VCardAction,
|
||||
is VEventAction -> VTypeParser.parseMap(text).let { vData ->
|
||||
items.putAll(
|
||||
vData.map { item ->
|
||||
item.key to item.value.joinToString("\n") {
|
||||
it.value
|
||||
}
|
||||
}.toMap()
|
||||
)
|
||||
}
|
||||
|
||||
is WebAction -> try {
|
||||
items.putAll(
|
||||
text.toUri().run {
|
||||
mapOf(
|
||||
R.string.scheme to scheme,
|
||||
R.string.host to host,
|
||||
R.string.query to query
|
||||
)
|
||||
}
|
||||
)
|
||||
} catch (_: Exception) {
|
||||
// Ignore
|
||||
}
|
||||
|
||||
is WifiAction -> WifiConnector.parseMap(text)?.let { wifiData ->
|
||||
items.putAll(
|
||||
linkedMapOf(
|
||||
R.string.entry_type to getString(R.string.wifi_network),
|
||||
R.string.wifi_ssid to wifiData["S"],
|
||||
R.string.wifi_password to wifiData["P"],
|
||||
R.string.wifi_type to wifiData["T"],
|
||||
R.string.wifi_hidden to wifiData["H"],
|
||||
R.string.wifi_eap to wifiData["E"],
|
||||
R.string.wifi_identity to wifiData["I"],
|
||||
R.string.wifi_anonymous_identity to wifiData["A"],
|
||||
R.string.wifi_phase2 to wifiData["PH2"]
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
fill(items)
|
||||
}
|
||||
|
||||
private fun TableLayout.fillMetaView(scan: Scan) {
|
||||
val items = linkedMapOf<Any, CharSequence?>(
|
||||
R.string.error_correction_level to scan.errorCorrectionLevel,
|
||||
R.string.sequence_size to scan.sequenceSize.positiveToString(),
|
||||
R.string.sequence_index to scan.sequenceIndex.positiveToString(),
|
||||
R.string.sequence_id to scan.sequenceId,
|
||||
R.string.gtin_country to scan.country,
|
||||
R.string.gtin_add_on to scan.addOn,
|
||||
R.string.gtin_price to scan.price,
|
||||
R.string.gtin_issue_number to scan.issueNumber,
|
||||
)
|
||||
if (!scan.version.isNullOrBlank()) {
|
||||
val versionString = if (scan.format == ZxingCpp.BarcodeFormat.QRCode) {
|
||||
getString(
|
||||
R.string.qr_version_and_modules,
|
||||
scan.version,
|
||||
(scan.version.toIntOrNull() ?: 0) * 4 + 17
|
||||
)
|
||||
} else {
|
||||
scan.version
|
||||
}
|
||||
items.putAll(
|
||||
linkedMapOf(
|
||||
R.string.barcode_version_number to versionString
|
||||
)
|
||||
)
|
||||
}
|
||||
if (scan.format == ZxingCpp.BarcodeFormat.QRCode &&
|
||||
scan.dataMask > -1
|
||||
) {
|
||||
items.putAll(
|
||||
linkedMapOf(
|
||||
R.string.qr_data_mask to scan.dataMask.toString()
|
||||
)
|
||||
)
|
||||
}
|
||||
fill(items)
|
||||
}
|
||||
|
||||
private fun TableLayout.fill(
|
||||
items: LinkedHashMap<Any, CharSequence?>
|
||||
) {
|
||||
removeAllViews()
|
||||
visibility = if (items.isEmpty()) View.GONE else {
|
||||
val ctx = context
|
||||
val spaceBetween = (16f * dp).roundToInt()
|
||||
items.forEach { item ->
|
||||
val text = item.value
|
||||
if (!text.isNullOrBlank()) {
|
||||
val tr = TableRow(ctx)
|
||||
val keyView = TextView(ctx)
|
||||
when (val key = item.key) {
|
||||
is Int -> keyView.setText(key)
|
||||
is String -> keyView.text = key
|
||||
else -> keyView.text = key.toString()
|
||||
}
|
||||
val valueView = TextView(ctx).apply {
|
||||
setPadding(spaceBetween, 0, 0, 0)
|
||||
this.text = text
|
||||
setOnClickListener {
|
||||
copyToClipboard(text.toString())
|
||||
}
|
||||
}
|
||||
tr.addView(keyView)
|
||||
tr.addView(valueView)
|
||||
addView(tr)
|
||||
}
|
||||
}
|
||||
View.VISIBLE
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
inflater.inflate(R.menu.fragment_decode, menu)
|
||||
if (scan.id > 0L) {
|
||||
menu.findItem(R.id.remove).isVisible = true
|
||||
}
|
||||
if (action is WifiAction) {
|
||||
menu.findItem(R.id.copy_password).isVisible = true
|
||||
}
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
return when (item.itemId) {
|
||||
R.id.copy_password -> {
|
||||
copyPasswordToClipboard()
|
||||
maybeBackOrFinish()
|
||||
true
|
||||
}
|
||||
|
||||
R.id.copy_to_clipboard -> {
|
||||
copyToClipboard(textOrHex())
|
||||
maybeBackOrFinish()
|
||||
true
|
||||
}
|
||||
|
||||
R.id.copy_as_deeplink -> {
|
||||
copyToClipboard(deeplinkToCopy())
|
||||
maybeBackOrFinish()
|
||||
true
|
||||
}
|
||||
|
||||
R.id.share -> {
|
||||
shareText(textOrHex())
|
||||
maybeBackOrFinish()
|
||||
true
|
||||
}
|
||||
|
||||
R.id.share_file -> {
|
||||
val (data, name) = if (isBinary) {
|
||||
Pair(originalBytes, "barcode_content.bin")
|
||||
} else {
|
||||
Pair(content, "barcode_content.txt")
|
||||
}
|
||||
shareAsFile(data, name)
|
||||
maybeBackOrFinish()
|
||||
true
|
||||
}
|
||||
|
||||
R.id.create -> {
|
||||
startActivity(
|
||||
EncodeActivity.newIntent(
|
||||
this,
|
||||
if (isBinary) originalBytes else content,
|
||||
format
|
||||
)
|
||||
)
|
||||
true
|
||||
}
|
||||
|
||||
R.id.remove -> {
|
||||
askToRemoveScan(scan.id)
|
||||
true
|
||||
}
|
||||
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
}
|
||||
|
||||
private fun askToRemoveScan(id: Long) {
|
||||
AlertDialog.Builder(this)
|
||||
.setMessage(R.string.really_remove_scan)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
db.removeScan(id)
|
||||
backOrFinish()
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||
}
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun textOrHex() = if (isBinary) {
|
||||
originalBytes.toHexString()
|
||||
} else {
|
||||
content
|
||||
}
|
||||
|
||||
private fun deeplinkToCopy() = createEncodeDeeplink(
|
||||
format = format,
|
||||
content = textOrHex(),
|
||||
)
|
||||
|
||||
private fun copyPasswordToClipboard() {
|
||||
val ac = action
|
||||
if (ac is WifiAction) {
|
||||
ac.password?.let { password ->
|
||||
copyToClipboard(password, isSensitive = true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun copyToClipboard(text: String, isSensitive: Boolean = false) {
|
||||
(this as android.content.Context).copyToClipboard(text, isSensitive)
|
||||
// There's a clipboard popup from Android 13 on.
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
|
||||
toast(R.string.copied_to_clipboard)
|
||||
}
|
||||
}
|
||||
|
||||
private fun maybeBackOrFinish() {
|
||||
if (closeAutomatically) {
|
||||
backOrFinish()
|
||||
}
|
||||
}
|
||||
|
||||
private fun backOrFinish() {
|
||||
finish()
|
||||
}
|
||||
|
||||
private fun executeAction(str: String) {
|
||||
if (str.isEmpty() || openLocalDocument(str)) {
|
||||
return
|
||||
}
|
||||
if (action is WifiAction &&
|
||||
Build.VERSION.SDK_INT < Build.VERSION_CODES.Q &&
|
||||
!hasLocationPermission { executeAction(str) }
|
||||
) {
|
||||
return
|
||||
}
|
||||
scope.launch {
|
||||
action.execute(this@DecodeActivity, str.toByteArray())
|
||||
}
|
||||
}
|
||||
|
||||
private fun openLocalDocument(str: String): Boolean {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N ||
|
||||
!str.startsWith(SCHEME_FILE)
|
||||
) {
|
||||
return false
|
||||
}
|
||||
// Needs to be disabled so we get the result.
|
||||
closeAutomatically = false
|
||||
// file:// can't be used with Scoped Storage anymore.
|
||||
startActivityForResult(
|
||||
Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
|
||||
addCategory(Intent.CATEGORY_OPENABLE)
|
||||
type = "*/*"
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val primary = str.substring(
|
||||
SCHEME_FILE.length
|
||||
).localDirName() ?: Environment.getExternalStorageDirectory().path
|
||||
putExtra(
|
||||
DocumentsContract.EXTRA_INITIAL_URI,
|
||||
DocumentsContract.buildDocumentUri(
|
||||
"com.android.externalstorage.documents",
|
||||
"primary:$primary"
|
||||
)
|
||||
)
|
||||
}
|
||||
},
|
||||
OPEN_DOCUMENT
|
||||
)
|
||||
return true
|
||||
}
|
||||
|
||||
private fun askForFileNameAndSave(raw: ByteArray) {
|
||||
// Write permission is only required before Android Q.
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q &&
|
||||
!hasWritePermission { askForFileNameAndSave(raw) }
|
||||
) {
|
||||
return
|
||||
}
|
||||
scope.launch(Dispatchers.Main) {
|
||||
val name = askForFileName() ?: return@launch
|
||||
withContext(Dispatchers.IO) {
|
||||
val message = writeExternalFile(
|
||||
name,
|
||||
"application/octet-stream"
|
||||
) {
|
||||
it.write(raw)
|
||||
}.toSaveResult()
|
||||
withContext(Dispatchers.Main) {
|
||||
toast(message)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val DECODED_SCAN = "decoded_scan"
|
||||
const val DECODED = "decoded"
|
||||
private const val SCAN_BUNDLE = "scan_bundle"
|
||||
private const val OPEN_DOCUMENT = 1
|
||||
private const val SCHEME_FILE = "file://"
|
||||
|
||||
fun newIntent(context: Context, scan: Scan): Intent {
|
||||
val args = Bundle()
|
||||
args.putBundle(SCAN_BUNDLE, scan.toBundle())
|
||||
return Intent(context, DecodeActivity::class.java).apply {
|
||||
putExtra(DECODED_SCAN, true)
|
||||
putExtras(args)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private inline fun <T : View> T.showIf(
|
||||
visible: Boolean,
|
||||
block: (T) -> Unit
|
||||
) {
|
||||
visibility = if (visible) {
|
||||
block.invoke(this)
|
||||
View.VISIBLE
|
||||
} else {
|
||||
View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
private val nonAlNum = "[^a-zA-Z0-9]".toRegex()
|
||||
private val multipleDots = "…+".toRegex()
|
||||
private fun String.foldNonAlNum() = replace(nonAlNum, "…")
|
||||
.replace(multipleDots, "…")
|
||||
|
||||
private fun hexDump(bytes: ByteArray, charsPerLine: Int = 33): String {
|
||||
if (charsPerLine < 4 || bytes.isEmpty()) {
|
||||
return ""
|
||||
}
|
||||
val dump = StringBuilder()
|
||||
val hex = StringBuilder()
|
||||
val ascii = StringBuilder()
|
||||
val itemsPerLine = (charsPerLine - 1) / 4
|
||||
val len = bytes.size
|
||||
var i = 0
|
||||
while (true) {
|
||||
val ord = bytes[i]
|
||||
hex.append(String.format("%02X ", ord))
|
||||
ascii.append(if (ord > 31) ord.toInt().toChar() else " ")
|
||||
++i
|
||||
val posInLine = i % itemsPerLine
|
||||
val atEnd = i >= len
|
||||
var atLineEnd = posInLine == 0
|
||||
if (atEnd && !atLineEnd) {
|
||||
for (j in posInLine until itemsPerLine) {
|
||||
hex.append(" ")
|
||||
}
|
||||
atLineEnd = true
|
||||
}
|
||||
if (atLineEnd) {
|
||||
dump.append(hex.toString())
|
||||
dump.append(" ")
|
||||
dump.append(ascii.toString())
|
||||
dump.append("\n")
|
||||
hex.setLength(0)
|
||||
ascii.setLength(0)
|
||||
}
|
||||
if (atEnd) {
|
||||
break
|
||||
}
|
||||
}
|
||||
return dump.toString()
|
||||
}
|
||||
|
||||
private fun Int.positiveToString() = if (this > -1) this.toString() else ""
|
||||
|
||||
private fun TextView.setTrackingLink(bytes: ByteArray, format: String) {
|
||||
val trackingLink = generateDpTrackingLink(bytes, format)
|
||||
if (trackingLink != null) {
|
||||
text = trackingLink.fromHtml()
|
||||
isClickable = true
|
||||
movementMethod = LinkMovementMethod.getInstance()
|
||||
} else {
|
||||
visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
private fun generateDpTrackingLink(bytes: ByteArray, format: String): String? {
|
||||
// Check for Deutsche Post Matrixcode stamp.
|
||||
var isStamp = false
|
||||
var rawData = bytes
|
||||
if (format == ZxingCpp.BarcodeFormat.DataMatrix.name &&
|
||||
bytes.toString(Charsets.ISO_8859_1).startsWith("DEA5")
|
||||
) {
|
||||
if (bytes.size == 47) {
|
||||
isStamp = true
|
||||
} else if (bytes.size > 47) {
|
||||
// Transform back to original data.
|
||||
rawData = bytes.toString(Charsets.UTF_8).toByteArray(
|
||||
Charsets.ISO_8859_1
|
||||
)
|
||||
if (rawData.size == 47) {
|
||||
isStamp = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!isStamp) {
|
||||
return null
|
||||
}
|
||||
|
||||
val hex = StringBuilder()
|
||||
hex.append(String.format("%02X", rawData[9]))
|
||||
hex.append(String.format("%02X", rawData[10]))
|
||||
hex.append(String.format("%02X", rawData[11]))
|
||||
hex.append(String.format("%02X", rawData[12]))
|
||||
hex.append(String.format("%02X", rawData[13]))
|
||||
hex.append(String.format("%X", (rawData[4].toInt() and 0x0f).toByte()))
|
||||
hex.append(String.format("%02X", rawData[5]))
|
||||
hex.append(String.format("%02X", rawData[6]))
|
||||
hex.append(String.format("%02X", rawData[7]))
|
||||
hex.append(String.format("%02X", rawData[8]))
|
||||
val hexString = hex.toString()
|
||||
val trackingNumber = hexString + String.format(
|
||||
"%X",
|
||||
crc4(hexString.toByteArray(Charsets.ISO_8859_1))
|
||||
)
|
||||
return "<a href=\"https://www.deutschepost.de/de/s/sendungsverfolgung.html?piececode=$trackingNumber\">Deutsche Post: $trackingNumber</a>"
|
||||
}
|
||||
|
||||
// CRC-4 with polynomial x^4 + x + 1.
|
||||
private fun crc4(input: ByteArray): Int {
|
||||
var crc = 0
|
||||
var i = 0
|
||||
while (i < input.size) {
|
||||
val c = input[i].toInt()
|
||||
var j = 0x80
|
||||
while (j != 0) {
|
||||
var bit = crc and 0x8
|
||||
crc = crc shl 1
|
||||
if (c and j != 0) {
|
||||
bit = bit xor 0x8
|
||||
}
|
||||
if (bit != 0) {
|
||||
crc = crc xor 0x3
|
||||
}
|
||||
j = j ushr 1
|
||||
}
|
||||
++i
|
||||
}
|
||||
crc = crc and 0xF
|
||||
return crc
|
||||
}
|
||||
|
||||
private fun String.fromHtml() = if (
|
||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.N
|
||||
) {
|
||||
Html.fromHtml(this, Html.FROM_HTML_MODE_LEGACY)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
Html.fromHtml(this)
|
||||
}
|
||||
|
||||
private fun ByteArray.md5(): ByteArray = MessageDigest.getInstance(
|
||||
"MD5"
|
||||
).run {
|
||||
update(this@md5)
|
||||
digest()
|
||||
}
|
||||
|
||||
private fun ByteArray.sha1(): ByteArray = MessageDigest.getInstance(
|
||||
"SHA-1"
|
||||
).run {
|
||||
update(this@sha1)
|
||||
digest()
|
||||
}
|
||||
|
||||
private fun ByteArray.sha256(): ByteArray = MessageDigest.getInstance(
|
||||
"SHA-256"
|
||||
).run {
|
||||
update(this@sha256)
|
||||
digest()
|
||||
}
|
||||
|
||||
private fun String.fold(): CharSequence {
|
||||
val l = length
|
||||
return if (l > 10) {
|
||||
val half = l / 2
|
||||
val s = substring(0, half) + "\n" + substring(half)
|
||||
SpannableString(s).apply {
|
||||
setSpan(
|
||||
TypefaceSpan("monospace"),
|
||||
0, s.length,
|
||||
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||
)
|
||||
}
|
||||
} else {
|
||||
this
|
||||
}
|
||||
}
|
||||
|
||||
private fun String.localDirName(): String? = when {
|
||||
endsWith("/") -> this
|
||||
else -> File(this).parentFile?.absolutePath
|
||||
}?.replaceFirst(Regex("^/storage/emulated/0/"), "")
|
||||
?.replaceFirst(Regex("^/sdcard/"), "")
|
||||
?.trim('/')
|
||||
|
||||
private fun Activity.openPickedFile(uri: Uri) {
|
||||
startActivity(
|
||||
Intent.createChooser(
|
||||
Intent(Intent.ACTION_VIEW).apply {
|
||||
setDataAndType(uri, contentResolver.getType(uri) ?: "*/*")
|
||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
},
|
||||
getString(R.string.open_url)
|
||||
)
|
||||
)
|
||||
}
|
||||
+164
-184
@@ -1,12 +1,9 @@
|
||||
package de.markusfisch.android.binaryeye.fragment
|
||||
package de.markusfisch.android.binaryeye.activity
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.support.v4.app.Fragment
|
||||
import android.view.LayoutInflater
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
@@ -16,51 +13,62 @@ import android.widget.AdapterView
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.CheckBox
|
||||
import android.widget.EditText
|
||||
import android.widget.SeekBar
|
||||
import android.widget.Spinner
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.widget.SwitchCompat
|
||||
import androidx.core.net.toUri
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.adapter.prettifyFormatName
|
||||
import de.markusfisch.android.binaryeye.app.addFragment
|
||||
import de.markusfisch.android.binaryeye.app.prefs
|
||||
import de.markusfisch.android.binaryeye.net.isEncodeDeeplink
|
||||
import de.markusfisch.android.binaryeye.text.unescape
|
||||
import de.markusfisch.android.binaryeye.view.hideSoftKeyboard
|
||||
import de.markusfisch.android.binaryeye.view.setPaddingFromWindowInsets
|
||||
import de.markusfisch.android.binaryeye.widget.toast
|
||||
import de.markusfisch.android.zxingcpp.ZxingCpp.BarcodeFormat
|
||||
import java.io.InputStream
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
|
||||
class EncodeFragment : Fragment() {
|
||||
|
||||
class EncodeActivity : ScreenActivity() {
|
||||
private lateinit var formatView: Spinner
|
||||
private lateinit var ecLabel: TextView
|
||||
private lateinit var ecSpinner: Spinner
|
||||
private lateinit var colorsLabel: TextView
|
||||
private lateinit var colorsSpinner: Spinner
|
||||
private lateinit var sizeView: TextView
|
||||
private lateinit var sizeBarView: SeekBar
|
||||
private lateinit var marginLayout: View
|
||||
private lateinit var marginView: TextView
|
||||
private lateinit var marginBarView: SeekBar
|
||||
private lateinit var addQuietZoneSwitch: SwitchCompat
|
||||
private lateinit var contentView: EditText
|
||||
private lateinit var unescapeCheckBox: CheckBox
|
||||
|
||||
private val formats = arrayListOf(
|
||||
BarcodeFormat.AZTEC,
|
||||
BarcodeFormat.CODABAR,
|
||||
BarcodeFormat.CODE_39,
|
||||
BarcodeFormat.CODE_128,
|
||||
BarcodeFormat.DATA_MATRIX,
|
||||
BarcodeFormat.EAN_8,
|
||||
BarcodeFormat.EAN_13,
|
||||
private val writeableFormats = arrayListOf(
|
||||
BarcodeFormat.Aztec,
|
||||
BarcodeFormat.Codabar,
|
||||
BarcodeFormat.Code39,
|
||||
BarcodeFormat.Code39Ext,
|
||||
BarcodeFormat.Code32,
|
||||
BarcodeFormat.PZN,
|
||||
BarcodeFormat.Code93,
|
||||
BarcodeFormat.Code128,
|
||||
BarcodeFormat.DataBar,
|
||||
BarcodeFormat.DataBarOmni,
|
||||
BarcodeFormat.DataBarStk,
|
||||
BarcodeFormat.DataBarLtd,
|
||||
BarcodeFormat.DataBarExp,
|
||||
BarcodeFormat.DataBarExpStk,
|
||||
BarcodeFormat.DataMatrix,
|
||||
BarcodeFormat.DXFilmEdge,
|
||||
BarcodeFormat.EAN8,
|
||||
BarcodeFormat.EAN13,
|
||||
BarcodeFormat.ITF,
|
||||
BarcodeFormat.PDF_417,
|
||||
BarcodeFormat.QR_CODE,
|
||||
BarcodeFormat.UPC_A
|
||||
BarcodeFormat.MaxiCode,
|
||||
BarcodeFormat.PDF417,
|
||||
BarcodeFormat.QRCode,
|
||||
BarcodeFormat.MicroQRCode,
|
||||
BarcodeFormat.RMQRCode,
|
||||
BarcodeFormat.UPCA,
|
||||
BarcodeFormat.UPCE,
|
||||
)
|
||||
|
||||
private var minMargin = 0
|
||||
private var bytes: ByteArray? = null
|
||||
|
||||
override fun onActivityResult(
|
||||
@@ -68,16 +76,16 @@ class EncodeFragment : Fragment() {
|
||||
resultCode: Int,
|
||||
resultData: Intent?
|
||||
) {
|
||||
super.onActivityResult(requestCode, resultCode, resultData)
|
||||
when (requestCode) {
|
||||
PICK_FILE_RESULT_CODE -> {
|
||||
if (resultCode == Activity.RESULT_OK &&
|
||||
resultData != null &&
|
||||
resultData.data != null
|
||||
) {
|
||||
val ac = activity ?: return
|
||||
ac.hideSoftKeyboard(contentView)
|
||||
hideSoftKeyboard(contentView)
|
||||
val uri = resultData.data ?: return
|
||||
bytes = ac.contentResolver?.openInputStream(uri)?.use {
|
||||
bytes = contentResolver?.openInputStream(uri)?.use {
|
||||
it.readBytesMax(4096)
|
||||
} ?: return
|
||||
setEncodeByteArray()
|
||||
@@ -88,28 +96,20 @@ class EncodeFragment : Fragment() {
|
||||
|
||||
override fun onCreate(state: Bundle?) {
|
||||
super.onCreate(state)
|
||||
setHasOptionsMenu(true)
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
state: Bundle?
|
||||
): View? {
|
||||
val ac = activity ?: return null
|
||||
ac.setTitle(R.string.compose_barcode)
|
||||
|
||||
val view = inflater.inflate(
|
||||
setTitle(R.string.compose_barcode)
|
||||
val frame = findViewById(R.id.content_frame) as ViewGroup
|
||||
val view = layoutInflater.inflate(
|
||||
R.layout.fragment_encode,
|
||||
container,
|
||||
frame,
|
||||
false
|
||||
)
|
||||
frame.addView(view)
|
||||
|
||||
formatView = view.findViewById(R.id.format)
|
||||
val formatAdapter = ArrayAdapter(
|
||||
ac,
|
||||
this,
|
||||
android.R.layout.simple_spinner_item,
|
||||
formats.map { prettifyFormatName(it.name) }
|
||||
writeableFormats.map { it.name.prettifyFormatName() }
|
||||
)
|
||||
formatAdapter.setDropDownViewResource(
|
||||
android.R.layout.simple_spinner_dropdown_item
|
||||
@@ -122,11 +122,13 @@ class EncodeFragment : Fragment() {
|
||||
position: Int,
|
||||
id: Long
|
||||
) {
|
||||
val format = formats[position]
|
||||
val format = writeableFormats[position]
|
||||
val arrayId = when (format) {
|
||||
BarcodeFormat.AZTEC -> R.array.aztec_error_correction_levels
|
||||
BarcodeFormat.QR_CODE -> R.array.qr_error_correction_levels
|
||||
BarcodeFormat.PDF_417 -> R.array.pdf417_error_correction_levels
|
||||
BarcodeFormat.Aztec -> R.array.aztec_error_correction_levels
|
||||
BarcodeFormat.QRCode -> R.array.qr_error_correction_levels
|
||||
BarcodeFormat.MicroQRCode -> R.array.mqr_error_correction_levels
|
||||
BarcodeFormat.RMQRCode -> R.array.rmqr_error_correction_levels
|
||||
BarcodeFormat.PDF417 -> R.array.pdf417_error_correction_levels
|
||||
else -> 0
|
||||
}
|
||||
if (arrayId > 0) {
|
||||
@@ -144,13 +146,6 @@ class EncodeFragment : Fragment() {
|
||||
format.canBeInverted().setVisibilityFor(
|
||||
colorsLabel, colorsSpinner
|
||||
)
|
||||
when (format) {
|
||||
BarcodeFormat.AZTEC -> setMarginBar(4, 4)
|
||||
BarcodeFormat.DATA_MATRIX -> setMarginBar(1, 1)
|
||||
BarcodeFormat.QR_CODE -> setMarginBar(4, 4)
|
||||
BarcodeFormat.PDF_417 -> setMarginBar(0, 30)
|
||||
else -> false
|
||||
}.setVisibilityFor(marginLayout)
|
||||
}
|
||||
|
||||
override fun onNothingSelected(parentView: AdapterView<*>?) {}
|
||||
@@ -165,7 +160,7 @@ class EncodeFragment : Fragment() {
|
||||
position: Int,
|
||||
id: Long
|
||||
) {
|
||||
val format = formats[formatView.selectedItemPosition]
|
||||
val format = writeableFormats[formatView.selectedItemPosition]
|
||||
prefs.indexOfLastSelectedEcLevel = format.packEcLevel(
|
||||
prefs.indexOfLastSelectedEcLevel,
|
||||
position
|
||||
@@ -177,33 +172,29 @@ class EncodeFragment : Fragment() {
|
||||
|
||||
colorsLabel = view.findViewById(R.id.colors_label)
|
||||
colorsSpinner = view.findViewById(R.id.colors)
|
||||
|
||||
sizeView = view.findViewById(R.id.size_display)
|
||||
sizeBarView = view.findViewById(R.id.size_bar)
|
||||
initSizeBar()
|
||||
|
||||
marginLayout = view.findViewById(R.id.margin)
|
||||
marginView = view.findViewById(R.id.margin_display)
|
||||
marginBarView = view.findViewById(R.id.margin_bar)
|
||||
initMarginBar()
|
||||
addQuietZoneSwitch = view.findViewById(R.id.add_quiet_zone)
|
||||
addQuietZoneSwitch.isChecked = prefs.addQuietZone
|
||||
|
||||
contentView = view.findViewById(R.id.content)
|
||||
unescapeCheckBox = view.findViewById(R.id.unescape)
|
||||
unescapeCheckBox.isChecked = prefs.expandEscapeSequences
|
||||
|
||||
val args = arguments
|
||||
var complete = false
|
||||
val args = intent?.extras ?: getExtrasFromDeeplink()
|
||||
args?.getString(CONTENT_TEXT)?.let {
|
||||
contentView.setText(it)
|
||||
complete = it.isNotEmpty()
|
||||
}
|
||||
args?.getByteArray(CONTENT_RAW)?.let {
|
||||
bytes = it
|
||||
complete = it.isNotEmpty()
|
||||
setEncodeByteArray()
|
||||
}
|
||||
|
||||
val barcodeFormat = args?.getString(FORMAT)
|
||||
if (barcodeFormat != null) {
|
||||
formatView.setSelection(
|
||||
formats.indexOf(barcodeFormat.toFormat())
|
||||
writeableFormats.indexOf(barcodeFormat.toFormat())
|
||||
)
|
||||
} else if (state == null) {
|
||||
formatView.post {
|
||||
@@ -215,10 +206,18 @@ class EncodeFragment : Fragment() {
|
||||
it.context.encode()
|
||||
}
|
||||
|
||||
(view.findViewById(R.id.inset_layout) as View).setPaddingFromWindowInsets()
|
||||
(view.findViewById(R.id.scroll_view) as View).setPaddingFromWindowInsets()
|
||||
view.findViewById<View>(R.id.inset_layout).setPaddingFromWindowInsets()
|
||||
view.findViewById<View>(R.id.scroll_view).setPaddingFromWindowInsets()
|
||||
|
||||
return view
|
||||
if (complete &&
|
||||
args?.getBoolean(EXECUTE, false) == true
|
||||
) {
|
||||
view.post {
|
||||
val content = view.context.getContent() ?: return@post
|
||||
startActivity(newBarcodeIntent(content))
|
||||
finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
@@ -248,113 +247,61 @@ class EncodeFragment : Fragment() {
|
||||
super.onPause()
|
||||
prefs.indexOfLastSelectedFormat = formatView.selectedItemPosition
|
||||
prefs.expandEscapeSequences = unescapeCheckBox.isChecked
|
||||
prefs.lastMargin = marginBarView.progress
|
||||
prefs.addQuietZone = addQuietZoneSwitch.isChecked
|
||||
}
|
||||
|
||||
private fun Context.encode() {
|
||||
hideSoftKeyboard(contentView)
|
||||
val content = if (bytes != null) {
|
||||
bytes
|
||||
} else {
|
||||
var text = contentView.text.toString()
|
||||
if (unescapeCheckBox.isChecked) {
|
||||
try {
|
||||
text = text.unescape()
|
||||
} catch (e: IllegalArgumentException) {
|
||||
toast(e.message ?: "Invalid escape sequence")
|
||||
return
|
||||
}
|
||||
}
|
||||
if (text.isEmpty()) {
|
||||
toast(R.string.error_no_content)
|
||||
return
|
||||
}
|
||||
text
|
||||
val content = getContent() ?: return
|
||||
contentView.post {
|
||||
startActivity(newBarcodeIntent(content))
|
||||
}
|
||||
val format = formats[formatView.selectedItemPosition]
|
||||
fragmentManager?.addFragment(
|
||||
BarcodeFragment.newInstance(
|
||||
content,
|
||||
format,
|
||||
getSize(sizeBarView.progress),
|
||||
when (format) {
|
||||
BarcodeFormat.AZTEC,
|
||||
BarcodeFormat.DATA_MATRIX,
|
||||
BarcodeFormat.QR_CODE,
|
||||
BarcodeFormat.PDF_417 -> marginBarView.progress
|
||||
}
|
||||
|
||||
else -> -1
|
||||
},
|
||||
format.getErrorCorrectionLevel(ecSpinner.selectedItemPosition),
|
||||
if (format.canBeInverted()) {
|
||||
colorsSpinner.selectedItemPosition
|
||||
} else 0
|
||||
)
|
||||
private fun newBarcodeIntent(content: Any): Intent {
|
||||
val format = writeableFormats[formatView.selectedItemPosition]
|
||||
return BarcodeActivity.newIntent(
|
||||
this,
|
||||
content,
|
||||
format,
|
||||
format.getErrorCorrectionLevel(ecSpinner.selectedItemPosition),
|
||||
if (addQuietZoneSwitch.isChecked) {
|
||||
1
|
||||
} else {
|
||||
0
|
||||
},
|
||||
if (format.canBeInverted()) {
|
||||
colorsSpinner.selectedItemPosition
|
||||
} else 0
|
||||
)
|
||||
}
|
||||
|
||||
private fun initSizeBar() {
|
||||
updateSize(sizeBarView.progress)
|
||||
sizeBarView.setOnSeekBarChangeListener(
|
||||
object : SeekBar.OnSeekBarChangeListener {
|
||||
override fun onProgressChanged(
|
||||
seekBar: SeekBar,
|
||||
progressValue: Int,
|
||||
fromUser: Boolean
|
||||
) {
|
||||
updateSize(progressValue)
|
||||
}
|
||||
|
||||
override fun onStartTrackingTouch(seekBar: SeekBar) {}
|
||||
|
||||
override fun onStopTrackingTouch(seekBar: SeekBar) {}
|
||||
})
|
||||
}
|
||||
|
||||
private fun updateSize(power: Int) {
|
||||
val size = getSize(power)
|
||||
sizeView.text = getString(R.string.size_width_by_height, size, size)
|
||||
}
|
||||
|
||||
private fun initMarginBar() {
|
||||
updateMargin(marginBarView.progress)
|
||||
marginBarView.setOnSeekBarChangeListener(
|
||||
object : SeekBar.OnSeekBarChangeListener {
|
||||
override fun onProgressChanged(
|
||||
seekBar: SeekBar,
|
||||
progressValue: Int,
|
||||
fromUser: Boolean
|
||||
) {
|
||||
if (progressValue >= minMargin) {
|
||||
updateMargin(progressValue)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStartTrackingTouch(seekBar: SeekBar) {}
|
||||
|
||||
override fun onStopTrackingTouch(seekBar: SeekBar) {}
|
||||
})
|
||||
}
|
||||
|
||||
private fun updateMargin(margin: Int) {
|
||||
marginView.text = getString(R.string.margin_size, margin)
|
||||
}
|
||||
|
||||
private fun setMarginBar(minimum: Int, value: Int): Boolean {
|
||||
minMargin = minimum
|
||||
marginBarView.apply {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
min = minimum
|
||||
}
|
||||
progress = max(
|
||||
minimum, if (prefs.lastMargin > -1) {
|
||||
prefs.lastMargin
|
||||
} else {
|
||||
value
|
||||
}
|
||||
)
|
||||
private fun Context.getContent(): Any? {
|
||||
if (bytes != null) {
|
||||
return bytes
|
||||
}
|
||||
var text = contentView.text.toString()
|
||||
if (text.isEmpty()) {
|
||||
toast(R.string.error_no_content)
|
||||
return null
|
||||
}
|
||||
if (!unescapeCheckBox.isChecked) {
|
||||
return text
|
||||
}
|
||||
try {
|
||||
text = text.unescape()
|
||||
} catch (e: IllegalArgumentException) {
|
||||
toast(e.message ?: "Invalid escape sequence")
|
||||
return null
|
||||
}
|
||||
// Return a ByteArray if there were escape sequences for
|
||||
// non-printable characters.
|
||||
return if (text.any { it.code < 32 && it.code !in setOf(9, 10, 13) }) {
|
||||
// Do not expand UTF-8 sequences in binary data.
|
||||
text.toByteArray(Charsets.ISO_8859_1)
|
||||
} else {
|
||||
text
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private fun setEncodeByteArray() {
|
||||
@@ -364,16 +311,41 @@ class EncodeFragment : Fragment() {
|
||||
unescapeCheckBox.isEnabled = false
|
||||
}
|
||||
|
||||
private fun getExtrasFromDeeplink(): Bundle? {
|
||||
val data = intent?.dataString ?: return null
|
||||
if (!isEncodeDeeplink(data)) {
|
||||
return null
|
||||
}
|
||||
val uri = data.toUri()
|
||||
return Bundle().apply {
|
||||
uri.getQueryParameter("content")?.let {
|
||||
putString(CONTENT_TEXT, it)
|
||||
}
|
||||
uri.getQueryParameter("format")?.let {
|
||||
putString(FORMAT, it)
|
||||
}
|
||||
putBoolean(
|
||||
EXECUTE,
|
||||
uri.getQueryParameter("execute").let {
|
||||
it == "" || it?.toBoolean() == true
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val CONTENT_TEXT = "content_text"
|
||||
private const val CONTENT_RAW = "content_raw"
|
||||
private const val FORMAT = "format"
|
||||
private const val EXECUTE = "execute"
|
||||
private const val PICK_FILE_RESULT_CODE = 1
|
||||
|
||||
fun <T> newInstance(
|
||||
fun <T> newIntent(
|
||||
context: Context,
|
||||
content: T? = null,
|
||||
format: String? = null
|
||||
): Fragment {
|
||||
format: String? = null,
|
||||
execute: Boolean = false
|
||||
): Intent {
|
||||
val args = Bundle()
|
||||
content?.let {
|
||||
when (content) {
|
||||
@@ -383,11 +355,12 @@ class EncodeFragment : Fragment() {
|
||||
"content must be a String or a ByteArray"
|
||||
)
|
||||
}
|
||||
args.putBoolean(EXECUTE, execute)
|
||||
}
|
||||
format?.let { args.putString(FORMAT, it) }
|
||||
val fragment = EncodeFragment()
|
||||
fragment.arguments = args
|
||||
return fragment
|
||||
return Intent(context, EncodeActivity::class.java).apply {
|
||||
putExtras(args)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -408,30 +381,39 @@ private fun BarcodeFormat.unpackEcLevel(packed: Int) =
|
||||
(packed shr ecLevelShift()) and 15
|
||||
|
||||
private fun BarcodeFormat.ecLevelShift() = when (this) {
|
||||
BarcodeFormat.AZTEC -> 0
|
||||
BarcodeFormat.QR_CODE -> 4
|
||||
BarcodeFormat.PDF_417 -> 8
|
||||
BarcodeFormat.Aztec -> 0
|
||||
BarcodeFormat.QRCode -> 4
|
||||
BarcodeFormat.MicroQRCode -> 3
|
||||
BarcodeFormat.RMQRCode -> 2
|
||||
BarcodeFormat.PDF417 -> 8
|
||||
else -> throw IllegalArgumentException("$this does not have error levels")
|
||||
}
|
||||
|
||||
private fun BarcodeFormat.canBeInverted() = when (this) {
|
||||
BarcodeFormat.AZTEC,
|
||||
BarcodeFormat.DATA_MATRIX,
|
||||
BarcodeFormat.QR_CODE -> true
|
||||
BarcodeFormat.Aztec,
|
||||
BarcodeFormat.DataMatrix,
|
||||
BarcodeFormat.QRCode,
|
||||
BarcodeFormat.MicroQRCode,
|
||||
BarcodeFormat.RMQRCode -> true
|
||||
|
||||
else -> false
|
||||
}
|
||||
|
||||
private fun String.toFormat(default: BarcodeFormat = BarcodeFormat.QR_CODE): BarcodeFormat = try {
|
||||
private fun String.toFormat(
|
||||
default: BarcodeFormat = BarcodeFormat.QRCode
|
||||
): BarcodeFormat = try {
|
||||
BarcodeFormat.valueOf(this)
|
||||
} catch (_: IllegalArgumentException) {
|
||||
default
|
||||
}
|
||||
|
||||
private fun BarcodeFormat.getErrorCorrectionLevel(position: Int) = when (this) {
|
||||
BarcodeFormat.AZTEC -> position
|
||||
BarcodeFormat.QR_CODE -> (position + 1) * 2
|
||||
BarcodeFormat.PDF_417 -> position
|
||||
BarcodeFormat.Aztec -> position
|
||||
BarcodeFormat.QRCode,
|
||||
BarcodeFormat.MicroQRCode -> (position + 1) * 2
|
||||
|
||||
BarcodeFormat.RMQRCode -> (position + 1) * 4
|
||||
BarcodeFormat.PDF417 -> position
|
||||
else -> 0
|
||||
}.coerceIn(0, 8)
|
||||
|
||||
@@ -446,8 +428,6 @@ private fun Spinner.setEntries(resId: Int) = ArrayAdapter.createFromResource(
|
||||
adapter = aa
|
||||
}
|
||||
|
||||
private fun getSize(power: Int) = 128 * (power + 1)
|
||||
|
||||
private fun InputStream.readBytesMax(max: Int): ByteArray {
|
||||
var offset = 0
|
||||
var remaining = min(available(), max)
|
||||
+89
-82
@@ -1,4 +1,4 @@
|
||||
package de.markusfisch.android.binaryeye.fragment
|
||||
package de.markusfisch.android.binaryeye.activity
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.AlertDialog
|
||||
@@ -8,31 +8,47 @@ import android.database.Cursor
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Parcelable
|
||||
import android.support.v4.app.ActivityCompat
|
||||
import android.support.v4.app.Fragment
|
||||
import android.support.v4.content.ContextCompat
|
||||
import android.support.v7.app.AppCompatActivity
|
||||
import android.support.v7.view.ActionMode
|
||||
import android.support.v7.widget.SearchView
|
||||
import android.support.v7.widget.SwitchCompat
|
||||
import android.view.*
|
||||
import android.view.LayoutInflater
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.EditText
|
||||
import android.widget.ListView
|
||||
import androidx.appcompat.view.ActionMode
|
||||
import androidx.appcompat.widget.SearchView
|
||||
import androidx.appcompat.widget.SwitchCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.adapter.ScansAdapter
|
||||
import de.markusfisch.android.binaryeye.app.*
|
||||
import de.markusfisch.android.binaryeye.app.alertDialog
|
||||
import de.markusfisch.android.binaryeye.app.db
|
||||
import de.markusfisch.android.binaryeye.app.hasWritePermission
|
||||
import de.markusfisch.android.binaryeye.app.prefs
|
||||
import de.markusfisch.android.binaryeye.content.copyToClipboard
|
||||
import de.markusfisch.android.binaryeye.content.shareAsFile
|
||||
import de.markusfisch.android.binaryeye.content.shareText
|
||||
import de.markusfisch.android.binaryeye.content.wipeLastShareFile
|
||||
import de.markusfisch.android.binaryeye.database.*
|
||||
import de.markusfisch.android.binaryeye.database.Database
|
||||
import de.markusfisch.android.binaryeye.database.exportCsv
|
||||
import de.markusfisch.android.binaryeye.database.exportDatabase
|
||||
import de.markusfisch.android.binaryeye.database.exportJson
|
||||
import de.markusfisch.android.binaryeye.io.askForFileName
|
||||
import de.markusfisch.android.binaryeye.io.toSaveResult
|
||||
import de.markusfisch.android.binaryeye.view.*
|
||||
import de.markusfisch.android.binaryeye.view.lockStatusBarColor
|
||||
import de.markusfisch.android.binaryeye.view.setPaddingFromWindowInsets
|
||||
import de.markusfisch.android.binaryeye.view.systemBarListViewScrollListener
|
||||
import de.markusfisch.android.binaryeye.view.unlockStatusBarColor
|
||||
import de.markusfisch.android.binaryeye.view.useVisibility
|
||||
import de.markusfisch.android.binaryeye.widget.toast
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class HistoryFragment : Fragment() {
|
||||
class HistoryActivity : ScreenActivity() {
|
||||
private lateinit var useHistorySwitch: SwitchCompat
|
||||
private lateinit var listView: ListView
|
||||
private lateinit var fab: View
|
||||
@@ -49,9 +65,11 @@ class HistoryFragment : Fragment() {
|
||||
R.menu.fragment_history_edit,
|
||||
menu
|
||||
)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
// SDK35+ doesn't draw a status bar background anymore.
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.VANILLA_ICE_CREAM) {
|
||||
lockStatusBarColor()
|
||||
val ac = activity ?: return false
|
||||
val ac = this@HistoryActivity
|
||||
@Suppress("DEPRECATION")
|
||||
ac.window.statusBarColor = ContextCompat.getColor(
|
||||
ac,
|
||||
R.color.accent_dark
|
||||
@@ -71,7 +89,7 @@ class HistoryFragment : Fragment() {
|
||||
mode: ActionMode,
|
||||
item: MenuItem
|
||||
): Boolean {
|
||||
val ac = activity ?: return false
|
||||
val ac = this@HistoryActivity
|
||||
return when (item.itemId) {
|
||||
R.id.copy_scan -> {
|
||||
scansAdapter?.getSelectedContent("\n")?.let {
|
||||
@@ -82,7 +100,7 @@ class HistoryFragment : Fragment() {
|
||||
true
|
||||
}
|
||||
|
||||
R.id.edit_scan -> {
|
||||
R.id.edit_label -> {
|
||||
scansAdapter?.forSelection { id, position ->
|
||||
ac.askForName(
|
||||
id,
|
||||
@@ -123,36 +141,28 @@ class HistoryFragment : Fragment() {
|
||||
|
||||
override fun onCreate(state: Bundle?) {
|
||||
super.onCreate(state)
|
||||
setHasOptionsMenu(true)
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
state: Bundle?
|
||||
): View? {
|
||||
val ac = activity ?: return null
|
||||
ac.setTitle(R.string.history)
|
||||
|
||||
val view = inflater.inflate(
|
||||
R.layout.fragment_history,
|
||||
container,
|
||||
false
|
||||
)
|
||||
setTitle(R.string.history)
|
||||
val frame = findViewById(R.id.content_frame) as ViewGroup
|
||||
val view = layoutInflater.inflate(R.layout.fragment_history, frame, false)
|
||||
frame.addView(view)
|
||||
|
||||
useHistorySwitch = view.findViewById(
|
||||
R.id.use_history
|
||||
) as SwitchCompat
|
||||
)
|
||||
initHistorySwitch(useHistorySwitch)
|
||||
|
||||
listView = view.findViewById(R.id.scans)
|
||||
listView.setOnItemClickListener { _, _, _, id ->
|
||||
showScan(id)
|
||||
listView.setOnItemClickListener { _, v, position, id ->
|
||||
if (actionMode != null) {
|
||||
scansAdapter?.select(v, id, position)
|
||||
} else {
|
||||
showScan(id)
|
||||
}
|
||||
}
|
||||
listView.setOnItemLongClickListener { _, v, position, id ->
|
||||
scansAdapter?.select(v, id, position)
|
||||
if (actionMode == null && ac is AppCompatActivity) {
|
||||
actionMode = ac.delegate.startSupportActionMode(
|
||||
if (actionMode == null) {
|
||||
actionMode = delegate.startSupportActionMode(
|
||||
actionModeCallback
|
||||
)
|
||||
}
|
||||
@@ -167,12 +177,8 @@ class HistoryFragment : Fragment() {
|
||||
|
||||
progressView = view.findViewById(R.id.progress_view)
|
||||
|
||||
(view.findViewById(R.id.inset_layout) as View).setPaddingFromWindowInsets()
|
||||
view.findViewById<View>(R.id.inset_layout).setPaddingFromWindowInsets()
|
||||
listView.setPaddingFromWindowInsets()
|
||||
|
||||
update()
|
||||
|
||||
return view
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
@@ -182,6 +188,11 @@ class HistoryFragment : Fragment() {
|
||||
wipeLastShareFile()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
update()
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
listViewState = listView.onSaveInstanceState()
|
||||
@@ -206,35 +217,31 @@ class HistoryFragment : Fragment() {
|
||||
|
||||
private fun initSearchView(item: MenuItem?) {
|
||||
item ?: return
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) {
|
||||
item.isVisible = false
|
||||
} else {
|
||||
val ac = activity ?: return
|
||||
val searchView = item.actionView as SearchView
|
||||
val searchManager = ac.getSystemService(
|
||||
Context.SEARCH_SERVICE
|
||||
) as SearchManager
|
||||
searchView.setSearchableInfo(
|
||||
searchManager.getSearchableInfo(ac.componentName)
|
||||
)
|
||||
searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
|
||||
override fun onQueryTextSubmit(query: String): Boolean {
|
||||
update(query)
|
||||
return false
|
||||
}
|
||||
val ac = this
|
||||
val searchView = item.actionView as SearchView
|
||||
val searchManager = ac.getSystemService(
|
||||
Context.SEARCH_SERVICE
|
||||
) as SearchManager
|
||||
searchView.setSearchableInfo(
|
||||
searchManager.getSearchableInfo(ac.componentName)
|
||||
)
|
||||
searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
|
||||
override fun onQueryTextSubmit(query: String): Boolean {
|
||||
update(query)
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onQueryTextChange(query: String): Boolean {
|
||||
update(query)
|
||||
return false
|
||||
}
|
||||
})
|
||||
}
|
||||
override fun onQueryTextChange(query: String): Boolean {
|
||||
update(query)
|
||||
return false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
return when (item.itemId) {
|
||||
R.id.clear -> {
|
||||
context.askToRemoveScans()
|
||||
this.askToRemoveScans()
|
||||
true
|
||||
}
|
||||
|
||||
@@ -244,7 +251,7 @@ class HistoryFragment : Fragment() {
|
||||
}
|
||||
|
||||
R.id.share_as_file -> {
|
||||
context.pickListSeparatorAndShare(true)
|
||||
this.pickListSeparatorAndShare(true)
|
||||
true
|
||||
}
|
||||
|
||||
@@ -271,13 +278,13 @@ class HistoryFragment : Fragment() {
|
||||
scope.launch {
|
||||
val cursor = db.getScans(filter)
|
||||
withContext(Dispatchers.Main) {
|
||||
val ac = activity ?: return@withContext
|
||||
val hasScans = cursor != null && cursor.count > 0
|
||||
val ac = this@HistoryActivity
|
||||
val hasScans = cursor.count > 0
|
||||
if (filter == null) {
|
||||
if (!hasScans) {
|
||||
listView.emptyView = useHistorySwitch
|
||||
}
|
||||
ActivityCompat.invalidateOptionsMenu(ac)
|
||||
ac.invalidateOptionsMenu()
|
||||
}
|
||||
enableMenuItems(hasScans)
|
||||
fab.visibility = if (hasScans) {
|
||||
@@ -285,7 +292,7 @@ class HistoryFragment : Fragment() {
|
||||
} else {
|
||||
View.GONE
|
||||
}
|
||||
cursor?.let { cursor ->
|
||||
cursor.let { cursor ->
|
||||
// Close previous cursor.
|
||||
scansAdapter?.also { it.changeCursor(null) }
|
||||
scansAdapter = ScansAdapter(ac, cursor)
|
||||
@@ -315,8 +322,8 @@ class HistoryFragment : Fragment() {
|
||||
private fun showScan(id: Long) = db.getScan(id)?.also { scan ->
|
||||
closeActionMode()
|
||||
try {
|
||||
fragmentManager?.addFragment(DecodeFragment.newInstance(scan))
|
||||
} catch (e: IllegalArgumentException) {
|
||||
startActivity(DecodeActivity.newIntent(this, scan))
|
||||
} catch (_: IllegalArgumentException) {
|
||||
// Ignore, can never happen.
|
||||
}
|
||||
}
|
||||
@@ -389,7 +396,7 @@ class HistoryFragment : Fragment() {
|
||||
|
||||
private fun askToExportToFile() {
|
||||
scope.launch {
|
||||
val ac = activity ?: return@launch
|
||||
val ac = this@HistoryActivity
|
||||
progressView.useVisibility {
|
||||
// Write permission is only required before Android Q.
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q &&
|
||||
@@ -417,12 +424,12 @@ class HistoryFragment : Fragment() {
|
||||
} ?: return@useVisibility
|
||||
val message = when (delimiter) {
|
||||
"db" -> ac.exportDatabase(name)
|
||||
else -> db.getScansDetailed(filter)?.use {
|
||||
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)
|
||||
@@ -450,23 +457,23 @@ class HistoryFragment : Fragment() {
|
||||
db.getScansDetailed(selectedIds.toLongArray())
|
||||
} else {
|
||||
db.getScansDetailed(filter)
|
||||
}?.use { cursor ->
|
||||
}.use { cursor ->
|
||||
val details = format.split(":")
|
||||
when (details[0]) {
|
||||
"text" -> Pair(cursor.exportText(details[1]), "txt")
|
||||
"csv" -> Pair(cursor.exportCsv(details[1]), "csv")
|
||||
else -> Pair(cursor.exportJson(), "json")
|
||||
}
|
||||
}?.let { (text, ext) ->
|
||||
}.let { (text, ext) ->
|
||||
text?.let {
|
||||
withContext(Dispatchers.Main) {
|
||||
if (asFile) {
|
||||
context.shareAsFile(
|
||||
this@HistoryActivity.shareAsFile(
|
||||
it,
|
||||
String.format("scans.%s", ext)
|
||||
)
|
||||
} else {
|
||||
context.shareText(it)
|
||||
this@HistoryActivity.shareText(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -477,7 +484,7 @@ class HistoryFragment : Fragment() {
|
||||
|
||||
private fun Cursor.exportText(separator: String): String {
|
||||
val sb = StringBuilder()
|
||||
val contentIndex = getColumnIndex(Database.SCANS_CONTENT)
|
||||
val contentIndex = getColumnIndex(Database.SCANS_TEXT)
|
||||
if (contentIndex > -1 && moveToFirst()) {
|
||||
do {
|
||||
val content = getString(contentIndex)
|
||||
@@ -1,139 +0,0 @@
|
||||
package de.markusfisch.android.binaryeye.activity
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.support.v4.app.Fragment
|
||||
import android.support.v7.app.AppCompatActivity
|
||||
import android.support.v7.widget.Toolbar
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.app.PERMISSION_LOCATION
|
||||
import de.markusfisch.android.binaryeye.app.PERMISSION_WRITE
|
||||
import de.markusfisch.android.binaryeye.app.applyLocale
|
||||
import de.markusfisch.android.binaryeye.app.permissionGrantedCallback
|
||||
import de.markusfisch.android.binaryeye.app.prefs
|
||||
import de.markusfisch.android.binaryeye.app.setFragment
|
||||
import de.markusfisch.android.binaryeye.database.Scan
|
||||
import de.markusfisch.android.binaryeye.fragment.DecodeFragment
|
||||
import de.markusfisch.android.binaryeye.fragment.EncodeFragment
|
||||
import de.markusfisch.android.binaryeye.fragment.HistoryFragment
|
||||
import de.markusfisch.android.binaryeye.fragment.PreferencesFragment
|
||||
import de.markusfisch.android.binaryeye.view.colorSystemAndToolBars
|
||||
import de.markusfisch.android.binaryeye.view.initSystemBars
|
||||
import de.markusfisch.android.binaryeye.view.recordToolbarHeight
|
||||
|
||||
class MainActivity : AppCompatActivity() {
|
||||
override fun onRequestPermissionsResult(
|
||||
requestCode: Int,
|
||||
permissions: Array<String>,
|
||||
grantResults: IntArray
|
||||
) {
|
||||
when (requestCode) {
|
||||
PERMISSION_LOCATION, PERMISSION_WRITE -> if (grantResults.isNotEmpty() &&
|
||||
grantResults[0] == PackageManager.PERMISSION_GRANTED
|
||||
) {
|
||||
permissionGrantedCallback?.invoke()
|
||||
permissionGrantedCallback = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSupportNavigateUp(): Boolean {
|
||||
val fm = supportFragmentManager
|
||||
if (fm != null && fm.backStackEntryCount > 0) {
|
||||
fm.popBackStack()
|
||||
} else {
|
||||
finish()
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
override fun attachBaseContext(base: Context?) {
|
||||
base?.applyLocale(prefs.customLocale)
|
||||
super.attachBaseContext(base)
|
||||
}
|
||||
|
||||
override fun onCreate(state: Bundle?) {
|
||||
super.onCreate(state)
|
||||
setContentView(R.layout.activity_main)
|
||||
|
||||
initSystemBars(this)
|
||||
val toolbar = findViewById(R.id.toolbar) as Toolbar
|
||||
recordToolbarHeight(toolbar)
|
||||
setSupportActionBar(toolbar)
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
|
||||
supportFragmentManager.addOnBackStackChangedListener {
|
||||
colorSystemAndToolBars(this@MainActivity)
|
||||
}
|
||||
|
||||
if (state == null) {
|
||||
supportFragmentManager?.setFragment(getFragmentForIntent(intent))
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val PREFERENCES = "preferences"
|
||||
private const val HISTORY = "history"
|
||||
private const val ENCODE = "encode"
|
||||
const val DECODED = "decoded"
|
||||
|
||||
private fun getFragmentForIntent(intent: Intent?): Fragment {
|
||||
intent ?: return PreferencesFragment()
|
||||
return when {
|
||||
intent.hasExtra(PREFERENCES) -> PreferencesFragment()
|
||||
intent.hasExtra(HISTORY) -> HistoryFragment()
|
||||
intent.hasExtra(ENCODE) -> EncodeFragment.newInstance(
|
||||
intent.getStringExtra(ENCODE)
|
||||
)
|
||||
|
||||
intent.hasExtra(DECODED) -> DecodeFragment.newInstance(
|
||||
intent.getParcelableExtra(DECODED)!!
|
||||
)
|
||||
|
||||
else -> PreferencesFragment()
|
||||
}
|
||||
}
|
||||
|
||||
fun getPreferencesIntent(context: Context): Intent {
|
||||
val intent = Intent(context, MainActivity::class.java)
|
||||
intent.putExtra(PREFERENCES, true)
|
||||
return intent
|
||||
}
|
||||
|
||||
fun getHistoryIntent(context: Context): Intent {
|
||||
val intent = Intent(context, MainActivity::class.java)
|
||||
intent.putExtra(HISTORY, true)
|
||||
return intent
|
||||
}
|
||||
|
||||
fun getEncodeIntent(
|
||||
context: Context,
|
||||
text: String? = "",
|
||||
isExternal: Boolean = false
|
||||
): Intent {
|
||||
val intent = Intent(context, MainActivity::class.java)
|
||||
intent.putExtra(ENCODE, text)
|
||||
if (isExternal) {
|
||||
val flagActivityClearTask =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
|
||||
Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||
} else 0
|
||||
intent.addFlags(
|
||||
Intent.FLAG_ACTIVITY_NO_HISTORY or
|
||||
flagActivityClearTask or
|
||||
Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
)
|
||||
}
|
||||
return intent
|
||||
}
|
||||
|
||||
fun getDecodeIntent(context: Context, scan: Scan): Intent {
|
||||
val intent = Intent(context, MainActivity::class.java)
|
||||
intent.putExtra(DECODED, scan)
|
||||
return intent
|
||||
}
|
||||
}
|
||||
}
|
||||
+18
-22
@@ -1,41 +1,39 @@
|
||||
package de.markusfisch.android.binaryeye.fragment
|
||||
package de.markusfisch.android.binaryeye.activity
|
||||
|
||||
import android.content.Context
|
||||
import android.net.wifi.WifiManager
|
||||
import android.net.wifi.WifiNetworkSuggestion
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.support.annotation.RequiresApi
|
||||
import android.support.v4.app.Fragment
|
||||
import android.view.*
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.ListView
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.core.util.size
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.view.setPaddingFromWindowInsets
|
||||
import de.markusfisch.android.binaryeye.view.systemBarListViewScrollListener
|
||||
import de.markusfisch.android.binaryeye.widget.toast
|
||||
|
||||
class NetworkSuggestionsFragment : Fragment() {
|
||||
class NetworkSuggestionsActivity : ScreenActivity() {
|
||||
@RequiresApi(Build.VERSION_CODES.R)
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
state: Bundle?
|
||||
): View? {
|
||||
val ac = activity ?: return null
|
||||
ac.setTitle(R.string.network_suggestions)
|
||||
|
||||
val view = inflater.inflate(
|
||||
override fun onCreate(state: Bundle?) {
|
||||
super.onCreate(state)
|
||||
setTitle(R.string.network_suggestions)
|
||||
val frame = findViewById(R.id.content_frame) as ViewGroup
|
||||
val view = layoutInflater.inflate(
|
||||
R.layout.fragment_network_suggestions,
|
||||
container,
|
||||
frame,
|
||||
false
|
||||
)
|
||||
frame.addView(view)
|
||||
|
||||
val wm = ac.applicationContext.getSystemService(
|
||||
val wm = applicationContext.getSystemService(
|
||||
Context.WIFI_SERVICE
|
||||
) as WifiManager
|
||||
val suggestionArrayAdapter = ArrayAdapter(
|
||||
ac,
|
||||
this,
|
||||
android.R.layout.simple_list_item_checked,
|
||||
wm.networkSuggestions.map {
|
||||
Suggestion(
|
||||
@@ -49,14 +47,14 @@ class NetworkSuggestionsFragment : Fragment() {
|
||||
listView.emptyView = view.findViewById(R.id.no_suggestions)
|
||||
listView.adapter = suggestionArrayAdapter
|
||||
listView.setOnScrollListener(systemBarListViewScrollListener)
|
||||
(view.findViewById(R.id.inset_layout) as View).setPaddingFromWindowInsets()
|
||||
view.findViewById<View>(R.id.inset_layout).setPaddingFromWindowInsets()
|
||||
listView.setPaddingFromWindowInsets()
|
||||
|
||||
view.findViewById<View>(R.id.remove).setOnClickListener {
|
||||
val removeList = ArrayList<WifiNetworkSuggestion>()
|
||||
val removeFromAdapter = ArrayList<Suggestion>()
|
||||
val checked = listView.checkedItemPositions
|
||||
val size = checked.size()
|
||||
val size = checked.size
|
||||
for (i in 0 until size) {
|
||||
if (checked.valueAt(i)) {
|
||||
val pos = checked.keyAt(i)
|
||||
@@ -74,13 +72,11 @@ class NetworkSuggestionsFragment : Fragment() {
|
||||
}
|
||||
wm.removeNetworkSuggestions(removeList)
|
||||
} else {
|
||||
ac.toast(
|
||||
toast(
|
||||
R.string.clear_network_suggestions_nothing_to_remove
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return view
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,12 +7,12 @@ import android.graphics.Matrix
|
||||
import android.graphics.Rect
|
||||
import android.graphics.RectF
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Parcelable
|
||||
import android.support.v7.app.AppCompatActivity
|
||||
import android.support.v7.widget.Toolbar
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.app.applyLocale
|
||||
import de.markusfisch.android.binaryeye.app.prefs
|
||||
@@ -22,8 +22,7 @@ import de.markusfisch.android.binaryeye.graphics.loadImageUri
|
||||
import de.markusfisch.android.binaryeye.graphics.mapPosition
|
||||
import de.markusfisch.android.binaryeye.media.releaseToneGenerators
|
||||
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.initBars
|
||||
import de.markusfisch.android.binaryeye.view.scanFeedback
|
||||
import de.markusfisch.android.binaryeye.view.setPaddingFromWindowInsets
|
||||
import de.markusfisch.android.binaryeye.widget.CropImageView
|
||||
@@ -55,10 +54,9 @@ class PickActivity : AppCompatActivity() {
|
||||
formats = prefs.barcodeFormats.toFormatSet()
|
||||
)
|
||||
|
||||
private lateinit var cropImageView: CropImageView
|
||||
private lateinit var detectorView: DetectorView
|
||||
private lateinit var freeRotationItem: MenuItem
|
||||
|
||||
private var cropImageView: CropImageView? = null
|
||||
private var detectorView: DetectorView? = null
|
||||
private var freeRotationItem: MenuItem? = null
|
||||
private var result: Result? = null
|
||||
|
||||
override fun attachBaseContext(base: Context?) {
|
||||
@@ -74,11 +72,7 @@ class PickActivity : AppCompatActivity() {
|
||||
// locale.
|
||||
setTitle(R.string.pick_code_to_scan)
|
||||
|
||||
initSystemBars(this)
|
||||
val toolbar = findViewById(R.id.toolbar) as Toolbar
|
||||
recordToolbarHeight(toolbar)
|
||||
setSupportActionBar(toolbar)
|
||||
|
||||
initBars()
|
||||
supportFragmentManager.addOnBackStackChangedListener {
|
||||
colorSystemAndToolBars(this@PickActivity)
|
||||
}
|
||||
@@ -87,25 +81,36 @@ class PickActivity : AppCompatActivity() {
|
||||
if (bitmap == null) {
|
||||
applicationContext.toast(R.string.error_no_content)
|
||||
finish()
|
||||
// This is why cropImageView and detectorView aren't lateinit.
|
||||
// If they were, a kotlin.UninitializedPropertyAccessException
|
||||
// would happen here.
|
||||
return
|
||||
}
|
||||
|
||||
cropImageView = findViewById(R.id.image) as CropImageView
|
||||
cropImageView.restrictTranslation = false
|
||||
cropImageView.freeRotation = prefs.freeRotation
|
||||
cropImageView.setImageBitmap(bitmap)
|
||||
cropImageView.onScan = {
|
||||
scanWithinBounds(bitmap)
|
||||
cropImageView = (findViewById(R.id.image) as CropImageView).apply {
|
||||
restrictTranslation = false
|
||||
freeRotation = prefs.freeRotation
|
||||
setImageBitmap(bitmap)
|
||||
onScan = {
|
||||
scanWithinBounds(bitmap)
|
||||
}
|
||||
runAfterLayout = {
|
||||
minWidth = min(
|
||||
minWidth / 2f,
|
||||
max(bitmap.width, bitmap.height).toFloat()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
detectorView = findViewById(R.id.detector_view) as DetectorView
|
||||
detectorView.onRoiChanged = {
|
||||
scanWithinBounds(bitmap)
|
||||
detectorView = (findViewById(R.id.detector_view) as DetectorView).apply {
|
||||
onRoiChanged = {
|
||||
scanWithinBounds(bitmap)
|
||||
}
|
||||
setPaddingFromWindowInsets()
|
||||
cropHandleName = "picker_crop_handle"
|
||||
}
|
||||
detectorView.setPaddingFromWindowInsets()
|
||||
detectorView.restoreCropHandlePos()
|
||||
|
||||
findViewById(R.id.scan).setOnClickListener {
|
||||
findViewById<View>(R.id.scan).setOnClickListener {
|
||||
showResult()
|
||||
}
|
||||
}
|
||||
@@ -125,35 +130,42 @@ class PickActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
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())
|
||||
val dv = detectorView ?: return
|
||||
val civ = cropImageView ?: return
|
||||
val viewRoi = Rect(
|
||||
if (dv.roi.width() < 1) {
|
||||
civ.getBoundsRect()
|
||||
} else {
|
||||
dv.roi
|
||||
}
|
||||
)
|
||||
matrix.apply {
|
||||
setScale(
|
||||
croppedInView.width().toFloat() / cropped.width,
|
||||
croppedInView.height().toFloat() / cropped.height
|
||||
)
|
||||
postTranslate(
|
||||
croppedInView.left.toFloat(),
|
||||
croppedInView.top.toFloat()
|
||||
)
|
||||
}
|
||||
val mappedRect = RectF(civ.mappedRect)
|
||||
val imageRotation = civ.imageRotation
|
||||
val pivotX = civ.pivotX
|
||||
val pivotY = civ.pivotY
|
||||
scope.launch {
|
||||
val cropped = bitmap.crop(
|
||||
getNormalizedRoi(mappedRect, viewRoi),
|
||||
imageRotation,
|
||||
pivotX,
|
||||
pivotY
|
||||
) ?: return@launch
|
||||
val croppedInView = Rect(
|
||||
max(viewRoi.left, mappedRect.left.roundToInt()),
|
||||
max(viewRoi.top, mappedRect.top.roundToInt()),
|
||||
min(viewRoi.right, mappedRect.right.roundToInt()),
|
||||
min(viewRoi.bottom, mappedRect.bottom.roundToInt())
|
||||
)
|
||||
matrix.apply {
|
||||
setScale(
|
||||
croppedInView.width().toFloat() / cropped.width,
|
||||
croppedInView.height().toFloat() / cropped.height
|
||||
)
|
||||
postTranslate(
|
||||
croppedInView.left.toFloat(),
|
||||
croppedInView.top.toFloat()
|
||||
)
|
||||
}
|
||||
cropped.decode()?.first()?.let {
|
||||
withContext(Dispatchers.Main) {
|
||||
if (isFinishing) {
|
||||
@@ -161,10 +173,10 @@ class PickActivity : AppCompatActivity() {
|
||||
}
|
||||
result = it
|
||||
scanFeedback()
|
||||
detectorView.update(
|
||||
dv.update(
|
||||
matrix.mapPosition(
|
||||
it.position,
|
||||
detectorView.coordinates
|
||||
dv.coordinates
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -194,7 +206,7 @@ class PickActivity : AppCompatActivity() {
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
detectorView.saveCropHandlePos()
|
||||
detectorView?.storeCropHandlePos()
|
||||
parentJob.cancel()
|
||||
releaseToneGenerators()
|
||||
}
|
||||
@@ -216,8 +228,8 @@ class PickActivity : AppCompatActivity() {
|
||||
|
||||
R.id.toggle_free -> {
|
||||
prefs.freeRotation = prefs.freeRotation xor true
|
||||
cropImageView.freeRotation = prefs.freeRotation
|
||||
freeRotationItem.updateFreeRotationIcon()
|
||||
cropImageView?.freeRotation = prefs.freeRotation
|
||||
freeRotationItem?.updateFreeRotationIcon()
|
||||
true
|
||||
}
|
||||
|
||||
@@ -236,13 +248,12 @@ class PickActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
private fun rotateClockwise() {
|
||||
cropImageView.imageRotation = cropImageView.imageRotation + 90 % 360
|
||||
val civ = cropImageView ?: return
|
||||
civ.imageRotation = (civ.imageRotation + 90) % 360
|
||||
}
|
||||
|
||||
private fun loadSentImage(intent: Intent): Bitmap? {
|
||||
val uri = intent.getParcelableExtra<Parcelable>(
|
||||
Intent.EXTRA_STREAM
|
||||
) as? Uri ?: return null
|
||||
val uri = intent.getUriExtra() ?: return null
|
||||
return contentResolver.loadImageUri(uri)
|
||||
}
|
||||
|
||||
@@ -272,3 +283,12 @@ private fun getNormalizedRoi(imageRect: RectF, roi: Rect): RectF {
|
||||
(roi.bottom - imageRect.top) / h
|
||||
)
|
||||
}
|
||||
|
||||
private fun Intent.getUriExtra(): Uri? = if (
|
||||
Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU
|
||||
) {
|
||||
@Suppress("DEPRECATION")
|
||||
getParcelableExtra(Intent.EXTRA_STREAM)
|
||||
} else {
|
||||
getParcelableExtra(Intent.EXTRA_STREAM, Uri::class.java)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
package de.markusfisch.android.binaryeye.activity
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.app.applyLocale
|
||||
import de.markusfisch.android.binaryeye.app.prefs
|
||||
import de.markusfisch.android.binaryeye.fragment.PreferencesFragment
|
||||
import de.markusfisch.android.binaryeye.view.initBars
|
||||
|
||||
class PreferencesActivity : AppCompatActivity() {
|
||||
override fun attachBaseContext(base: Context?) {
|
||||
base?.applyLocale(prefs.customLocale)
|
||||
super.attachBaseContext(base)
|
||||
}
|
||||
|
||||
override fun onCreate(state: Bundle?) {
|
||||
super.onCreate(state)
|
||||
setContentView(R.layout.activity_main)
|
||||
initBars()
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
if (state == null) {
|
||||
supportFragmentManager.beginTransaction()
|
||||
.replace(R.id.content_frame, PreferencesFragment())
|
||||
.commit()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSupportNavigateUp(): Boolean {
|
||||
finish()
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
package de.markusfisch.android.binaryeye.activity
|
||||
|
||||
import android.app.AlertDialog
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.EditText
|
||||
import android.widget.ListView
|
||||
import android.widget.TextView
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.app.prefs
|
||||
import de.markusfisch.android.binaryeye.view.setPaddingFromWindowInsets
|
||||
import de.markusfisch.android.binaryeye.view.systemBarListViewScrollListener
|
||||
import de.markusfisch.android.binaryeye.widget.toast
|
||||
|
||||
class ProfilesActivity : ScreenActivity() {
|
||||
private val profiles = ArrayList<ProfileItem>()
|
||||
|
||||
private lateinit var adapter: ProfilesAdapter
|
||||
|
||||
override fun onCreate(state: Bundle?) {
|
||||
super.onCreate(state)
|
||||
setTitle(R.string.profile)
|
||||
val frame = findViewById(R.id.content_frame) as ViewGroup
|
||||
val view = layoutInflater.inflate(
|
||||
R.layout.fragment_profiles,
|
||||
frame,
|
||||
false
|
||||
)
|
||||
frame.addView(view)
|
||||
|
||||
refreshProfiles()
|
||||
|
||||
val listView = view.findViewById<ListView>(R.id.profiles)
|
||||
listView.emptyView = view.findViewById(R.id.no_profiles)
|
||||
adapter = ProfilesAdapter(this, profiles)
|
||||
listView.adapter = adapter
|
||||
listView.setOnScrollListener(systemBarListViewScrollListener)
|
||||
listView.setOnItemClickListener { _, _, position, _ ->
|
||||
selectProfile(position)
|
||||
}
|
||||
listView.setOnItemLongClickListener { _, _, position, _ ->
|
||||
confirmRemoveProfile(position)
|
||||
true
|
||||
}
|
||||
|
||||
view.findViewById<View>(R.id.add).setOnClickListener {
|
||||
addProfile()
|
||||
}
|
||||
|
||||
view.findViewById<View>(R.id.inset_layout).setPaddingFromWindowInsets()
|
||||
listView.setPaddingFromWindowInsets()
|
||||
}
|
||||
|
||||
private fun refreshProfiles() {
|
||||
profiles.clear()
|
||||
profiles.add(
|
||||
ProfileItem(
|
||||
null,
|
||||
getString(R.string.profile_default)
|
||||
)
|
||||
)
|
||||
prefs.profiles.forEach { name ->
|
||||
profiles.add(ProfileItem(name, name))
|
||||
}
|
||||
if (this::adapter.isInitialized) {
|
||||
adapter.notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
|
||||
private fun selectProfile(position: Int) {
|
||||
val profile = profiles[position].name
|
||||
if (prefs.profile == profile) {
|
||||
return
|
||||
}
|
||||
prefs.load(this, profile)
|
||||
adapter.notifyDataSetChanged()
|
||||
}
|
||||
|
||||
// Dialogs do not have a parent view.
|
||||
@Suppress("InflateParams")
|
||||
private fun addProfile() {
|
||||
val view = layoutInflater.inflate(
|
||||
R.layout.dialog_profile_name, null
|
||||
)
|
||||
val editText = view.findViewById<EditText>(R.id.name)
|
||||
AlertDialog.Builder(this)
|
||||
.setView(view)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
val name = editText.text.toString().trim()
|
||||
if (name.isNotEmpty() && prefs.addProfile(name)) {
|
||||
prefs.load(this, name)
|
||||
refreshProfiles()
|
||||
} else {
|
||||
toast(R.string.profile_invalid_name)
|
||||
}
|
||||
}
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun confirmRemoveProfile(position: Int) {
|
||||
val profile = profiles[position]
|
||||
val profileName = profile.name ?: return
|
||||
AlertDialog.Builder(this)
|
||||
.setMessage(R.string.profile_remove_confirm)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
val isCurrent = prefs.profile == profileName
|
||||
prefs.removeProfile(profileName)
|
||||
if (isCurrent) {
|
||||
prefs.load(this, null)
|
||||
}
|
||||
refreshProfiles()
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
||||
.show()
|
||||
}
|
||||
|
||||
private class ProfilesAdapter(
|
||||
context: Context,
|
||||
private val items: List<ProfileItem>
|
||||
) : ArrayAdapter<ProfileItem>(
|
||||
context,
|
||||
android.R.layout.simple_list_item_2,
|
||||
android.R.id.text1,
|
||||
items
|
||||
) {
|
||||
override fun getView(
|
||||
position: Int,
|
||||
convertView: View?,
|
||||
parent: ViewGroup
|
||||
): View {
|
||||
val view = super.getView(position, convertView, parent)
|
||||
val item = items[position]
|
||||
val isCurrent = item.name == prefs.profile
|
||||
getViewHolder(view).apply {
|
||||
title.text = item.label
|
||||
if (isCurrent) {
|
||||
subtitle.text = context.getString(R.string.profile_current)
|
||||
subtitle.visibility = View.VISIBLE
|
||||
} else {
|
||||
subtitle.text = ""
|
||||
subtitle.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
return view
|
||||
}
|
||||
|
||||
private fun getViewHolder(
|
||||
view: View
|
||||
): ViewHolder = view.tag as ViewHolder? ?: ViewHolder(
|
||||
view.findViewById(android.R.id.text1),
|
||||
view.findViewById(android.R.id.text2),
|
||||
).also {
|
||||
view.tag = it
|
||||
}
|
||||
|
||||
private data class ViewHolder(
|
||||
val title: TextView,
|
||||
val subtitle: TextView,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private data class ProfileItem(
|
||||
val name: String?,
|
||||
val label: String
|
||||
)
|
||||
@@ -0,0 +1,68 @@
|
||||
package de.markusfisch.android.binaryeye.activity
|
||||
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Bundle
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.view.isNotEmpty
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.app.PERMISSION_LOCATION
|
||||
import de.markusfisch.android.binaryeye.app.PERMISSION_WRITE
|
||||
import de.markusfisch.android.binaryeye.app.applyLocale
|
||||
import de.markusfisch.android.binaryeye.app.permissionGrantedCallback
|
||||
import de.markusfisch.android.binaryeye.app.prefs
|
||||
import de.markusfisch.android.binaryeye.view.initBars
|
||||
|
||||
abstract class ScreenActivity : AppCompatActivity() {
|
||||
override fun attachBaseContext(base: Context?) {
|
||||
base?.applyLocale(prefs.customLocale)
|
||||
super.attachBaseContext(base)
|
||||
}
|
||||
|
||||
override fun onCreate(state: Bundle?) {
|
||||
super.onCreate(state)
|
||||
setContentView(R.layout.activity_main)
|
||||
initBars()
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
}
|
||||
|
||||
override fun onSupportNavigateUp(): Boolean {
|
||||
finish()
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onRequestPermissionsResult(
|
||||
requestCode: Int,
|
||||
permissions: Array<String>,
|
||||
grantResults: IntArray
|
||||
) {
|
||||
super.onRequestPermissionsResult(
|
||||
requestCode,
|
||||
permissions,
|
||||
grantResults
|
||||
)
|
||||
when (requestCode) {
|
||||
PERMISSION_LOCATION, PERMISSION_WRITE -> {
|
||||
if (grantResults.isNotEmpty() &&
|
||||
grantResults[0] == PackageManager.PERMISSION_GRANTED
|
||||
) {
|
||||
permissionGrantedCallback?.invoke()
|
||||
permissionGrantedCallback = null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||
onCreateOptionsMenu(menu, menuInflater)
|
||||
return menu.isNotEmpty() || super.onCreateOptionsMenu(menu)
|
||||
}
|
||||
|
||||
protected open fun onCreateOptionsMenu(
|
||||
menu: Menu,
|
||||
inflater: MenuInflater
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@ package de.markusfisch.android.binaryeye.activity
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.support.v7.app.AppCompatActivity
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
|
||||
class SplashActivity : AppCompatActivity() {
|
||||
override fun onCreate(state: Bundle?) {
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
package de.markusfisch.android.binaryeye.activity
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.app.prefs
|
||||
|
||||
class WelcomeActivity : AppCompatActivity() {
|
||||
override fun onCreate(state: Bundle?) {
|
||||
super.onCreate(state)
|
||||
setContentView(R.layout.activity_welcome)
|
||||
findViewById<View>(R.id.expert).setOnClickListener {
|
||||
finish()
|
||||
}
|
||||
findViewById<View>(R.id.simple).setOnClickListener {
|
||||
prefs.apply {
|
||||
showCropHandle = false
|
||||
zoomBySwiping = false
|
||||
beep = true
|
||||
copyImmediately = true
|
||||
openImmediately = true
|
||||
showMetaData = false
|
||||
showHexDump = false
|
||||
defaultSearchUrl = "https://www.google.com/search?q="
|
||||
openWithUrl = defaultSearchUrl
|
||||
expandEscapeSequences = false
|
||||
brightenScreen = true
|
||||
}
|
||||
finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,12 +2,12 @@ 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
|
||||
import android.widget.CursorAdapter
|
||||
import android.widget.TextView
|
||||
import androidx.core.content.ContextCompat
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.database.Database
|
||||
import java.text.DateFormat
|
||||
@@ -20,7 +20,7 @@ class ScansAdapter(context: Context, cursor: Cursor) :
|
||||
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 textIndex = cursor.getColumnIndex(Database.SCANS_TEXT)
|
||||
private val formatIndex = cursor.getColumnIndex(Database.SCANS_FORMAT)
|
||||
private val selections = mutableMapOf<Long, Int>()
|
||||
private val selectedColor = ContextCompat.getColor(
|
||||
@@ -74,7 +74,7 @@ class ScansAdapter(context: Context, cursor: Cursor) :
|
||||
|
||||
fun getContent(
|
||||
position: Int
|
||||
) = (getItem(position) as Cursor?)?.getString(contentIndex)
|
||||
) = (getItem(position) as Cursor?)?.getString(textIndex)
|
||||
|
||||
override fun newView(
|
||||
context: Context,
|
||||
@@ -92,7 +92,7 @@ class ScansAdapter(context: Context, cursor: Cursor) :
|
||||
getViewHolder(view).apply {
|
||||
timeView.text = formatDateTime(cursor.getString(timeIndex))
|
||||
val name = cursor.getString(nameIndex)
|
||||
val content = cursor.getString(contentIndex)
|
||||
val text = cursor.getString(textIndex)
|
||||
var icon = 0
|
||||
contentView.text = when {
|
||||
name?.isNotEmpty() == true -> {
|
||||
@@ -100,16 +100,16 @@ class ScansAdapter(context: Context, cursor: Cursor) :
|
||||
name
|
||||
}
|
||||
|
||||
content.isNullOrEmpty() -> context.getString(
|
||||
text.isNullOrEmpty() -> context.getString(
|
||||
R.string.binary_data
|
||||
)
|
||||
|
||||
else -> content
|
||||
else -> text
|
||||
}
|
||||
contentView.setCompoundDrawablesWithIntrinsicBounds(
|
||||
icon, 0, 0, 0
|
||||
)
|
||||
formatView.text = prettifyFormatName(cursor.getString(formatIndex))
|
||||
formatView.text = cursor.getString(formatIndex).prettifyFormatName()
|
||||
}
|
||||
val selected = selections[cursor.getLong(idIndex)] != null
|
||||
view.post {
|
||||
@@ -139,7 +139,11 @@ class ScansAdapter(context: Context, cursor: Cursor) :
|
||||
)
|
||||
}
|
||||
|
||||
fun prettifyFormatName(format: String) = format.replace("_", " ")
|
||||
private val formatWordBoundary =
|
||||
"(?<=[a-z0-9])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])|(?<=[A-Za-z])(?=\\d)|(?<=\\d)(?=[A-Za-z])".toRegex()
|
||||
|
||||
fun String.prettifyFormatName() = replace("_", " ")
|
||||
.replace(formatWordBoundary, " ")
|
||||
|
||||
private fun formatDateTime(rfc: String): String {
|
||||
try {
|
||||
@@ -149,7 +153,7 @@ private fun formatDateTime(rfc: String): String {
|
||||
DateFormat.SHORT
|
||||
).format(it)
|
||||
}
|
||||
} catch (e: ParseException) {
|
||||
} catch (_: ParseException) {
|
||||
// Ignore and fall through.
|
||||
}
|
||||
return rfc
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
package de.markusfisch.android.binaryeye.app
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.support.v4.app.Fragment
|
||||
import android.support.v4.app.FragmentManager
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
|
||||
fun FragmentManager.setFragment(fragment: Fragment) {
|
||||
getTransaction(fragment).commit()
|
||||
}
|
||||
|
||||
fun FragmentManager.addFragment(fragment: Fragment) {
|
||||
getTransaction(fragment).addToBackStack(null).commit()
|
||||
}
|
||||
|
||||
@SuppressLint("CommitTransaction")
|
||||
private fun FragmentManager.getTransaction(
|
||||
fragment: Fragment
|
||||
) = beginTransaction().replace(R.id.content_frame, fragment)
|
||||
@@ -2,7 +2,7 @@ package de.markusfisch.android.binaryeye.app
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import java.util.*
|
||||
import java.util.Locale
|
||||
|
||||
fun Context.applyLocale(localeName: String) {
|
||||
if (localeName.isEmpty()) {
|
||||
|
||||
@@ -4,15 +4,15 @@ import android.Manifest
|
||||
import android.app.Activity
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import android.support.v4.app.ActivityCompat
|
||||
import android.support.v4.content.ContextCompat
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
|
||||
var permissionGrantedCallback: (() -> Any)? = null
|
||||
|
||||
const val PERMISSION_CAMERA = 1
|
||||
fun Activity.hasCameraPermission() = hasPermission(
|
||||
fun Activity.hasCameraPermission(request: Boolean = true) = hasPermission(
|
||||
Manifest.permission.CAMERA,
|
||||
PERMISSION_CAMERA
|
||||
if (request) PERMISSION_CAMERA else 0
|
||||
)
|
||||
|
||||
const val PERMISSION_WRITE = 2
|
||||
@@ -53,11 +53,13 @@ private fun Activity.hasPermission(
|
||||
permission
|
||||
) != PackageManager.PERMISSION_GRANTED
|
||||
) {
|
||||
ActivityCompat.requestPermissions(
|
||||
this,
|
||||
arrayOf(permission),
|
||||
requestCode
|
||||
)
|
||||
if (requestCode > 0) {
|
||||
ActivityCompat.requestPermissions(
|
||||
this,
|
||||
arrayOf(permission),
|
||||
requestCode
|
||||
)
|
||||
}
|
||||
false
|
||||
} else {
|
||||
true
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
package de.markusfisch.android.binaryeye.automation
|
||||
|
||||
import de.markusfisch.android.binaryeye.database.Scan
|
||||
import de.markusfisch.android.binaryeye.net.urlEncode
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
import java.util.Locale
|
||||
|
||||
data class AutomatedAction(
|
||||
val pattern: String,
|
||||
val type: Type,
|
||||
val urlTemplate: String? = null,
|
||||
val intentUriTemplate: String? = null
|
||||
) {
|
||||
enum class Type {
|
||||
Intent, CustomIntent
|
||||
}
|
||||
|
||||
fun matches(content: String): Boolean = try {
|
||||
Regex(pattern).matches(content)
|
||||
} catch (_: Exception) {
|
||||
false
|
||||
}
|
||||
|
||||
fun toJson(): JSONObject = JSONObject().apply {
|
||||
put(KEY_PATTERN, pattern)
|
||||
put(KEY_TYPE, type.name.lowercase(Locale.ROOT))
|
||||
put(KEY_URL_TEMPLATE, urlTemplate ?: "")
|
||||
put(KEY_INTENT_URI_TEMPLATE, intentUriTemplate ?: "")
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val KEY_PATTERN = "pattern"
|
||||
private const val KEY_TYPE = "type"
|
||||
private const val KEY_URL_TEMPLATE = "url_template"
|
||||
private const val KEY_INTENT_URI_TEMPLATE = "intent_uri_template"
|
||||
|
||||
fun fromJson(obj: JSONObject): AutomatedAction? {
|
||||
val pattern = obj.optString(KEY_PATTERN, "").trim()
|
||||
if (pattern.isEmpty()) {
|
||||
return null
|
||||
}
|
||||
val type = when (obj.optString(KEY_TYPE, "")) {
|
||||
"custom_intent" -> Type.CustomIntent
|
||||
"intent" -> Type.Intent
|
||||
"html" -> return null
|
||||
else -> Type.Intent
|
||||
}
|
||||
val urlTemplate = obj.optString(KEY_URL_TEMPLATE, "").trim()
|
||||
val intentUriTemplate = obj.optString(
|
||||
KEY_INTENT_URI_TEMPLATE,
|
||||
""
|
||||
).trim()
|
||||
return AutomatedAction(
|
||||
pattern = pattern,
|
||||
type = type,
|
||||
urlTemplate = urlTemplate.ifEmpty { null },
|
||||
intentUriTemplate = intentUriTemplate.ifEmpty { null }
|
||||
)
|
||||
}
|
||||
|
||||
fun fromJsonArray(json: String): MutableList<AutomatedAction> {
|
||||
val actions = ArrayList<AutomatedAction>()
|
||||
val array = try {
|
||||
JSONArray(json)
|
||||
} catch (_: Exception) {
|
||||
JSONArray()
|
||||
}
|
||||
for (i in 0 until array.length()) {
|
||||
val obj = array.optJSONObject(i) ?: continue
|
||||
fromJson(obj)?.let { actions.add(it) }
|
||||
}
|
||||
return actions
|
||||
}
|
||||
|
||||
fun toJsonArray(actions: List<AutomatedAction>): String {
|
||||
val array = JSONArray()
|
||||
actions.forEach { array.put(it.toJson()) }
|
||||
return array.toString()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun String.buildUrl(scan: Scan): String {
|
||||
return buildTemplate(scan, encodeResult = true)
|
||||
}
|
||||
|
||||
fun String.buildTemplate(scan: Scan, encodeResult: Boolean): String {
|
||||
val text = scan.text
|
||||
val raw = scan.raw ?: text.toByteArray()
|
||||
val result = if (encodeResult) {
|
||||
text.urlEncode()
|
||||
} else {
|
||||
text
|
||||
}
|
||||
val format = if (encodeResult) {
|
||||
scan.format.name.urlEncode()
|
||||
} else {
|
||||
scan.format.name
|
||||
}
|
||||
return this
|
||||
.replace("{RESULT}", result)
|
||||
.replace("{RESULT_BYTES}", raw.toHexString())
|
||||
.replace("{FORMAT}", format)
|
||||
}
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
package de.markusfisch.android.binaryeye.automation
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.app.prefs
|
||||
import de.markusfisch.android.binaryeye.content.execShareIntent
|
||||
import de.markusfisch.android.binaryeye.content.openUrl
|
||||
import de.markusfisch.android.binaryeye.database.Scan
|
||||
import de.markusfisch.android.binaryeye.widget.toast
|
||||
|
||||
fun Context.runAutomatedActions(scan: Scan): Boolean {
|
||||
val content = scan.text
|
||||
if (content.isEmpty()) {
|
||||
return false
|
||||
}
|
||||
val actions = prefs.automatedActions
|
||||
if (actions.isEmpty()) {
|
||||
return false
|
||||
}
|
||||
for (action in actions) {
|
||||
if (action.matches(content)) {
|
||||
return when (action.type) {
|
||||
AutomatedAction.Type.Intent -> {
|
||||
val url = action.urlTemplate?.buildUrl(scan)
|
||||
if (url.isNullOrEmpty()) {
|
||||
toast(R.string.automated_action_invalid_url)
|
||||
false
|
||||
} else {
|
||||
openUrl(url)
|
||||
}
|
||||
}
|
||||
|
||||
AutomatedAction.Type.CustomIntent ->
|
||||
execCustomIntent(action, scan)
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private fun Context.execCustomIntent(
|
||||
action: AutomatedAction,
|
||||
scan: Scan
|
||||
): Boolean {
|
||||
val template = action.intentUriTemplate
|
||||
if (template.isNullOrEmpty()) {
|
||||
toast(R.string.automated_action_intent_uri_required)
|
||||
return false
|
||||
}
|
||||
val intentUri = template.buildTemplate(scan, encodeResult = false)
|
||||
val intent = try {
|
||||
Intent.parseUri(intentUri, Intent.URI_INTENT_SCHEME)
|
||||
} catch (_: Exception) {
|
||||
toast(R.string.automated_action_intent_uri_invalid)
|
||||
return false
|
||||
}
|
||||
return execShareIntent(intent)
|
||||
}
|
||||
@@ -3,7 +3,7 @@ package de.markusfisch.android.binaryeye.bluetooth
|
||||
import android.bluetooth.BluetoothAdapter
|
||||
import android.bluetooth.BluetoothDevice
|
||||
import android.bluetooth.BluetoothSocket
|
||||
import android.support.v7.preference.ListPreference
|
||||
import androidx.preference.ListPreference
|
||||
import de.markusfisch.android.binaryeye.database.Scan
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -21,10 +21,10 @@ fun Scan.sendBluetoothAsync(
|
||||
val connected = if (isConnected) {
|
||||
true
|
||||
} else {
|
||||
connect(host)
|
||||
connect(host, true)
|
||||
}
|
||||
val sent = if (connected) {
|
||||
send(content)
|
||||
send(text, host)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
@@ -35,21 +35,20 @@ fun Scan.sendBluetoothAsync(
|
||||
}
|
||||
|
||||
fun setBluetoothHosts(listPref: ListPreference) {
|
||||
val devices = try {
|
||||
BluetoothAdapter.getDefaultAdapter().bondedDevices
|
||||
} catch (e: SecurityException) {
|
||||
try {
|
||||
val devices = blue.bondedDevices ?: return
|
||||
listPref.entries = devices.map {
|
||||
it.name
|
||||
}.toTypedArray()
|
||||
listPref.entryValues = devices.map {
|
||||
it.address
|
||||
}.toTypedArray()
|
||||
listPref.callChangeListener(listPref.value)
|
||||
} catch (_: SecurityException) {
|
||||
// Do nothing, either the user has denied Bluetooth access
|
||||
// or the permission was removed by the system. We're catching
|
||||
// the exception to keep the app from crashing.
|
||||
null
|
||||
} ?: return
|
||||
listPref.entries = devices.map {
|
||||
it.name
|
||||
}.toTypedArray()
|
||||
listPref.entryValues = devices.map {
|
||||
it.address
|
||||
}.toTypedArray()
|
||||
listPref.callChangeListener(listPref.value)
|
||||
}
|
||||
}
|
||||
|
||||
private var socket: BluetoothSocket? = null
|
||||
@@ -62,7 +61,7 @@ private val uuid = UUID.fromString(
|
||||
|
||||
private var isConnected = false
|
||||
|
||||
private fun connect(deviceName: String): Boolean = try {
|
||||
private fun connect(deviceName: String, onceMore: Boolean): Boolean = try {
|
||||
val device = findByName(deviceName) ?: throw RuntimeException(
|
||||
"Bluetooth device not found"
|
||||
)
|
||||
@@ -71,32 +70,41 @@ private fun connect(deviceName: String): Boolean = try {
|
||||
writer = socket?.outputStream?.writer()
|
||||
isConnected = true
|
||||
true
|
||||
} catch (e: Exception) {
|
||||
close()
|
||||
} catch (_: Exception) {
|
||||
if (onceMore) {
|
||||
connect(deviceName, false)
|
||||
} else {
|
||||
close()
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
private fun send(message: String): Boolean = try {
|
||||
private fun send(message: String, host: String): Boolean = try {
|
||||
writer?.apply {
|
||||
write(message)
|
||||
write("\n")
|
||||
flush()
|
||||
}
|
||||
true
|
||||
} catch (e: Exception) {
|
||||
} catch (_: Exception) {
|
||||
close()
|
||||
false
|
||||
|
||||
if (connect(host, false)) {
|
||||
send(message, host)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
private fun close() {
|
||||
try {
|
||||
writer?.close()
|
||||
} catch (e: IOException) {
|
||||
} catch (_: IOException) {
|
||||
// Catch exception if writer wasn't initialized.
|
||||
}
|
||||
try {
|
||||
socket?.close()
|
||||
} catch (e: IOException) {
|
||||
} catch (_: IOException) {
|
||||
// Nothing we can do about this but keep
|
||||
// the app from crashing.
|
||||
}
|
||||
@@ -106,11 +114,15 @@ private fun close() {
|
||||
}
|
||||
|
||||
private fun findByName(findableName: String): BluetoothDevice? {
|
||||
val deviceList = blue.bondedDevices
|
||||
for (device in deviceList) {
|
||||
if (device.address == findableName) {
|
||||
return device
|
||||
try {
|
||||
val deviceList = blue.bondedDevices
|
||||
for (device in deviceList) {
|
||||
if (device.address == findableName) {
|
||||
return device
|
||||
}
|
||||
}
|
||||
} catch (_: SecurityException) {
|
||||
// Fall through.
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -0,0 +1,210 @@
|
||||
package de.markusfisch.android.binaryeye.content
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import de.markusfisch.android.binaryeye.database.Scan
|
||||
import de.markusfisch.android.binaryeye.graphics.COLOR_BLACK
|
||||
import de.markusfisch.android.binaryeye.graphics.COLOR_WHITE
|
||||
import de.markusfisch.android.zxingcpp.ZxingCpp
|
||||
import de.markusfisch.android.zxingcpp.ZxingCpp.BarcodeFormat
|
||||
import de.markusfisch.android.zxingcpp.ZxingCpp.BitMatrix
|
||||
import de.markusfisch.android.zxingcpp.ZxingCpp.toBitmap
|
||||
import de.markusfisch.android.zxingcpp.ZxingCpp.toSvg
|
||||
import de.markusfisch.android.zxingcpp.ZxingCpp.toText
|
||||
import java.util.Arrays
|
||||
|
||||
enum class BarcodeColors {
|
||||
BLACK_ON_WHITE,
|
||||
WHITE_ON_BLACK,
|
||||
BLACK_ON_TRANSPARENT,
|
||||
WHITE_ON_TRANSPARENT;
|
||||
|
||||
fun foregroundColor(): Int = when (this) {
|
||||
BLACK_ON_WHITE,
|
||||
BLACK_ON_TRANSPARENT -> COLOR_BLACK
|
||||
|
||||
WHITE_ON_BLACK,
|
||||
WHITE_ON_TRANSPARENT -> COLOR_WHITE
|
||||
}
|
||||
|
||||
fun backgroundColor(): Int = when (this) {
|
||||
BLACK_ON_WHITE -> COLOR_WHITE
|
||||
WHITE_ON_BLACK -> COLOR_BLACK
|
||||
BLACK_ON_TRANSPARENT,
|
||||
WHITE_ON_TRANSPARENT -> 0
|
||||
}
|
||||
}
|
||||
|
||||
fun Scan.toBarcode(): Barcode<*> {
|
||||
val content = text.ifEmpty {
|
||||
raw
|
||||
}
|
||||
return if (symbol != null) {
|
||||
BitMatrixBarcode(
|
||||
symbol,
|
||||
content,
|
||||
format
|
||||
)
|
||||
} else {
|
||||
ContentBarcode(
|
||||
content,
|
||||
format,
|
||||
errorCorrectionLevel.toErrorCorrectionInt(),
|
||||
margin = 1
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun String?.toErrorCorrectionInt() = when (this) {
|
||||
"L" -> 0
|
||||
"M" -> 4
|
||||
"Q" -> 6
|
||||
"H" -> 8
|
||||
else -> -1
|
||||
}
|
||||
|
||||
sealed class Barcode<T>(
|
||||
val content: T,
|
||||
val format: BarcodeFormat,
|
||||
val colors: BarcodeColors
|
||||
) {
|
||||
private var _bitmap: Bitmap? = null
|
||||
fun bitmap(): Bitmap {
|
||||
val b = _bitmap ?: bitmap(512)
|
||||
_bitmap = b
|
||||
return b
|
||||
}
|
||||
|
||||
fun bitmap(size: Int): Bitmap {
|
||||
return toBitmap(size)
|
||||
}
|
||||
|
||||
protected abstract fun toBitmap(size: Int): Bitmap
|
||||
|
||||
private var _svg: String? = null
|
||||
fun svg(): String {
|
||||
val s = _svg ?: toSvg()
|
||||
_svg = s
|
||||
return s
|
||||
}
|
||||
|
||||
protected abstract fun toSvg(): String
|
||||
|
||||
private var _text: String? = null
|
||||
fun text(): String {
|
||||
val t = _text ?: toText()
|
||||
_text = t
|
||||
return t
|
||||
}
|
||||
|
||||
protected abstract fun toText(): String
|
||||
|
||||
fun textOrHex(): String = when (content) {
|
||||
is String -> content
|
||||
is ByteArray -> content.toHexString()
|
||||
else -> throw IllegalArgumentException("Illegal arguments")
|
||||
}
|
||||
}
|
||||
|
||||
class ContentBarcode<T>(
|
||||
content: T,
|
||||
format: BarcodeFormat,
|
||||
val ecLevel: Int = -1,
|
||||
val margin: Int = 0,
|
||||
colors: BarcodeColors = BarcodeColors.BLACK_ON_WHITE
|
||||
) : Barcode<T>(content, format, colors) {
|
||||
override fun toBitmap(size: Int) = ZxingCpp.encodeAsBitmap(
|
||||
content, format, size, format.height(size), margin, ecLevel,
|
||||
setColor = colors.foregroundColor(),
|
||||
unsetColor = colors.backgroundColor()
|
||||
)
|
||||
|
||||
override fun toSvg() = ZxingCpp.encodeAsSvg(
|
||||
content, format, margin, ecLevel
|
||||
)
|
||||
|
||||
override fun toText() = ZxingCpp.encodeAsText(
|
||||
content, format, margin, ecLevel,
|
||||
inverted = colors == BarcodeColors.BLACK_ON_WHITE
|
||||
)
|
||||
}
|
||||
|
||||
class BitMatrixBarcode<T>(
|
||||
val bitMatrix: BitMatrix,
|
||||
content: T,
|
||||
format: BarcodeFormat,
|
||||
colors: BarcodeColors = BarcodeColors.BLACK_ON_WHITE
|
||||
) : Barcode<T>(content, format, colors) {
|
||||
override fun toBitmap(size: Int) = bitMatrix.inflate(size).toBitmap(
|
||||
setColor = colors.foregroundColor(),
|
||||
unsetColor = colors.backgroundColor()
|
||||
)
|
||||
|
||||
override fun toSvg() = bitMatrix.inflate().toSvg()
|
||||
|
||||
override fun toText() = bitMatrix.inflate().toText(
|
||||
inverted = colors == BarcodeColors.BLACK_ON_WHITE
|
||||
)
|
||||
}
|
||||
|
||||
private fun BitMatrix.inflate(
|
||||
size: Int = 0,
|
||||
quietZone: Boolean = true
|
||||
): BitMatrix {
|
||||
val extraModules = if (quietZone) 2 else 0
|
||||
val codeWidth = width + extraModules
|
||||
val codeHeight = height + extraModules
|
||||
var scale = size / codeWidth
|
||||
if (scale < 1) {
|
||||
if (size > 0 && !quietZone) {
|
||||
return this
|
||||
}
|
||||
scale = 1
|
||||
}
|
||||
val outWidth = codeWidth * scale
|
||||
val outHeight = codeHeight * scale
|
||||
val scaled = BitMatrix(
|
||||
outWidth,
|
||||
outHeight,
|
||||
ByteArray(outWidth * outHeight)
|
||||
)
|
||||
Arrays.fill(scaled.data, 1)
|
||||
val padding = extraModules / 2
|
||||
val left = padding * scale
|
||||
val top = padding * scale
|
||||
val right = outWidth - left
|
||||
val bottom = outHeight - top
|
||||
for (y in top until bottom) {
|
||||
var dst = y * outWidth + left
|
||||
val offset = (y - top) / scale * width
|
||||
for (x in left until right) {
|
||||
scaled.data[dst++] = data[
|
||||
offset + (x - left) / scale
|
||||
]
|
||||
}
|
||||
}
|
||||
return scaled
|
||||
}
|
||||
|
||||
private fun BarcodeFormat.height(size: Int) = when (this) {
|
||||
// 1D barcodes don't need as much vertical space.
|
||||
BarcodeFormat.Codabar,
|
||||
BarcodeFormat.Code39,
|
||||
BarcodeFormat.Code39Ext,
|
||||
BarcodeFormat.Code32,
|
||||
BarcodeFormat.PZN,
|
||||
BarcodeFormat.Code93,
|
||||
BarcodeFormat.Code128,
|
||||
BarcodeFormat.DataBarOmni,
|
||||
BarcodeFormat.DataBarStk,
|
||||
BarcodeFormat.DataBarLtd,
|
||||
BarcodeFormat.DataBarExp,
|
||||
BarcodeFormat.DataBarExpStk,
|
||||
BarcodeFormat.DXFilmEdge,
|
||||
BarcodeFormat.EAN8,
|
||||
BarcodeFormat.EAN13,
|
||||
BarcodeFormat.ITF,
|
||||
BarcodeFormat.UPCA,
|
||||
BarcodeFormat.UPCE -> size / 3
|
||||
|
||||
else -> size
|
||||
}
|
||||
@@ -8,16 +8,11 @@ import android.os.PersistableBundle
|
||||
|
||||
fun Context.copyToClipboard(text: String, isSensitive: Boolean = false) {
|
||||
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)
|
||||
}
|
||||
)
|
||||
}
|
||||
(service as android.content.ClipboardManager).setPrimaryClip(
|
||||
ClipData.newPlainText("plain text", text).apply {
|
||||
setSensitive(isSensitive)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun ClipData.setSensitive(isSensitive: Boolean) {
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
package de.markusfisch.android.binaryeye.content
|
||||
|
||||
fun ByteArray.toHexString() = joinToString("") { "%02X".format(it) }
|
||||
@@ -0,0 +1,67 @@
|
||||
package de.markusfisch.android.binaryeye.content
|
||||
|
||||
import android.content.Context
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
|
||||
private val idToLabel = hashMapOf(
|
||||
"DCA" to R.string.idl_vehicle_class,
|
||||
"DCB" to R.string.idl_restriction_codes,
|
||||
"DCD" to R.string.idl_endorsement_codes,
|
||||
"DBA" to R.string.idl_date_of_expiry,
|
||||
"DCS" to R.string.idl_customer_family_name,
|
||||
"DAC" to R.string.idl_customer_first_name,
|
||||
"DCT" to R.string.idl_customer_first_name_alt,
|
||||
"DAD" to R.string.idl_customer_middle_name,
|
||||
"DBD" to R.string.idl_date_of_issue,
|
||||
"DBB" to R.string.idl_date_of_birth,
|
||||
"DBC" to R.string.idl_sex,
|
||||
"DAY" to R.string.idl_eye_color,
|
||||
"DAU" to R.string.idl_body_height,
|
||||
"DAG" to R.string.idl_address_street,
|
||||
"DAI" to R.string.idl_address_city,
|
||||
"DAJ" to R.string.idl_address_jurisdiction_code,
|
||||
"DAK" to R.string.idl_address_postal_code,
|
||||
"DAQ" to R.string.idl_customer_id_number,
|
||||
"DCF" to R.string.idl_document_discriminator,
|
||||
"DCG" to R.string.idl_country,
|
||||
"DDE" to R.string.idl_family_name_truncation,
|
||||
"DDF" to R.string.idl_first_name_truncation,
|
||||
"DDG" to R.string.idl_middle_name_truncation,
|
||||
"DAH" to R.string.idl_address_street_2,
|
||||
"DAZ" to R.string.idl_hair_color,
|
||||
"DCI" to R.string.idl_place_of_birth,
|
||||
"DCJ" to R.string.idl_audit_information,
|
||||
"DCK" to R.string.idl_inventory_control_number,
|
||||
"DBN" to R.string.idl_alias_family_name,
|
||||
"DBG" to R.string.idl_alias_given_name,
|
||||
"DBS" to R.string.idl_alias_suffix_name,
|
||||
"DCU" to R.string.idl_name_suffix,
|
||||
"DCE" to R.string.idl_weight_range,
|
||||
"DCL" to R.string.idl_ethnicity,
|
||||
"DCM" to R.string.idl_standard_vehicle_classification,
|
||||
"DCN" to R.string.idl_standard_endorsement_code,
|
||||
"DCO" to R.string.idl_standard_restriction_code,
|
||||
"DCP" to R.string.idl_vehicle_classification_description,
|
||||
"DCQ" to R.string.idl_endorsement_code_description,
|
||||
"DCR" to R.string.idl_restriction_code_description,
|
||||
"DDA" to R.string.idl_compliance_type,
|
||||
"DDB" to R.string.idl_card_revision_date,
|
||||
"DDC" to R.string.idl_hazmat_endorsement_expiration_date,
|
||||
"DDD" to R.string.idl_limited_duration_document_indicator,
|
||||
"DAW" to R.string.idl_weight_pounds,
|
||||
"DAX" to R.string.idl_weight_kilograms,
|
||||
"DDH" to R.string.idl_under_18_until_date,
|
||||
"DDI" to R.string.idl_under_19_until_date,
|
||||
"DDJ" to R.string.idl_under_21_until_date,
|
||||
"DDK" to R.string.idl_organ_donor_indicator,
|
||||
"DDL" to R.string.idl_veteran_indicator
|
||||
)
|
||||
|
||||
fun Context.idlToRes(id: String): String {
|
||||
val resId = idToLabel[id]
|
||||
return if (resId != null) {
|
||||
getString(resId)
|
||||
} else {
|
||||
id
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
package de.markusfisch.android.binaryeye.content
|
||||
|
||||
data class IdlInfo(
|
||||
var iin: String? = null,
|
||||
val elements: MutableMap<String, String> = linkedMapOf()
|
||||
)
|
||||
|
||||
object IdlParser {
|
||||
private fun resolveSex(value: String): String = when (value.firstOrNull()) {
|
||||
'1' -> "M"
|
||||
'2' -> "F"
|
||||
'9' -> "X"
|
||||
else -> value
|
||||
}
|
||||
|
||||
private fun skipWhitespace(s: String, p: Int): Int {
|
||||
var i = p
|
||||
while (i < s.length && s[i].isWhitespace()) {
|
||||
++i;
|
||||
}
|
||||
return i
|
||||
}
|
||||
|
||||
private fun findSubType(s: String, idlInfo: IdlInfo): String {
|
||||
val len = s.length
|
||||
var p = skipWhitespace(s, 0)
|
||||
|
||||
if (p < len && s[p] == '@') {
|
||||
while (p < len) {
|
||||
if (!s[p].isDigit()) {
|
||||
++p
|
||||
continue
|
||||
}
|
||||
var d = p
|
||||
while (d < len && s[d].isDigit()) {
|
||||
++d
|
||||
}
|
||||
if (d - p < 8 || len - d < 3) {
|
||||
p = d
|
||||
continue
|
||||
}
|
||||
|
||||
val code = s.substring(d, d + 2)
|
||||
if (code != "DL" && code != "ID") {
|
||||
p = d + 2
|
||||
continue
|
||||
}
|
||||
|
||||
idlInfo.elements["DL"] = code
|
||||
d += 2
|
||||
|
||||
while (d < len) {
|
||||
while (d < len && s[d] !in "DI") {
|
||||
++d
|
||||
}
|
||||
if (len - d < 2) break
|
||||
|
||||
val sec = s.substring(d, d + 2)
|
||||
if (sec == "DL" || sec == "ID") {
|
||||
return s.substring(d + 2)
|
||||
}
|
||||
++d
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
private fun findIIN(s: String): String? {
|
||||
val ansi = s.indexOf("ANSI").takeIf { it >= 0 } ?: return null
|
||||
var p = ansi + 4
|
||||
val len = s.length
|
||||
val whitespaceSkipped = skipWhitespace(s, p)
|
||||
if (whitespaceSkipped == p) {
|
||||
return null
|
||||
}
|
||||
p = whitespaceSkipped
|
||||
val iinStart = p
|
||||
var digits = 0
|
||||
while (digits < 6 && p < len && s[p].isDigit()) {
|
||||
++p
|
||||
++digits
|
||||
}
|
||||
return s.substring(iinStart, p)
|
||||
}
|
||||
|
||||
fun parse(s: String?): IdlInfo? {
|
||||
if (s.isNullOrEmpty()) {
|
||||
return null
|
||||
}
|
||||
|
||||
val idlInfo = IdlInfo().apply {
|
||||
iin = findIIN(s)
|
||||
}
|
||||
val data = findSubType(s, idlInfo)
|
||||
val len = data.length
|
||||
var start = 0
|
||||
|
||||
for (p in 0..len) {
|
||||
if (p < len && data[p] > '\u001f') {
|
||||
continue
|
||||
}
|
||||
|
||||
if (p - start > 3) {
|
||||
val vp = start + 3
|
||||
val key = data.substring(start, vp)
|
||||
|
||||
if (key.matches(Regex("[DZ][A-Z]{2}"))) {
|
||||
var value = data.substring(vp, p)
|
||||
if (key == "DBC") {
|
||||
value = resolveSex(value)
|
||||
}
|
||||
idlInfo.elements[key] = value.trim()
|
||||
}
|
||||
}
|
||||
start = p + 1
|
||||
}
|
||||
|
||||
return if (idlInfo.elements.isEmpty()) null else idlInfo
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,8 @@ import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.support.v4.content.FileProvider
|
||||
import androidx.core.content.FileProvider
|
||||
import androidx.core.net.toUri
|
||||
import de.markusfisch.android.binaryeye.BuildConfig
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.widget.toast
|
||||
@@ -33,12 +34,20 @@ fun Context.startIntent(intent: Intent): Boolean = try {
|
||||
// necessary.
|
||||
startActivity(intent)
|
||||
true
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
} catch (_: ActivityNotFoundException) {
|
||||
false
|
||||
}
|
||||
|
||||
fun Context.openUrl(url: String, silent: Boolean = false): Boolean {
|
||||
val intent = Intent(Intent.ACTION_VIEW, url.parseAndNormalizeUri())
|
||||
return openUri(url.parseAndNormalizeUri(), silent)
|
||||
}
|
||||
|
||||
fun Context.openUri(uri: Uri, silent: Boolean = false): Boolean {
|
||||
val intent = Intent(Intent.ACTION_VIEW, uri).apply {
|
||||
if (uri.scheme == "content") {
|
||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
}
|
||||
}
|
||||
return if (silent) {
|
||||
startIntent(intent)
|
||||
} else {
|
||||
@@ -46,12 +55,8 @@ fun Context.openUrl(url: String, silent: Boolean = false): Boolean {
|
||||
}
|
||||
}
|
||||
|
||||
fun String.parseAndNormalizeUri(): Uri = Uri.parse(this).let {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
|
||||
it.normalizeScheme()
|
||||
} else {
|
||||
it
|
||||
}
|
||||
fun String.parseAndNormalizeUri(): Uri = toUri().let {
|
||||
it.normalizeScheme()
|
||||
}
|
||||
|
||||
fun Context.shareText(text: String, mimeType: String = "text/plain") {
|
||||
@@ -91,7 +96,7 @@ fun <T> Context.shareAsFile(content: T, fileName: String) {
|
||||
FileOutputStream(file).use {
|
||||
it.write(bytes)
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
} catch (_: IOException) {
|
||||
toast(R.string.error_saving_file)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ package de.markusfisch.android.binaryeye.database
|
||||
|
||||
import android.content.Context
|
||||
import android.database.Cursor
|
||||
import de.markusfisch.android.binaryeye.content.toHexString
|
||||
import de.markusfisch.android.binaryeye.io.writeExternalFile
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.OutputStream
|
||||
@@ -32,7 +31,8 @@ private fun exportCsv(
|
||||
val columns = arrayOf(
|
||||
Database.SCANS_DATETIME,
|
||||
Database.SCANS_FORMAT,
|
||||
Database.SCANS_CONTENT,
|
||||
Database.SCANS_NAME,
|
||||
Database.SCANS_TEXT,
|
||||
Database.SCANS_ERROR_CORRECTION_LEVEL,
|
||||
Database.SCANS_VERSION,
|
||||
Database.SCANS_SEQUENCE_SIZE,
|
||||
@@ -46,7 +46,7 @@ private fun exportCsv(
|
||||
val indices = columns.map {
|
||||
cursor.getColumnIndex(it)
|
||||
}
|
||||
val contentIndex = cursor.getColumnIndex(Database.SCANS_CONTENT)
|
||||
val contentIndex = cursor.getColumnIndex(Database.SCANS_TEXT)
|
||||
val rawIndex = cursor.getColumnIndex(Database.SCANS_RAW)
|
||||
outputStream.write(
|
||||
columns.joinToString(
|
||||
|
||||
@@ -1,15 +1,7 @@
|
||||
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()
|
||||
}
|
||||
import de.markusfisch.android.zxingcpp.ZxingCpp
|
||||
|
||||
fun Cursor.getInt(name: String): Int {
|
||||
val idx = getColumnIndex(name)
|
||||
@@ -30,3 +22,18 @@ fun Cursor.getBlob(name: String): ByteArray? {
|
||||
val idx = getColumnIndex(name)
|
||||
return if (idx < 0) null else getBlob(idx)
|
||||
}
|
||||
|
||||
fun Cursor.getBitMatrix(
|
||||
widthName: String,
|
||||
heightName: String,
|
||||
dataName: String
|
||||
): ZxingCpp.BitMatrix? {
|
||||
val width = getInt(widthName)
|
||||
val height = getInt(heightName)
|
||||
val data = getBlob(dataName)
|
||||
return if (width > 0 && height > 0 && data != null) {
|
||||
ZxingCpp.BitMatrix(width, height, data)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,8 @@ import android.database.Cursor
|
||||
import android.database.sqlite.SQLiteDatabase
|
||||
import android.database.sqlite.SQLiteOpenHelper
|
||||
import de.markusfisch.android.binaryeye.app.prefs
|
||||
import de.markusfisch.android.binaryeye.preference.Preferences
|
||||
import de.markusfisch.android.binaryeye.zxingcpp.migrateBarcodeFormatName
|
||||
import de.markusfisch.android.zxingcpp.ZxingCpp.BarcodeFormat
|
||||
|
||||
class Database {
|
||||
@@ -15,12 +17,12 @@ class Database {
|
||||
db = OpenHelper(context).writableDatabase
|
||||
}
|
||||
|
||||
fun getScans(query: String? = null): Cursor? = db.rawQuery(
|
||||
fun getScans(query: String? = null): Cursor = db.rawQuery(
|
||||
"""SELECT
|
||||
$SCANS_ID,
|
||||
$SCANS_DATETIME,
|
||||
$SCANS_NAME,
|
||||
$SCANS_CONTENT,
|
||||
$SCANS_TEXT,
|
||||
$SCANS_FORMAT
|
||||
FROM $SCANS
|
||||
${getWhereClause(query)}
|
||||
@@ -28,12 +30,12 @@ class Database {
|
||||
""".trimMargin(), getWhereArguments(query)
|
||||
)
|
||||
|
||||
fun getScansDetailed(query: String? = null): Cursor? = db.rawQuery(
|
||||
fun getScansDetailed(query: String? = null): Cursor = db.rawQuery(
|
||||
"""SELECT
|
||||
$SCANS_ID,
|
||||
$SCANS_DATETIME,
|
||||
$SCANS_NAME,
|
||||
$SCANS_CONTENT,
|
||||
$SCANS_TEXT,
|
||||
$SCANS_RAW,
|
||||
$SCANS_FORMAT,
|
||||
$SCANS_ERROR_CORRECTION_LEVEL,
|
||||
@@ -55,7 +57,7 @@ class Database {
|
||||
query: String?,
|
||||
prefix: String = "WHERE"
|
||||
) = if (query?.isNotEmpty() == true) {
|
||||
"""$prefix $SCANS_CONTENT LIKE ?
|
||||
"""$prefix $SCANS_TEXT LIKE ?
|
||||
OR $SCANS_NAME LIKE ?"""
|
||||
} else {
|
||||
""
|
||||
@@ -70,12 +72,12 @@ class Database {
|
||||
null
|
||||
}
|
||||
|
||||
fun getScansDetailed(ids: LongArray): Cursor? = db.rawQuery(
|
||||
fun getScansDetailed(ids: LongArray): Cursor = db.rawQuery(
|
||||
"""SELECT
|
||||
$SCANS_ID,
|
||||
$SCANS_DATETIME,
|
||||
$SCANS_NAME,
|
||||
$SCANS_CONTENT,
|
||||
$SCANS_TEXT,
|
||||
$SCANS_RAW,
|
||||
$SCANS_FORMAT,
|
||||
$SCANS_ERROR_CORRECTION_LEVEL,
|
||||
@@ -88,9 +90,9 @@ class Database {
|
||||
$SCANS_GTIN_PRICE,
|
||||
$SCANS_GTIN_ISSUE_NUMBER
|
||||
FROM $SCANS
|
||||
WHERE $SCANS_ID IN (?)
|
||||
WHERE $SCANS_ID IN (${ids.joinToString(",")})
|
||||
ORDER BY $SCANS_DATETIME DESC
|
||||
""".trimMargin(), arrayOf(ids.joinToString(","))
|
||||
""".trimMargin(), null
|
||||
)
|
||||
|
||||
fun getScan(id: Long): Scan? = db.rawQuery(
|
||||
@@ -98,11 +100,15 @@ class Database {
|
||||
$SCANS_ID,
|
||||
$SCANS_DATETIME,
|
||||
$SCANS_NAME,
|
||||
$SCANS_CONTENT,
|
||||
$SCANS_TEXT,
|
||||
$SCANS_RAW,
|
||||
$SCANS_FORMAT,
|
||||
$SCANS_ERROR_CORRECTION_LEVEL,
|
||||
$SCANS_VERSION,
|
||||
$SCANS_DATA_MASK,
|
||||
$SCANS_SYMBOL_WIDTH,
|
||||
$SCANS_SYMBOL_HEIGHT,
|
||||
$SCANS_SYMBOL_DATA,
|
||||
$SCANS_SEQUENCE_SIZE,
|
||||
$SCANS_SEQUENCE_INDEX,
|
||||
$SCANS_SEQUENCE_ID,
|
||||
@@ -113,14 +119,22 @@ class Database {
|
||||
FROM $SCANS
|
||||
WHERE $SCANS_ID = ?
|
||||
""".trimMargin(), arrayOf("$id")
|
||||
)?.use {
|
||||
).use {
|
||||
if (it.moveToFirst()) {
|
||||
Scan(
|
||||
it.getString(SCANS_CONTENT),
|
||||
it.getString(SCANS_TEXT),
|
||||
it.getBlob(SCANS_RAW),
|
||||
BarcodeFormat.valueOf(it.getString(SCANS_FORMAT)),
|
||||
BarcodeFormat.valueOf(
|
||||
it.getString(SCANS_FORMAT).migrateBarcodeFormatName()
|
||||
),
|
||||
it.getString(SCANS_ERROR_CORRECTION_LEVEL),
|
||||
it.getString(SCANS_VERSION),
|
||||
it.getInt(SCANS_DATA_MASK),
|
||||
it.getBitMatrix(
|
||||
SCANS_SYMBOL_WIDTH,
|
||||
SCANS_SYMBOL_HEIGHT,
|
||||
SCANS_SYMBOL_DATA
|
||||
),
|
||||
it.getInt(SCANS_SEQUENCE_SIZE),
|
||||
it.getInt(SCANS_SEQUENCE_INDEX),
|
||||
it.getString(SCANS_SEQUENCE_ID),
|
||||
@@ -129,46 +143,66 @@ class Database {
|
||||
it.getString(SCANS_GTIN_PRICE),
|
||||
it.getString(SCANS_GTIN_ISSUE_NUMBER),
|
||||
it.getString(SCANS_DATETIME),
|
||||
it.getLong(SCANS_ID)
|
||||
it.getLong(SCANS_ID),
|
||||
it.getString(SCANS_NAME),
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun insertScan(scan: Scan): Long = db.insert(
|
||||
SCANS,
|
||||
null,
|
||||
ContentValues().apply {
|
||||
put(SCANS_DATETIME, scan.dateTime)
|
||||
put(SCANS_CONTENT, scan.content)
|
||||
if (scan.raw != null) {
|
||||
put(SCANS_RAW, scan.raw)
|
||||
}
|
||||
put(SCANS_FORMAT, scan.format.name)
|
||||
scan.errorCorrectionLevel?.let {
|
||||
put(SCANS_ERROR_CORRECTION_LEVEL, it)
|
||||
}
|
||||
put(SCANS_VERSION, scan.version)
|
||||
put(SCANS_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) {
|
||||
fun insertScan(scan: Scan): Long {
|
||||
when (prefs.ignoreDuplicates()) {
|
||||
Preferences.Companion.IgnoreDuplicates.Consecutive -> {
|
||||
val id = getIdOfLastScan(
|
||||
get(SCANS_CONTENT) as String,
|
||||
get(SCANS_RAW) as ByteArray?,
|
||||
scan.text,
|
||||
scan.raw,
|
||||
scan.format
|
||||
)
|
||||
if (id > 0L) {
|
||||
return id
|
||||
}
|
||||
}
|
||||
|
||||
Preferences.Companion.IgnoreDuplicates.Any -> {
|
||||
val id = getIdOfScanByContent(scan.text, scan.format)
|
||||
if (id > 0L) {
|
||||
return id
|
||||
}
|
||||
}
|
||||
|
||||
else -> Unit
|
||||
}
|
||||
)
|
||||
return db.insert(
|
||||
SCANS,
|
||||
null,
|
||||
ContentValues().apply {
|
||||
put(SCANS_DATETIME, scan.dateTime)
|
||||
put(SCANS_TEXT, scan.text)
|
||||
if (scan.raw != null) {
|
||||
put(SCANS_RAW, scan.raw)
|
||||
}
|
||||
put(SCANS_FORMAT, scan.format.name)
|
||||
scan.errorCorrectionLevel?.let {
|
||||
put(SCANS_ERROR_CORRECTION_LEVEL, it)
|
||||
}
|
||||
put(SCANS_VERSION, scan.version)
|
||||
put(SCANS_DATA_MASK, scan.dataMask)
|
||||
if (scan.symbol != null) {
|
||||
put(SCANS_SYMBOL_WIDTH, scan.symbol.width)
|
||||
put(SCANS_SYMBOL_HEIGHT, scan.symbol.height)
|
||||
put(SCANS_SYMBOL_DATA, scan.symbol.data)
|
||||
}
|
||||
put(SCANS_SEQUENCE_SIZE, scan.sequenceSize)
|
||||
put(SCANS_SEQUENCE_INDEX, scan.sequenceIndex)
|
||||
put(SCANS_SEQUENCE_ID, scan.sequenceId)
|
||||
scan.country?.let { put(SCANS_GTIN_COUNTRY, it) }
|
||||
scan.addOn?.let { put(SCANS_GTIN_ADD_ON, it) }
|
||||
scan.price?.let { put(SCANS_GTIN_PRICE, it) }
|
||||
scan.issueNumber?.let { put(SCANS_GTIN_ISSUE_NUMBER, it) }
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun getIdOfLastScan(
|
||||
content: String,
|
||||
@@ -177,17 +211,17 @@ class Database {
|
||||
): Long = db.rawQuery(
|
||||
"""SELECT
|
||||
$SCANS_ID,
|
||||
$SCANS_CONTENT,
|
||||
$SCANS_TEXT,
|
||||
$SCANS_RAW,
|
||||
$SCANS_FORMAT
|
||||
FROM $SCANS
|
||||
ORDER BY $SCANS_ID DESC
|
||||
LIMIT 1
|
||||
""".trimMargin(), null
|
||||
)?.use {
|
||||
).use {
|
||||
if (it.count > 0 &&
|
||||
it.moveToFirst() &&
|
||||
it.getString(SCANS_CONTENT) == content &&
|
||||
it.getString(SCANS_TEXT) == content &&
|
||||
(raw == null || it.getBlob(SCANS_RAW)
|
||||
?.contentEquals(raw) == true) &&
|
||||
it.getString(SCANS_FORMAT) == format.name
|
||||
@@ -196,7 +230,26 @@ class Database {
|
||||
} else {
|
||||
0L
|
||||
}
|
||||
} ?: 0L
|
||||
}
|
||||
|
||||
private fun getIdOfScanByContent(
|
||||
content: String,
|
||||
format: BarcodeFormat
|
||||
): Long = db.rawQuery(
|
||||
"""SELECT
|
||||
$SCANS_ID
|
||||
FROM $SCANS
|
||||
WHERE $SCANS_TEXT = ?
|
||||
AND $SCANS_FORMAT = ?
|
||||
LIMIT 1
|
||||
""".trimMargin(), arrayOf(content, format.name)
|
||||
).use {
|
||||
if (it.moveToFirst()) {
|
||||
it.getLong(SCANS_ID)
|
||||
} else {
|
||||
0L
|
||||
}
|
||||
}
|
||||
|
||||
fun removeScan(id: Long) {
|
||||
db.delete(SCANS, "$SCANS_ID = ?", arrayOf("$id"))
|
||||
@@ -213,7 +266,7 @@ class Database {
|
||||
}
|
||||
|
||||
private class OpenHelper(context: Context) :
|
||||
SQLiteOpenHelper(context, FILE_NAME, null, 6) {
|
||||
SQLiteOpenHelper(context, FILE_NAME, null, 9) {
|
||||
override fun onCreate(db: SQLiteDatabase) {
|
||||
db.createScans()
|
||||
}
|
||||
@@ -238,6 +291,15 @@ class Database {
|
||||
if (oldVersion < 6) {
|
||||
db.migrateToVersionString()
|
||||
}
|
||||
if (oldVersion < 7) {
|
||||
db.addDataMaskColumn()
|
||||
}
|
||||
if (oldVersion < 8) {
|
||||
db.addSymbolColumns()
|
||||
}
|
||||
if (oldVersion < 9) {
|
||||
db.migrateToNativeFormatNames()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -247,12 +309,16 @@ class Database {
|
||||
const val SCANS_ID = "_id"
|
||||
const val SCANS_DATETIME = "_datetime"
|
||||
const val SCANS_NAME = "name"
|
||||
const val SCANS_CONTENT = "content"
|
||||
const val SCANS_TEXT = "content"
|
||||
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_VERSION = "version"
|
||||
const val SCANS_DATA_MASK = "data_mask"
|
||||
const val SCANS_SYMBOL_WIDTH = "symbol_width"
|
||||
const val SCANS_SYMBOL_HEIGHT = "symbol_height"
|
||||
const val SCANS_SYMBOL_DATA = "symbol_data"
|
||||
const val SCANS_ISSUE_NUMBER = "issue_number"
|
||||
const val SCANS_ORIENTATION = "orientation"
|
||||
const val SCANS_OTHER_META_DATA = "other_meta_data"
|
||||
@@ -275,11 +341,15 @@ class Database {
|
||||
$SCANS_ID INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
$SCANS_DATETIME DATETIME NOT NULL,
|
||||
$SCANS_NAME TEXT,
|
||||
$SCANS_CONTENT TEXT NOT NULL,
|
||||
$SCANS_TEXT TEXT NOT NULL,
|
||||
$SCANS_RAW BLOB,
|
||||
$SCANS_FORMAT TEXT NOT NULL,
|
||||
$SCANS_ERROR_CORRECTION_LEVEL TEXT,
|
||||
$SCANS_VERSION TEXT,
|
||||
$SCANS_DATA_MASK INTEGER,
|
||||
$SCANS_SYMBOL_WIDTH INTEGER,
|
||||
$SCANS_SYMBOL_HEIGHT INTEGER,
|
||||
$SCANS_SYMBOL_DATA BLOB,
|
||||
$SCANS_SEQUENCE_SIZE INTEGER,
|
||||
$SCANS_SEQUENCE_INDEX INTEGER,
|
||||
$SCANS_SEQUENCE_ID TEXT,
|
||||
@@ -307,7 +377,7 @@ class Database {
|
||||
}
|
||||
|
||||
private fun SQLiteDatabase.addNameColumn() {
|
||||
execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_NAME TEXT".trimMargin())
|
||||
execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_NAME TEXT")
|
||||
}
|
||||
|
||||
private fun SQLiteDatabase.migrateToZxingCpp() {
|
||||
@@ -327,9 +397,57 @@ class Database {
|
||||
execSQL("UPDATE $SCANS SET $SCANS_GTIN_ISSUE_NUMBER = $SCANS_ISSUE_NUMBER")
|
||||
}
|
||||
|
||||
private fun SQLiteDatabase.migrateToNativeFormatNames() {
|
||||
val mappings = listOf(
|
||||
"NONE" to "None",
|
||||
"AZTEC" to "Aztec",
|
||||
"CODABAR" to "Codabar",
|
||||
"CODE_39" to "Code39",
|
||||
"CODE_39_EXT" to "Code39Ext",
|
||||
"CODE_32" to "Code32",
|
||||
"PZN" to "PZN",
|
||||
"CODE_93" to "Code93",
|
||||
"CODE_128" to "Code128",
|
||||
"DATA_BAR_OMNI" to "DataBarOmni",
|
||||
"DATA_BAR_STK" to "DataBarStk",
|
||||
"DATA_BAR_LTD" to "DataBarLtd",
|
||||
"DATA_BAR_EXPANDED" to "DataBarExp",
|
||||
"DATA_BAR_EXP_STK" to "DataBarExpStk",
|
||||
"DATA_MATRIX" to "DataMatrix",
|
||||
"DX_FILM_EDGE" to "DXFilmEdge",
|
||||
"EAN_8" to "EAN8",
|
||||
"EAN_13" to "EAN13",
|
||||
"ITF" to "ITF",
|
||||
"MAXICODE" to "MaxiCode",
|
||||
"PDF_417" to "PDF417",
|
||||
"QR_CODE" to "QRCode",
|
||||
"MICRO_QR_CODE" to "MicroQRCode",
|
||||
"RMQR_CODE" to "RMQRCode",
|
||||
"UPC_A" to "UPCA",
|
||||
"UPC_E" to "UPCE"
|
||||
)
|
||||
for ((oldName, newName) in mappings) {
|
||||
execSQL(
|
||||
"UPDATE $SCANS SET $SCANS_FORMAT = ? WHERE $SCANS_FORMAT = ?",
|
||||
arrayOf(newName, oldName)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun SQLiteDatabase.migrateToVersionString() {
|
||||
execSQL("ALTER TABLE $SCANS ADD $SCANS_VERSION TEXT")
|
||||
execSQL("UPDATE $SCANS SET $SCANS_VERSION = $SCANS_VERSION_NUMBER")
|
||||
}
|
||||
|
||||
private fun SQLiteDatabase.addDataMaskColumn() {
|
||||
execSQL("ALTER TABLE $SCANS ADD $SCANS_DATA_MASK INTEGER")
|
||||
execSQL("UPDATE $SCANS SET $SCANS_DATA_MASK = -1")
|
||||
}
|
||||
|
||||
private fun SQLiteDatabase.addSymbolColumns() {
|
||||
execSQL("ALTER TABLE $SCANS ADD $SCANS_SYMBOL_WIDTH INTEGER")
|
||||
execSQL("ALTER TABLE $SCANS ADD $SCANS_SYMBOL_HEIGHT INTEGER")
|
||||
execSQL("ALTER TABLE $SCANS ADD $SCANS_SYMBOL_DATA BLOB")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ package de.markusfisch.android.binaryeye.database
|
||||
|
||||
import android.content.Context
|
||||
import android.database.Cursor
|
||||
import de.markusfisch.android.binaryeye.content.toHexString
|
||||
import de.markusfisch.android.binaryeye.io.writeExternalFile
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONException
|
||||
@@ -24,7 +23,8 @@ fun Cursor.exportJson(): String? {
|
||||
val columns = arrayOf(
|
||||
Database.SCANS_DATETIME,
|
||||
Database.SCANS_FORMAT,
|
||||
Database.SCANS_CONTENT,
|
||||
Database.SCANS_NAME,
|
||||
Database.SCANS_TEXT,
|
||||
Database.SCANS_ERROR_CORRECTION_LEVEL,
|
||||
Database.SCANS_VERSION,
|
||||
Database.SCANS_SEQUENCE_SIZE,
|
||||
@@ -38,7 +38,7 @@ fun Cursor.exportJson(): String? {
|
||||
val indices = columns.map {
|
||||
Pair(getColumnIndex(it), it)
|
||||
}
|
||||
val contentIndex = getColumnIndex(Database.SCANS_CONTENT)
|
||||
val contentIndex = getColumnIndex(Database.SCANS_TEXT)
|
||||
val rawIndex = getColumnIndex(Database.SCANS_RAW)
|
||||
val root = JSONArray()
|
||||
do {
|
||||
@@ -69,7 +69,7 @@ private fun Cursor.toJsonObject(
|
||||
obj.put(it.second, value ?: "")
|
||||
}
|
||||
obj
|
||||
} catch (e: JSONException) {
|
||||
} catch (_: JSONException) {
|
||||
obj
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,21 +1,22 @@
|
||||
package de.markusfisch.android.binaryeye.database
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.os.Build
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
import android.os.Bundle
|
||||
import android.text.format.DateFormat
|
||||
import de.markusfisch.android.zxingcpp.ZxingCpp
|
||||
import de.markusfisch.android.zxingcpp.ZxingCpp.ContentType
|
||||
import de.markusfisch.android.binaryeye.zxingcpp.migrateBarcodeFormatName
|
||||
import de.markusfisch.android.zxingcpp.ZxingCpp.BarcodeFormat
|
||||
import de.markusfisch.android.zxingcpp.ZxingCpp.BitMatrix
|
||||
import de.markusfisch.android.zxingcpp.ZxingCpp.ContentType
|
||||
import de.markusfisch.android.zxingcpp.ZxingCpp.Result
|
||||
import java.util.Locale
|
||||
|
||||
data class Scan(
|
||||
val content: String,
|
||||
val text: String,
|
||||
val raw: ByteArray?,
|
||||
val format: BarcodeFormat,
|
||||
val errorCorrectionLevel: String? = null,
|
||||
val version: String? = null,
|
||||
val dataMask: Int = -1,
|
||||
val symbol: BitMatrix? = null,
|
||||
val sequenceSize: Int = -1,
|
||||
val sequenceIndex: Int = -1,
|
||||
val sequenceId: String = "",
|
||||
@@ -24,8 +25,9 @@ data class Scan(
|
||||
val price: String? = null,
|
||||
val issueNumber: String? = null,
|
||||
val dateTime: String = getDateTime(),
|
||||
var id: Long = 0L
|
||||
) : Parcelable {
|
||||
var id: Long = 0L,
|
||||
val label: String? = null
|
||||
) {
|
||||
// Needs to be overwritten manually, as ByteArray is an array and
|
||||
// this isn't handled well by Kotlin.
|
||||
override fun equals(other: Any?): Boolean {
|
||||
@@ -40,12 +42,14 @@ data class Scan(
|
||||
|
||||
return id == other.id &&
|
||||
dateTime == other.dateTime &&
|
||||
content == other.content &&
|
||||
text == other.text &&
|
||||
((raw == null && other.raw == null) ||
|
||||
(raw != null && other.raw != null && raw.contentEquals(other.raw))) &&
|
||||
(raw != null && other.raw != null &&
|
||||
raw.contentEquals(other.raw))) &&
|
||||
format == other.format &&
|
||||
errorCorrectionLevel == other.errorCorrectionLevel &&
|
||||
version == other.version &&
|
||||
dataMask == other.dataMask &&
|
||||
sequenceSize == other.sequenceSize &&
|
||||
sequenceIndex == other.sequenceIndex &&
|
||||
sequenceId == other.sequenceId &&
|
||||
@@ -60,11 +64,12 @@ data class Scan(
|
||||
override fun hashCode(): Int {
|
||||
var result = id.hashCode()
|
||||
result = 31 * result + dateTime.hashCode()
|
||||
result = 31 * result + content.hashCode()
|
||||
result = 31 * result + text.hashCode()
|
||||
result = 31 * result + (raw?.contentHashCode() ?: 0)
|
||||
result = 31 * result + format.hashCode()
|
||||
result = 31 * result + (errorCorrectionLevel?.hashCode() ?: 0)
|
||||
result = 31 * result + version.hashCode()
|
||||
result = 31 * result + dataMask
|
||||
result = 31 * result + sequenceSize
|
||||
result = 31 * result + sequenceIndex
|
||||
result = 31 * result + sequenceId.hashCode()
|
||||
@@ -75,51 +80,6 @@ data class Scan(
|
||||
return result
|
||||
}
|
||||
|
||||
private constructor(parcel: Parcel) : this(
|
||||
content = parcel.readString() ?: "",
|
||||
raw = parcel.readSizedByteArray(),
|
||||
format = BarcodeFormat.valueOf(parcel.readString() ?: ""),
|
||||
errorCorrectionLevel = parcel.readString(),
|
||||
version = parcel.readString(),
|
||||
sequenceSize = parcel.readInt(),
|
||||
sequenceIndex = parcel.readInt(),
|
||||
sequenceId = parcel.readString() ?: "",
|
||||
country = parcel.readString(),
|
||||
addOn = parcel.readString(),
|
||||
price = parcel.readString(),
|
||||
issueNumber = parcel.readString(),
|
||||
dateTime = parcel.readString() ?: "",
|
||||
id = parcel.readLong()
|
||||
)
|
||||
|
||||
override fun writeToParcel(parcel: Parcel, flags: Int) {
|
||||
parcel.apply {
|
||||
writeString(content)
|
||||
writeSizedByteArray(raw)
|
||||
writeString(format.name)
|
||||
writeString(errorCorrectionLevel)
|
||||
writeString(version)
|
||||
writeInt(sequenceSize)
|
||||
writeInt(sequenceIndex)
|
||||
writeString(sequenceId)
|
||||
writeString(country)
|
||||
writeString(addOn)
|
||||
writeString(price)
|
||||
writeString(issueNumber)
|
||||
writeString(dateTime)
|
||||
writeLong(id)
|
||||
}
|
||||
}
|
||||
|
||||
override fun describeContents() = 0
|
||||
|
||||
companion object {
|
||||
@JvmField
|
||||
val CREATOR = object : Parcelable.Creator<Scan> {
|
||||
override fun createFromParcel(parcel: Parcel) = Scan(parcel)
|
||||
override fun newArray(size: Int) = arrayOfNulls<Scan>(size)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun Result.toScan(): Scan {
|
||||
@@ -129,12 +89,14 @@ fun Result.toScan(): Scan {
|
||||
ContentType.ISO15434,
|
||||
ContentType.TEXT -> text
|
||||
|
||||
else -> ""
|
||||
else -> "" // Show as binary.
|
||||
},
|
||||
rawBytes,
|
||||
format,
|
||||
ecLevel,
|
||||
version,
|
||||
dataMask,
|
||||
symbol,
|
||||
sequenceSize,
|
||||
sequenceIndex,
|
||||
sequenceId,
|
||||
@@ -145,65 +107,95 @@ fun Result.toScan(): Scan {
|
||||
)
|
||||
}
|
||||
|
||||
data class Recreation(
|
||||
val format: BarcodeFormat,
|
||||
val ecLevel: Int,
|
||||
val size: Int,
|
||||
val margin: Int
|
||||
) {
|
||||
fun <T> encode(content: T): Bitmap? = try {
|
||||
ZxingCpp.encodeAsBitmap(
|
||||
content, format, size, size, margin, ecLevel
|
||||
fun Scan.toBundle() = Bundle().apply {
|
||||
putString(SCAN_TEXT, text)
|
||||
putByteArray(SCAN_RAW, raw)
|
||||
putString(SCAN_FORMAT, format.name)
|
||||
putString(SCAN_ERROR_CORRECTION_LEVEL, errorCorrectionLevel)
|
||||
putString(SCAN_VERSION, version)
|
||||
putInt(SCAN_DATA_MASK, dataMask)
|
||||
putInt(SCAN_SYMBOL_WIDTH, symbol?.width ?: 0)
|
||||
putInt(SCAN_SYMBOL_HEIGHT, symbol?.height ?: 0)
|
||||
putByteArray(SCAN_SYMBOL_DATA, symbol?.data)
|
||||
putInt(SCAN_SEQUENCE_SIZE, sequenceSize)
|
||||
putInt(SCAN_SEQUENCE_INDEX, sequenceIndex)
|
||||
putString(SCAN_SEQUENCE_ID, sequenceId)
|
||||
putString(SCAN_COUNTRY, country)
|
||||
putString(SCAN_ADD_ON, addOn)
|
||||
putString(SCAN_PRICE, price)
|
||||
putString(SCAN_ISSUE_NUMBER, issueNumber)
|
||||
putString(SCAN_DATE_TIME, dateTime)
|
||||
putLong(SCAN_ID, id)
|
||||
putString(SCAN_LABEL, label)
|
||||
}
|
||||
|
||||
fun Bundle.toScan(): Scan? {
|
||||
val formatName = getString(SCAN_FORMAT)?.migrateBarcodeFormatName()
|
||||
if (formatName.isNullOrEmpty()) {
|
||||
return null
|
||||
}
|
||||
return try {
|
||||
Scan(
|
||||
text = getString(SCAN_TEXT) ?: "",
|
||||
raw = getByteArray(SCAN_RAW),
|
||||
format = BarcodeFormat.valueOf(formatName),
|
||||
errorCorrectionLevel = getString(SCAN_ERROR_CORRECTION_LEVEL),
|
||||
version = getString(SCAN_VERSION),
|
||||
dataMask = getInt(SCAN_DATA_MASK, -1),
|
||||
symbol = getBitMatrix(),
|
||||
sequenceSize = getInt(SCAN_SEQUENCE_SIZE, -1),
|
||||
sequenceIndex = getInt(SCAN_SEQUENCE_INDEX, -1),
|
||||
sequenceId = getString(SCAN_SEQUENCE_ID) ?: "",
|
||||
country = getString(SCAN_COUNTRY),
|
||||
addOn = getString(SCAN_ADD_ON),
|
||||
price = getString(SCAN_PRICE),
|
||||
issueNumber = getString(SCAN_ISSUE_NUMBER),
|
||||
dateTime = getString(SCAN_DATE_TIME) ?: getDateTime(),
|
||||
id = getLong(SCAN_ID, 0L),
|
||||
label = getString(SCAN_LABEL)
|
||||
)
|
||||
} catch (_: RuntimeException) {
|
||||
} catch (_: IllegalArgumentException) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun Scan.toRecreation(
|
||||
size: Int = 128,
|
||||
margin: Int = -1
|
||||
) = Recreation(
|
||||
format,
|
||||
when (errorCorrectionLevel) {
|
||||
"L" -> 0
|
||||
"M" -> 4
|
||||
"Q" -> 6
|
||||
"H" -> 8
|
||||
else -> -1
|
||||
},
|
||||
size,
|
||||
margin
|
||||
)
|
||||
|
||||
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"
|
||||
},
|
||||
"yyyy-MM-dd HH:mm:ss",
|
||||
time
|
||||
).toString()
|
||||
).toString() + String.format(
|
||||
Locale.getDefault(),
|
||||
":%03d",
|
||||
time % 1000
|
||||
)
|
||||
|
||||
private fun Parcel.writeSizedByteArray(array: ByteArray?) {
|
||||
val size = array?.size ?: 0
|
||||
writeInt(size)
|
||||
if (size > 0) {
|
||||
writeByteArray(array)
|
||||
private fun Bundle.getBitMatrix(): BitMatrix? {
|
||||
val width = getInt(SCAN_SYMBOL_WIDTH, 0)
|
||||
val height = getInt(SCAN_SYMBOL_HEIGHT, 0)
|
||||
val data = getByteArray(SCAN_SYMBOL_DATA)
|
||||
if (width < 1 || height < 1 || data == null) {
|
||||
return null
|
||||
}
|
||||
return BitMatrix(width, height, data)
|
||||
}
|
||||
|
||||
private fun Parcel.readSizedByteArray(): ByteArray? {
|
||||
val size = readInt()
|
||||
return if (size > 0) {
|
||||
val array = ByteArray(size)
|
||||
readByteArray(array)
|
||||
array
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
private const val SCAN_TEXT = "text"
|
||||
private const val SCAN_RAW = "raw"
|
||||
private const val SCAN_FORMAT = "format"
|
||||
private const val SCAN_ERROR_CORRECTION_LEVEL = "error_correction_level"
|
||||
private const val SCAN_VERSION = "version"
|
||||
private const val SCAN_DATA_MASK = "data_mask"
|
||||
private const val SCAN_SYMBOL_WIDTH = "symbol_width"
|
||||
private const val SCAN_SYMBOL_HEIGHT = "symbol_height"
|
||||
private const val SCAN_SYMBOL_DATA = "symbol_data"
|
||||
private const val SCAN_SEQUENCE_SIZE = "sequence_size"
|
||||
private const val SCAN_SEQUENCE_INDEX = "sequence_index"
|
||||
private const val SCAN_SEQUENCE_ID = "sequence_id"
|
||||
private const val SCAN_COUNTRY = "country"
|
||||
private const val SCAN_ADD_ON = "add_on"
|
||||
private const val SCAN_PRICE = "price"
|
||||
private const val SCAN_ISSUE_NUMBER = "issue_number"
|
||||
private const val SCAN_DATE_TIME = "date_time"
|
||||
private const val SCAN_ID = "id"
|
||||
private const val SCAN_LABEL = "label"
|
||||
|
||||
@@ -1,377 +0,0 @@
|
||||
package de.markusfisch.android.binaryeye.fragment
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.AlertDialog
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.support.v4.app.Fragment
|
||||
import android.view.*
|
||||
import android.widget.EditText
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.app.hasWritePermission
|
||||
import de.markusfisch.android.binaryeye.content.copyToClipboard
|
||||
import de.markusfisch.android.binaryeye.content.shareFile
|
||||
import de.markusfisch.android.binaryeye.content.shareText
|
||||
import de.markusfisch.android.binaryeye.graphics.COLOR_BLACK
|
||||
import de.markusfisch.android.binaryeye.graphics.COLOR_WHITE
|
||||
import de.markusfisch.android.binaryeye.io.addSuffixIfNotGiven
|
||||
import de.markusfisch.android.binaryeye.io.toSaveResult
|
||||
import de.markusfisch.android.binaryeye.io.writeExternalFile
|
||||
import de.markusfisch.android.binaryeye.view.doOnApplyWindowInsets
|
||||
import de.markusfisch.android.binaryeye.view.setPaddingFromWindowInsets
|
||||
import de.markusfisch.android.binaryeye.widget.ConfinedScalingImageView
|
||||
import de.markusfisch.android.binaryeye.widget.toast
|
||||
import de.markusfisch.android.zxingcpp.ZxingCpp
|
||||
import de.markusfisch.android.zxingcpp.ZxingCpp.BarcodeFormat
|
||||
import kotlinx.coroutines.*
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
import java.io.OutputStream
|
||||
import java.util.*
|
||||
import kotlin.math.min
|
||||
|
||||
class BarcodeFragment : Fragment() {
|
||||
private enum class FileType {
|
||||
PNG, SVG, TXT
|
||||
}
|
||||
|
||||
private val parentJob = Job()
|
||||
private val scope = CoroutineScope(Dispatchers.IO + parentJob)
|
||||
|
||||
private lateinit var barcode: Barcode<*>
|
||||
|
||||
override fun onCreate(state: Bundle?) {
|
||||
super.onCreate(state)
|
||||
setHasOptionsMenu(true)
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
state: Bundle?
|
||||
): View? {
|
||||
val ac = activity ?: return null
|
||||
ac.setTitle(R.string.view_barcode)
|
||||
|
||||
val view = inflater.inflate(
|
||||
R.layout.fragment_barcode,
|
||||
container,
|
||||
false
|
||||
)
|
||||
|
||||
val bitmap: Bitmap
|
||||
try {
|
||||
barcode = arguments?.toBarcode() ?: throw IllegalArgumentException(
|
||||
"Illegal arguments"
|
||||
)
|
||||
// Catch exceptions from encoding.
|
||||
bitmap = barcode.bitmap()
|
||||
} catch (e: Exception) {
|
||||
var message = e.message
|
||||
if (message.isNullOrEmpty()) {
|
||||
message = getString(R.string.error_encoding_barcode)
|
||||
}
|
||||
message?.let {
|
||||
ac.toast(message)
|
||||
}
|
||||
fragmentManager.popBackStack()
|
||||
return null
|
||||
}
|
||||
|
||||
val imageView = view.findViewById<ConfinedScalingImageView>(
|
||||
R.id.barcode
|
||||
)
|
||||
imageView.setImageBitmap(bitmap)
|
||||
imageView.post {
|
||||
// Make sure to invoke this after ScalingImageView.onLayout().
|
||||
imageView.minWidth = min(
|
||||
imageView.minWidth / 2f,
|
||||
barcode.size.toFloat()
|
||||
)
|
||||
}
|
||||
|
||||
view.findViewById<View>(R.id.share).setOnClickListener {
|
||||
it.context?.apply {
|
||||
pickFileType(R.string.share_as) { fileType ->
|
||||
shareAs(fileType)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
(view.findViewById(R.id.inset_layout) as View).setPaddingFromWindowInsets()
|
||||
imageView.doOnApplyWindowInsets { v, insets ->
|
||||
(v as ConfinedScalingImageView).insets.set(insets)
|
||||
}
|
||||
|
||||
return view
|
||||
}
|
||||
|
||||
private fun Bundle.toBarcode() = Barcode(
|
||||
getString(CONTENT_TEXT) ?: getByteArray(CONTENT_RAW) ?: throw IllegalArgumentException(
|
||||
"content cannot be null"
|
||||
),
|
||||
BarcodeFormat.valueOf(
|
||||
getString(FORMAT) ?: throw IllegalArgumentException(
|
||||
"format cannot be null"
|
||||
)
|
||||
),
|
||||
getInt(SIZE),
|
||||
getInt(MARGIN),
|
||||
getInt(EC_LEVEL),
|
||||
Colors.values()[getInt(COLORS)]
|
||||
)
|
||||
|
||||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
parentJob.cancel()
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
inflater.inflate(R.menu.fragment_barcode, menu)
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
return when (item.itemId) {
|
||||
R.id.copy_to_clipboard -> {
|
||||
context.apply {
|
||||
copyToClipboard(barcode.text())
|
||||
toast(R.string.copied_to_clipboard)
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
R.id.export_to_file -> {
|
||||
context.pickFileType(R.string.export_as) {
|
||||
askForFileNameAndSave(it)
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
}
|
||||
|
||||
private fun Context.pickFileType(
|
||||
title: Int,
|
||||
action: (FileType) -> Unit
|
||||
) {
|
||||
val fileTypes = FileType.values()
|
||||
AlertDialog.Builder(this)
|
||||
.setTitle(title)
|
||||
.setItems(fileTypes.map { it.name }.toTypedArray()) { _, which ->
|
||||
action(fileTypes[which])
|
||||
}
|
||||
.show()
|
||||
}
|
||||
|
||||
// Dialogs do not have a parent view.
|
||||
@SuppressLint("InflateParams")
|
||||
private fun askForFileNameAndSave(fileType: FileType) {
|
||||
val ac = activity ?: return
|
||||
// Write permission is only required before Android Q.
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q &&
|
||||
!ac.hasWritePermission { askForFileNameAndSave(fileType) }
|
||||
) {
|
||||
return
|
||||
}
|
||||
val view = ac.layoutInflater.inflate(R.layout.dialog_save_file, null)
|
||||
val editText = view.findViewById<EditText>(R.id.file_name)
|
||||
editText.setText(
|
||||
encodeFileName("${barcode.format}_${barcode.content}")
|
||||
)
|
||||
AlertDialog.Builder(ac)
|
||||
.setView(view)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
val fileName = editText.text.toString()
|
||||
when (fileType) {
|
||||
FileType.PNG -> saveAs(
|
||||
addSuffixIfNotGiven(fileName, ".png"),
|
||||
MIME_PNG
|
||||
) {
|
||||
barcode.bitmap().saveAsPng(it)
|
||||
}
|
||||
|
||||
FileType.SVG -> saveAs(
|
||||
addSuffixIfNotGiven(fileName, ".svg"),
|
||||
MIME_SVG
|
||||
) { outputStream ->
|
||||
outputStream.write(barcode.svg().toByteArray())
|
||||
}
|
||||
|
||||
FileType.TXT -> saveAs(
|
||||
addSuffixIfNotGiven(fileName, ".txt"),
|
||||
MIME_TXT
|
||||
) { outputStream ->
|
||||
outputStream.write(barcode.text().toByteArray())
|
||||
}
|
||||
}
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||
}
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun saveAs(
|
||||
fileName: String,
|
||||
mimeType: String,
|
||||
write: (outputStream: OutputStream) -> Unit
|
||||
) {
|
||||
val ac = activity ?: return
|
||||
scope.launch(Dispatchers.IO) {
|
||||
val message = ac.writeExternalFile(fileName, mimeType, write).toSaveResult()
|
||||
withContext(Dispatchers.Main) {
|
||||
ac.toast(message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun Context.shareAs(fileType: FileType) {
|
||||
when (fileType) {
|
||||
FileType.PNG -> share(barcode.bitmap())
|
||||
FileType.SVG -> shareText(barcode.svg(), MIME_SVG)
|
||||
FileType.TXT -> shareText(barcode.text())
|
||||
}
|
||||
}
|
||||
|
||||
private fun Context.share(bitmap: Bitmap) {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
val file = File(
|
||||
externalCacheDir,
|
||||
"shared_barcode.png"
|
||||
)
|
||||
val success = try {
|
||||
FileOutputStream(file).use {
|
||||
bitmap.saveAsPng(it)
|
||||
}
|
||||
true
|
||||
} catch (e: IOException) {
|
||||
false
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
if (success) {
|
||||
shareFile(file, "image/png")
|
||||
} else {
|
||||
toast(R.string.error_saving_file)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val CONTENT_TEXT = "content_text"
|
||||
private const val CONTENT_RAW = "content_raw"
|
||||
private const val FORMAT = "format"
|
||||
private const val SIZE = "size"
|
||||
private const val MARGIN = "margin"
|
||||
private const val EC_LEVEL = "ec_level"
|
||||
private const val COLORS = "colors"
|
||||
private const val MIME_PNG = "image/png"
|
||||
private const val MIME_SVG = "image/svg+xmg"
|
||||
private const val MIME_TXT = "text/plain"
|
||||
|
||||
fun <T> newInstance(
|
||||
content: T,
|
||||
format: BarcodeFormat,
|
||||
size: Int,
|
||||
margin: Int,
|
||||
ecLevel: Int = -1,
|
||||
colors: Int = 0
|
||||
): Fragment {
|
||||
val args = Bundle()
|
||||
when (content) {
|
||||
is String -> {
|
||||
args.putString(CONTENT_TEXT, content)
|
||||
}
|
||||
|
||||
is ByteArray -> {
|
||||
args.putByteArray(CONTENT_RAW, content)
|
||||
}
|
||||
|
||||
else -> {
|
||||
throw IllegalArgumentException(
|
||||
"content must be a String of a ByteArray"
|
||||
)
|
||||
}
|
||||
}
|
||||
args.putString(FORMAT, format.name)
|
||||
args.putInt(SIZE, size)
|
||||
args.putInt(MARGIN, margin)
|
||||
args.putInt(EC_LEVEL, ecLevel)
|
||||
args.putInt(COLORS, colors)
|
||||
val fragment = BarcodeFragment()
|
||||
fragment.arguments = args
|
||||
return fragment
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private data class Barcode<T>(
|
||||
val content: T,
|
||||
val format: BarcodeFormat,
|
||||
val size: Int,
|
||||
val margin: Int,
|
||||
val ecLevel: Int,
|
||||
val colors: Colors
|
||||
) {
|
||||
private var _bitmap: Bitmap? = null
|
||||
fun bitmap(): Bitmap {
|
||||
val b = _bitmap ?: ZxingCpp.encodeAsBitmap(
|
||||
content, format, size, size, margin, ecLevel,
|
||||
setColor = colors.foregroundColor(),
|
||||
unsetColor = colors.backgroundColor()
|
||||
)
|
||||
_bitmap = b
|
||||
return b
|
||||
}
|
||||
|
||||
private var _svg: String? = null
|
||||
fun svg(): String {
|
||||
val s = _svg ?: ZxingCpp.encodeAsSvg(
|
||||
content, format, margin, ecLevel
|
||||
)
|
||||
_svg = s
|
||||
return s
|
||||
}
|
||||
|
||||
private var _text: String? = null
|
||||
fun text(): String {
|
||||
val t = _text ?: ZxingCpp.encodeAsText(
|
||||
content, format, margin, ecLevel,
|
||||
inverted = colors == Colors.BLACK_ON_WHITE
|
||||
)
|
||||
_text = t
|
||||
return t
|
||||
}
|
||||
}
|
||||
|
||||
private enum class Colors {
|
||||
BLACK_ON_WHITE,
|
||||
WHITE_ON_BLACK,
|
||||
BLACK_ON_TRANSPARENT,
|
||||
WHITE_ON_TRANSPARENT;
|
||||
|
||||
fun foregroundColor(): Int = when (this) {
|
||||
BLACK_ON_WHITE,
|
||||
BLACK_ON_TRANSPARENT -> COLOR_BLACK
|
||||
|
||||
WHITE_ON_BLACK,
|
||||
WHITE_ON_TRANSPARENT -> COLOR_WHITE
|
||||
}
|
||||
|
||||
fun backgroundColor(): Int = when (this) {
|
||||
BLACK_ON_WHITE -> COLOR_WHITE
|
||||
WHITE_ON_BLACK -> COLOR_BLACK
|
||||
BLACK_ON_TRANSPARENT,
|
||||
WHITE_ON_TRANSPARENT -> 0
|
||||
}
|
||||
}
|
||||
|
||||
private fun Bitmap.saveAsPng(outputStream: OutputStream, quality: Int = 90) =
|
||||
compress(Bitmap.CompressFormat.PNG, quality, outputStream)
|
||||
|
||||
private val fileNameCharacters = "[^A-Za-z0-9]".toRegex()
|
||||
private fun encodeFileName(name: String): String =
|
||||
fileNameCharacters.replace(name, "_").take(16).trim('_').lowercase(Locale.getDefault())
|
||||
@@ -1,605 +0,0 @@
|
||||
package de.markusfisch.android.binaryeye.fragment
|
||||
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.support.design.widget.FloatingActionButton
|
||||
import android.support.v4.app.Fragment
|
||||
import android.text.Editable
|
||||
import android.text.Html
|
||||
import android.text.TextWatcher
|
||||
import android.text.method.LinkMovementMethod
|
||||
import android.view.*
|
||||
import android.widget.*
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.actions.ActionRegistry
|
||||
import de.markusfisch.android.binaryeye.actions.IAction
|
||||
import de.markusfisch.android.binaryeye.actions.wifi.WifiAction
|
||||
import de.markusfisch.android.binaryeye.actions.wifi.WifiConnector
|
||||
import de.markusfisch.android.binaryeye.activity.MainActivity
|
||||
import de.markusfisch.android.binaryeye.adapter.prettifyFormatName
|
||||
import de.markusfisch.android.binaryeye.app.*
|
||||
import de.markusfisch.android.binaryeye.content.copyToClipboard
|
||||
import de.markusfisch.android.binaryeye.content.shareAsFile
|
||||
import de.markusfisch.android.binaryeye.content.shareText
|
||||
import de.markusfisch.android.binaryeye.content.toHexString
|
||||
import de.markusfisch.android.binaryeye.content.wipeLastShareFile
|
||||
import de.markusfisch.android.binaryeye.database.Recreation
|
||||
import de.markusfisch.android.binaryeye.database.Scan
|
||||
import de.markusfisch.android.binaryeye.database.toRecreation
|
||||
import de.markusfisch.android.binaryeye.io.askForFileName
|
||||
import de.markusfisch.android.binaryeye.io.toSaveResult
|
||||
import de.markusfisch.android.binaryeye.io.writeExternalFile
|
||||
import de.markusfisch.android.binaryeye.view.setPaddingFromWindowInsets
|
||||
import de.markusfisch.android.binaryeye.widget.toast
|
||||
import kotlinx.coroutines.*
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
class DecodeFragment : Fragment() {
|
||||
private lateinit var contentView: EditText
|
||||
private lateinit var formatView: TextView
|
||||
private lateinit var dataView: TableLayout
|
||||
private lateinit var metaView: TableLayout
|
||||
private lateinit var hexView: TextView
|
||||
private lateinit var stampView: TextView
|
||||
private lateinit var recreationView: ImageView
|
||||
private lateinit var fab: FloatingActionButton
|
||||
private lateinit var format: String
|
||||
|
||||
private val parentJob = Job()
|
||||
private val scope: CoroutineScope = CoroutineScope(Dispatchers.Main + parentJob)
|
||||
private val dp: Float
|
||||
get() = resources.displayMetrics.density
|
||||
private val content: String
|
||||
get() = contentView.text.toString()
|
||||
|
||||
private var closeAutomatically = false
|
||||
private var action = ActionRegistry.DEFAULT_ACTION
|
||||
private var isBinary = false
|
||||
private var originalBytes: ByteArray = ByteArray(0)
|
||||
private var id = 0L
|
||||
private var recreation: Recreation? = null
|
||||
|
||||
override fun onCreate(state: Bundle?) {
|
||||
super.onCreate(state)
|
||||
setHasOptionsMenu(true)
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
state: Bundle?
|
||||
): View {
|
||||
activity?.setTitle(R.string.content)
|
||||
|
||||
val view = inflater.inflate(
|
||||
R.layout.fragment_decode,
|
||||
container,
|
||||
false
|
||||
)
|
||||
|
||||
closeAutomatically = prefs.closeAutomatically &&
|
||||
activity?.intent?.hasExtra(MainActivity.DECODED) == true
|
||||
|
||||
val scan = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
arguments?.getParcelable(SCAN, Scan::class.java)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
arguments?.getParcelable(SCAN) as Scan?
|
||||
} ?: throw IllegalArgumentException("DecodeFragment needs a Scan")
|
||||
|
||||
val originalContent = scan.content
|
||||
isBinary = scan.content.isEmpty()
|
||||
originalBytes = scan.raw ?: originalContent.toByteArray()
|
||||
format = scan.format.name
|
||||
id = scan.id
|
||||
|
||||
contentView = view.findViewById(R.id.content)
|
||||
formatView = view.findViewById(R.id.format)
|
||||
dataView = view.findViewById(R.id.data)
|
||||
metaView = view.findViewById(R.id.meta)
|
||||
hexView = view.findViewById(R.id.hex)
|
||||
stampView = view.findViewById(R.id.stamp)
|
||||
recreationView = view.findViewById(R.id.recreation)
|
||||
fab = view.findViewById(R.id.open)
|
||||
|
||||
initContentAndFab(originalContent)
|
||||
|
||||
if (prefs.showMetaData) {
|
||||
metaView.fillMetaView(scan)
|
||||
} else {
|
||||
metaView.visibility = View.GONE
|
||||
}
|
||||
if (prefs.showRecreation) {
|
||||
recreation = scan.toRecreation(
|
||||
(200f * dp).roundToInt()
|
||||
)
|
||||
}
|
||||
|
||||
updateViewsAndFab(originalContent, originalBytes)
|
||||
|
||||
(view.findViewById(R.id.inset_layout) as View).setPaddingFromWindowInsets()
|
||||
(view.findViewById(R.id.scroll_view) as View).setPaddingFromWindowInsets()
|
||||
|
||||
return view
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
parentJob.cancel()
|
||||
wipeLastShareFile()
|
||||
}
|
||||
|
||||
private fun initContentAndFab(originalContent: String) {
|
||||
if (isBinary) {
|
||||
contentView.setText(String(originalBytes).foldNonAlNum())
|
||||
contentView.isEnabled = false
|
||||
fab.setImageResource(R.drawable.ic_action_save)
|
||||
fab.setOnClickListener {
|
||||
askForFileNameAndSave(originalBytes)
|
||||
}
|
||||
} else {
|
||||
contentView.setText(originalContent)
|
||||
contentView.addTextChangedListener(object : TextWatcher {
|
||||
override fun afterTextChanged(s: Editable?) {
|
||||
updateViewsAndFab(s?.toString() ?: "")
|
||||
}
|
||||
|
||||
override fun beforeTextChanged(
|
||||
s: CharSequence?,
|
||||
start: Int,
|
||||
count: Int,
|
||||
after: Int
|
||||
) = Unit
|
||||
|
||||
override fun onTextChanged(
|
||||
s: CharSequence?,
|
||||
start: Int,
|
||||
before: Int,
|
||||
count: Int
|
||||
) = Unit
|
||||
})
|
||||
fab.setOnClickListener {
|
||||
executeAction(content)
|
||||
}
|
||||
if (prefs.openImmediately) {
|
||||
executeAction(content)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateViewsAndFab(text: String, bytes: ByteArray? = null) {
|
||||
val b = bytes ?: text.toByteArray()
|
||||
resolveActionAndUpdateFab(b)
|
||||
updateViews(text, b)
|
||||
}
|
||||
|
||||
private fun resolveActionAndUpdateFab(bytes: ByteArray) {
|
||||
val prevAction = action
|
||||
if (!prevAction.canExecuteOn(bytes)) {
|
||||
action = ActionRegistry.getAction(bytes)
|
||||
}
|
||||
if (prevAction !== action) {
|
||||
updateFab(action)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateFab(action: IAction) {
|
||||
fab.setImageResource(action.iconResId)
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
||||
fab.setOnLongClickListener { v ->
|
||||
v.context.toast(action.titleResId)
|
||||
true
|
||||
}
|
||||
} else {
|
||||
fab.tooltipText = getString(action.titleResId)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateViews(text: String, bytes: ByteArray) {
|
||||
formatView.text = resources.getQuantityString(
|
||||
R.plurals.barcode_info,
|
||||
bytes.size,
|
||||
prettifyFormatName(format),
|
||||
bytes.size
|
||||
)
|
||||
hexView.showIf(prefs.showHexDump) { v ->
|
||||
v.text = hexDump(bytes)
|
||||
}
|
||||
recreationView.showIf(prefs.showRecreation) { v ->
|
||||
val r = recreation ?: return@showIf
|
||||
val content = if (isBinary) bytes else text
|
||||
v.setImageBitmap(r.encode(content))
|
||||
v.setOnClickListener {
|
||||
fragmentManager?.addFragment(
|
||||
BarcodeFragment.newInstance(
|
||||
content,
|
||||
r.format,
|
||||
r.size,
|
||||
r.ecLevel
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
dataView.fillDataView(text)
|
||||
stampView.setTrackingLink(bytes, format)
|
||||
}
|
||||
|
||||
private fun TableLayout.fillDataView(text: String) {
|
||||
val items = LinkedHashMap<Int, String?>()
|
||||
if (action is WifiAction) {
|
||||
WifiConnector.parseMap(text)?.let { wifiData ->
|
||||
items.putAll(
|
||||
linkedMapOf(
|
||||
R.string.entry_type to getString(R.string.wifi_network),
|
||||
R.string.wifi_ssid to wifiData["S"],
|
||||
R.string.wifi_password to wifiData["P"],
|
||||
R.string.wifi_type to wifiData["T"],
|
||||
R.string.wifi_hidden to wifiData["H"],
|
||||
R.string.wifi_eap to wifiData["E"],
|
||||
R.string.wifi_identity to wifiData["I"],
|
||||
R.string.wifi_anonymous_identity to wifiData["A"],
|
||||
R.string.wifi_phase2 to wifiData["PH2"]
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
fill(items)
|
||||
}
|
||||
|
||||
private fun TableLayout.fillMetaView(scan: Scan) {
|
||||
val items = linkedMapOf(
|
||||
R.string.error_correction_level to scan.errorCorrectionLevel,
|
||||
R.string.sequence_size to scan.sequenceSize.positiveToString(),
|
||||
R.string.sequence_index to scan.sequenceIndex.positiveToString(),
|
||||
R.string.sequence_id to scan.sequenceId,
|
||||
R.string.gtin_country to scan.country,
|
||||
R.string.gtin_add_on to scan.addOn,
|
||||
R.string.gtin_price to scan.price,
|
||||
R.string.gtin_issue_number to scan.issueNumber,
|
||||
)
|
||||
if (!scan.version.isNullOrBlank()) {
|
||||
val versionString = if (scan.format.name == "QR_CODE") {
|
||||
getString(
|
||||
R.string.qr_version_and_modules,
|
||||
scan.version,
|
||||
(scan.version.toIntOrNull() ?: 0) * 4 + 17
|
||||
)
|
||||
} else {
|
||||
scan.version
|
||||
}
|
||||
items.putAll(
|
||||
linkedMapOf(
|
||||
R.string.barcode_version_number to versionString
|
||||
)
|
||||
)
|
||||
}
|
||||
fill(items)
|
||||
}
|
||||
|
||||
private fun TableLayout.fill(
|
||||
items: LinkedHashMap<Int, String?>
|
||||
) {
|
||||
removeAllViews()
|
||||
visibility = if (items.isEmpty()) View.GONE else {
|
||||
val ctx = context
|
||||
val spaceBetween = (16f * dp).roundToInt()
|
||||
items.forEach { item ->
|
||||
val text = item.value
|
||||
if (!text.isNullOrBlank()) {
|
||||
val tr = TableRow(ctx)
|
||||
val keyView = TextView(ctx)
|
||||
keyView.setText(item.key)
|
||||
val valueView = TextView(ctx)
|
||||
valueView.setPadding(spaceBetween, 0, 0, 0)
|
||||
valueView.text = text
|
||||
tr.addView(keyView)
|
||||
tr.addView(valueView)
|
||||
addView(tr)
|
||||
}
|
||||
}
|
||||
View.VISIBLE
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
inflater.inflate(R.menu.fragment_decode, menu)
|
||||
if (id > 0L) {
|
||||
menu.findItem(R.id.remove).isVisible = true
|
||||
}
|
||||
if (action is WifiAction) {
|
||||
menu.findItem(R.id.copy_password).isVisible = true
|
||||
}
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
return when (item.itemId) {
|
||||
R.id.copy_password -> {
|
||||
copyPasswordToClipboard()
|
||||
maybeBackOrFinish()
|
||||
true
|
||||
}
|
||||
|
||||
R.id.copy_to_clipboard -> {
|
||||
copyToClipboard(textOrHex())
|
||||
maybeBackOrFinish()
|
||||
true
|
||||
}
|
||||
|
||||
R.id.share -> {
|
||||
context?.apply {
|
||||
shareText(textOrHex())
|
||||
maybeBackOrFinish()
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
R.id.share_file -> {
|
||||
context?.apply {
|
||||
val (data, name) = if (isBinary) {
|
||||
Pair(originalBytes, "barcode_content.bin")
|
||||
} else {
|
||||
Pair(content, "barcode_content.txt")
|
||||
}
|
||||
shareAsFile(data, name)
|
||||
maybeBackOrFinish()
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
R.id.create -> {
|
||||
fragmentManager?.addFragment(
|
||||
EncodeFragment.newInstance(
|
||||
if (isBinary) originalBytes else content,
|
||||
format
|
||||
)
|
||||
)
|
||||
true
|
||||
}
|
||||
|
||||
R.id.remove -> {
|
||||
db.removeScan(id)
|
||||
backOrFinish()
|
||||
true
|
||||
}
|
||||
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
}
|
||||
|
||||
private fun textOrHex() = if (isBinary) {
|
||||
originalBytes.toHexString()
|
||||
} else {
|
||||
content
|
||||
}
|
||||
|
||||
private fun copyPasswordToClipboard() {
|
||||
val ac = action
|
||||
if (ac is WifiAction) {
|
||||
ac.password?.let { password ->
|
||||
activity?.apply {
|
||||
copyToClipboard(password, isSensitive = true)
|
||||
toast(R.string.copied_password_to_clipboard)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun copyToClipboard(text: String, isSensitive: Boolean = false) {
|
||||
activity?.apply {
|
||||
copyToClipboard(text, isSensitive)
|
||||
toast(R.string.copied_to_clipboard)
|
||||
}
|
||||
}
|
||||
|
||||
private fun maybeBackOrFinish() {
|
||||
if (closeAutomatically) {
|
||||
backOrFinish()
|
||||
}
|
||||
}
|
||||
|
||||
private fun backOrFinish() {
|
||||
val fm = fragmentManager
|
||||
if (fm != null && fm.backStackEntryCount > 0) {
|
||||
fm.popBackStack()
|
||||
} else {
|
||||
activity?.finish()
|
||||
}
|
||||
}
|
||||
|
||||
private fun executeAction(content: String) {
|
||||
val ac = activity ?: return
|
||||
if (content.isNotEmpty()) {
|
||||
if (action is WifiAction &&
|
||||
Build.VERSION.SDK_INT < Build.VERSION_CODES.Q &&
|
||||
!ac.hasLocationPermission { executeAction(content) }
|
||||
) {
|
||||
return
|
||||
}
|
||||
scope.launch {
|
||||
action.execute(ac, content.toByteArray())
|
||||
maybeBackOrFinish()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun askForFileNameAndSave(raw: ByteArray) {
|
||||
val ac = activity ?: return
|
||||
// Write permission is only required before Android Q.
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q &&
|
||||
!ac.hasWritePermission { askForFileNameAndSave(raw) }
|
||||
) {
|
||||
return
|
||||
}
|
||||
scope.launch(Dispatchers.Main) {
|
||||
val name = ac.askForFileName() ?: return@launch
|
||||
withContext(Dispatchers.IO) {
|
||||
val message = ac.writeExternalFile(
|
||||
name,
|
||||
"application/octet-stream"
|
||||
) {
|
||||
it.write(raw)
|
||||
}.toSaveResult()
|
||||
withContext(Dispatchers.Main) {
|
||||
ac.toast(message)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val SCAN = "scan"
|
||||
|
||||
fun newInstance(scan: Scan): Fragment {
|
||||
val args = Bundle()
|
||||
args.putParcelable(SCAN, scan)
|
||||
val fragment = DecodeFragment()
|
||||
fragment.arguments = args
|
||||
return fragment
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private inline fun <T : View> T.showIf(
|
||||
visible: Boolean,
|
||||
block: (T) -> Unit
|
||||
) {
|
||||
visibility = if (visible) {
|
||||
block.invoke(this)
|
||||
View.VISIBLE
|
||||
} else {
|
||||
View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
private val nonAlNum = "[^a-zA-Z0-9]".toRegex()
|
||||
private val multipleDots = "[…]+".toRegex()
|
||||
private fun String.foldNonAlNum() = replace(nonAlNum, "…")
|
||||
.replace(multipleDots, "…")
|
||||
|
||||
private fun hexDump(bytes: ByteArray, charsPerLine: Int = 33): String {
|
||||
if (charsPerLine < 4 || bytes.isEmpty()) {
|
||||
return ""
|
||||
}
|
||||
val dump = StringBuilder()
|
||||
val hex = StringBuilder()
|
||||
val ascii = StringBuilder()
|
||||
val itemsPerLine = (charsPerLine - 1) / 4
|
||||
val len = bytes.size
|
||||
var i = 0
|
||||
while (true) {
|
||||
val ord = bytes[i]
|
||||
hex.append(String.format("%02X ", ord))
|
||||
ascii.append(if (ord > 31) ord.toInt().toChar() else " ")
|
||||
++i
|
||||
val posInLine = i % itemsPerLine
|
||||
val atEnd = i >= len
|
||||
var atLineEnd = posInLine == 0
|
||||
if (atEnd && !atLineEnd) {
|
||||
for (j in posInLine until itemsPerLine) {
|
||||
hex.append(" ")
|
||||
}
|
||||
atLineEnd = true
|
||||
}
|
||||
if (atLineEnd) {
|
||||
dump.append(hex.toString())
|
||||
dump.append(" ")
|
||||
dump.append(ascii.toString())
|
||||
dump.append("\n")
|
||||
hex.setLength(0)
|
||||
ascii.setLength(0)
|
||||
}
|
||||
if (atEnd) {
|
||||
break
|
||||
}
|
||||
}
|
||||
return dump.toString()
|
||||
}
|
||||
|
||||
private fun Int.positiveToString() = if (this > -1) this.toString() else ""
|
||||
|
||||
private fun TextView.setTrackingLink(bytes: ByteArray, format: String) {
|
||||
val trackingLink = generateDpTrackingLink(bytes, format)
|
||||
if (trackingLink != null) {
|
||||
text = trackingLink.fromHtml()
|
||||
isClickable = true
|
||||
movementMethod = LinkMovementMethod.getInstance()
|
||||
} else {
|
||||
visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
private fun generateDpTrackingLink(bytes: ByteArray, format: String): String? {
|
||||
// Check for Deutsche Post Matrixcode stamp.
|
||||
var isStamp = false
|
||||
var rawData = bytes
|
||||
if (format == "DATA_MATRIX" &&
|
||||
bytes.toString(Charsets.ISO_8859_1).startsWith("DEA5")
|
||||
) {
|
||||
if (bytes.size == 47) {
|
||||
isStamp = true
|
||||
} else if (bytes.size > 47) {
|
||||
// Transform back to original data.
|
||||
rawData = bytes.toString(Charsets.UTF_8).toByteArray(
|
||||
Charsets.ISO_8859_1
|
||||
)
|
||||
if (rawData.size == 47) {
|
||||
isStamp = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!isStamp) {
|
||||
return null
|
||||
}
|
||||
|
||||
val hex = StringBuilder()
|
||||
hex.append(String.format("%02X", rawData[9]))
|
||||
hex.append(String.format("%02X", rawData[10]))
|
||||
hex.append(String.format("%02X", rawData[11]))
|
||||
hex.append(String.format("%02X", rawData[12]))
|
||||
hex.append(String.format("%02X", rawData[13]))
|
||||
hex.append(String.format("%X", (rawData[4].toInt() and 0x0f).toByte()))
|
||||
hex.append(String.format("%02X", rawData[5]))
|
||||
hex.append(String.format("%02X", rawData[6]))
|
||||
hex.append(String.format("%02X", rawData[7]))
|
||||
hex.append(String.format("%02X", rawData[8]))
|
||||
val hexString = hex.toString()
|
||||
val trackingNumber = hexString + String.format(
|
||||
"%X",
|
||||
crc4(hexString.toByteArray(Charsets.ISO_8859_1))
|
||||
)
|
||||
return "<a href=\"https://www.deutschepost.de/de/s/sendungsverfolgung/verfolgen.html?piececode=$trackingNumber\">Deutsche Post: $trackingNumber</a>"
|
||||
}
|
||||
|
||||
// CRC-4 with polynomial x^4 + x + 1.
|
||||
private fun crc4(input: ByteArray): Int {
|
||||
var crc = 0
|
||||
var i = 0
|
||||
while (i < input.size) {
|
||||
val c = input[i].toInt()
|
||||
var j = 0x80
|
||||
while (j != 0) {
|
||||
var bit = crc and 0x8
|
||||
crc = crc shl 1
|
||||
if (c and j != 0) {
|
||||
bit = bit xor 0x8
|
||||
}
|
||||
if (bit != 0) {
|
||||
crc = crc xor 0x3
|
||||
}
|
||||
j = j ushr 1
|
||||
}
|
||||
++i
|
||||
}
|
||||
crc = crc and 0xF
|
||||
return crc
|
||||
}
|
||||
|
||||
private fun String.fromHtml() = if (
|
||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.N
|
||||
) {
|
||||
Html.fromHtml(this, Html.FROM_HTML_MODE_LEGACY)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
Html.fromHtml(this)
|
||||
}
|
||||
+170
-53
@@ -9,15 +9,17 @@ 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 androidx.annotation.RequiresApi
|
||||
import androidx.preference.ListPreference
|
||||
import androidx.preference.MultiSelectListPreference
|
||||
import androidx.preference.Preference
|
||||
import androidx.preference.PreferenceFragmentCompat
|
||||
import androidx.preference.PreferenceGroup
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.activity.AutomatedActionsActivity
|
||||
import de.markusfisch.android.binaryeye.activity.NetworkSuggestionsActivity
|
||||
import de.markusfisch.android.binaryeye.activity.ProfilesActivity
|
||||
import de.markusfisch.android.binaryeye.activity.SplashActivity
|
||||
import de.markusfisch.android.binaryeye.app.addFragment
|
||||
import de.markusfisch.android.binaryeye.app.hasBluetoothPermission
|
||||
import de.markusfisch.android.binaryeye.app.prefs
|
||||
import de.markusfisch.android.binaryeye.bluetooth.setBluetoothHosts
|
||||
@@ -28,58 +30,105 @@ import de.markusfisch.android.binaryeye.view.systemBarRecyclerViewScrollListener
|
||||
import de.markusfisch.android.binaryeye.widget.toast
|
||||
|
||||
class PreferencesFragment : PreferenceFragmentCompat() {
|
||||
private var lastProfile: String? = null
|
||||
|
||||
private val changeListener = object : OnSharedPreferenceChangeListener {
|
||||
override fun onSharedPreferenceChanged(
|
||||
sharedPreferences: SharedPreferences?,
|
||||
key: String?
|
||||
) {
|
||||
key ?: return
|
||||
val preference = findPreference(key) ?: return
|
||||
prefs.update()
|
||||
when (preference.key) {
|
||||
"custom_locale" -> activity?.restartApp()
|
||||
"beep_tone_name" -> {
|
||||
beepConfirm()
|
||||
setSummary(preference)
|
||||
}
|
||||
|
||||
"send_scan_bluetooth" -> {
|
||||
if (prefs.sendScanBluetooth &&
|
||||
activity?.hasBluetoothPermission() == false
|
||||
) {
|
||||
prefs.sendScanBluetooth = false
|
||||
}
|
||||
setSummary(preference)
|
||||
}
|
||||
|
||||
else -> setSummary(preference)
|
||||
val preference = findPreference<Preference>(key) ?: return
|
||||
if (preference.key != PROFILE) {
|
||||
prefs.update()
|
||||
}
|
||||
when (preference.key) {
|
||||
"custom_locale" -> {
|
||||
activity?.restartApp()
|
||||
return
|
||||
}
|
||||
|
||||
"beep_tone_name" -> beepConfirm()
|
||||
|
||||
"send_scan_bluetooth" -> if (
|
||||
prefs.sendScanBluetooth &&
|
||||
activity?.hasBluetoothPermission() == false
|
||||
) {
|
||||
prefs.sendScanBluetooth = false
|
||||
}
|
||||
}
|
||||
setSummary(preference)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreatePreferences(state: Bundle?, rootKey: String?) {
|
||||
addPreferencesFromResource(R.xml.preferences)
|
||||
loadPreferences(rootKey)
|
||||
}
|
||||
|
||||
private fun loadPreferences(rootKey: String? = null) {
|
||||
preferenceScreen?.sharedPreferences
|
||||
?.unregisterOnSharedPreferenceChangeListener(changeListener)
|
||||
|
||||
preferenceManager.apply {
|
||||
sharedPreferencesName = prefs.profile ?: "${context.packageName}_preferences"
|
||||
sharedPreferencesMode = Context.MODE_PRIVATE
|
||||
}
|
||||
|
||||
// Refresh the preferences.
|
||||
preferenceScreen = null
|
||||
setPreferencesFromResource(R.xml.preferences, rootKey)
|
||||
preferenceScreen.sharedPreferences
|
||||
?.registerOnSharedPreferenceChangeListener(changeListener)
|
||||
setSummaries(preferenceScreen)
|
||||
setIcons()
|
||||
|
||||
wireProfiles()
|
||||
wireAutomatedActions()
|
||||
setBluetoothResources()
|
||||
wireClearNetworkPreferences()
|
||||
}
|
||||
|
||||
private fun wireProfiles() {
|
||||
findPreference<Preference>(PROFILE)?.apply {
|
||||
updateProfileSummary(this)
|
||||
onPreferenceClickListener = Preference.OnPreferenceClickListener {
|
||||
startActivity(Intent(activity, ProfilesActivity::class.java))
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun wireAutomatedActions() {
|
||||
findPreference<Preference>(AUTOMATED_ACTIONS)?.apply {
|
||||
updateAutomatedActionsSummary(this)
|
||||
onPreferenceClickListener = Preference.OnPreferenceClickListener {
|
||||
startActivity(
|
||||
Intent(activity, AutomatedActionsActivity::class.java)
|
||||
)
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setBluetoothResources() {
|
||||
if (prefs.sendScanBluetooth &&
|
||||
activity?.hasBluetoothPermission() == true
|
||||
) {
|
||||
setBluetoothHosts(
|
||||
findPreference("send_scan_bluetooth_host") as ListPreference
|
||||
)
|
||||
findPreference<ListPreference>("send_scan_bluetooth_host")?.let {
|
||||
setBluetoothHosts(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun wireClearNetworkPreferences() {
|
||||
findPreference("clear_network_suggestions").apply {
|
||||
findPreference<Preference>("clear_network_suggestions")?.apply {
|
||||
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.Q) {
|
||||
// From R+ we can query past network suggestions and
|
||||
// make them editable.
|
||||
setOnPreferenceClickListener {
|
||||
fragmentManager.addFragment(NetworkSuggestionsFragment())
|
||||
startActivity(
|
||||
Intent(activity, NetworkSuggestionsActivity::class.java)
|
||||
)
|
||||
true
|
||||
}
|
||||
} else if (Build.VERSION.SDK_INT == Build.VERSION_CODES.Q) {
|
||||
@@ -87,10 +136,10 @@ class PreferencesFragment : PreferenceFragmentCompat() {
|
||||
// Note that previous versions of this app allowed
|
||||
// adding network suggestions on Q as well, so we
|
||||
// need to keep this option.
|
||||
setOnPreferenceClickListener {
|
||||
context.askToClearNetworkSuggestions()
|
||||
true
|
||||
}
|
||||
setOnPreferenceClickListener {
|
||||
context?.askToClearNetworkSuggestions()
|
||||
true
|
||||
}
|
||||
} else {
|
||||
// There are no network suggestions below Q.
|
||||
isVisible = false
|
||||
@@ -124,24 +173,30 @@ class PreferencesFragment : PreferenceFragmentCompat() {
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
if (lastProfile != prefs.profile) {
|
||||
loadPreferences()
|
||||
}
|
||||
activity?.setTitle(R.string.preferences)
|
||||
findPreference<Preference>(AUTOMATED_ACTIONS)?.let {
|
||||
updateAutomatedActionsSummary(it)
|
||||
}
|
||||
findPreference<Preference>(PROFILE)?.let {
|
||||
updateProfileSummary(it)
|
||||
}
|
||||
listView.setPaddingFromWindowInsets()
|
||||
listView.removeOnScrollListener(systemBarRecyclerViewScrollListener)
|
||||
listView.addOnScrollListener(systemBarRecyclerViewScrollListener)
|
||||
preferenceScreen.sharedPreferences
|
||||
.registerOnSharedPreferenceChangeListener(changeListener)
|
||||
setSummaries(preferenceScreen)
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
preferenceScreen.sharedPreferences
|
||||
.unregisterOnSharedPreferenceChangeListener(changeListener)
|
||||
?.unregisterOnSharedPreferenceChangeListener(changeListener)
|
||||
}
|
||||
|
||||
override fun onDisplayPreferenceDialog(preference: Preference) {
|
||||
if (preference is UrlPreference) {
|
||||
val fm = fragmentManager
|
||||
val fm = parentFragmentManager
|
||||
UrlDialogFragment.newInstance(preference.key).apply {
|
||||
setTargetFragment(this@PreferencesFragment, 0)
|
||||
show(fm, null)
|
||||
@@ -165,9 +220,13 @@ class PreferencesFragment : PreferenceFragmentCompat() {
|
||||
}
|
||||
|
||||
private fun setSummary(preference: Preference) {
|
||||
if (preference.key == AUTOMATED_ACTIONS) {
|
||||
updateAutomatedActionsSummary(preference)
|
||||
return
|
||||
}
|
||||
when (preference) {
|
||||
is UrlPreference -> {
|
||||
preference.setSummary(preference.getUrl())
|
||||
preference.summary = preference.getUrl()
|
||||
}
|
||||
|
||||
is ListPreference -> {
|
||||
@@ -175,11 +234,9 @@ class PreferencesFragment : PreferenceFragmentCompat() {
|
||||
}
|
||||
|
||||
is MultiSelectListPreference -> {
|
||||
preference.setSummary(
|
||||
preference.values.joinToString(", ") {
|
||||
it.replace(Regex("_"), " ")
|
||||
}
|
||||
)
|
||||
preference.summary = preference.values.joinToString(", ") {
|
||||
it.replace(Regex("_"), " ")
|
||||
}
|
||||
}
|
||||
|
||||
is PreferenceGroup -> {
|
||||
@@ -187,16 +244,76 @@ class PreferencesFragment : PreferenceFragmentCompat() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setIcons() {
|
||||
setIcon(PROFILE, R.drawable.ic_action_profile)
|
||||
setIcon("formats", R.drawable.ic_action_restrict_format)
|
||||
setIcon("show_crop_handle", R.drawable.ic_action_scan)
|
||||
setIcon("zoom_by_swiping", R.drawable.ic_action_search)
|
||||
setIcon("auto_rotate", R.drawable.ic_action_rotation_unlocked)
|
||||
setIcon("try_harder", R.drawable.ic_action_search)
|
||||
setIcon("bulk_mode", R.drawable.ic_action_bulk_mode)
|
||||
setIcon("bulk_mode_delay", R.drawable.ic_action_bulk_mode)
|
||||
setIcon("show_toast_in_bulk_mode", R.drawable.ic_action_bulk_mode)
|
||||
setIcon("vibrate", R.drawable.ic_action_info)
|
||||
setIcon("beep", R.drawable.ic_action_info)
|
||||
setIcon("beep_tone_name", R.drawable.ic_action_info)
|
||||
setIcon("use_history", R.drawable.ic_action_history)
|
||||
setIcon("ignore_duplicates_name", R.drawable.ic_action_remove)
|
||||
setIcon("copy_immediately", R.drawable.ic_action_copy)
|
||||
setIcon("send_scan_active", R.drawable.ic_action_share)
|
||||
setIcon("send_scan_url", R.drawable.ic_action_link)
|
||||
setIcon("send_scan_type", R.drawable.ic_action_share)
|
||||
setIcon("send_scan_bluetooth", R.drawable.ic_action_wifi)
|
||||
setIcon("send_scan_bluetooth_host", R.drawable.ic_action_wifi)
|
||||
setIcon("open_immediately", R.drawable.ic_action_open)
|
||||
setIcon("show_meta_data", R.drawable.ic_action_info)
|
||||
setIcon("show_hex_dump", R.drawable.ic_action_info)
|
||||
setIcon("show_checksum", R.drawable.ic_action_info)
|
||||
setIcon("show_recreation", R.drawable.ic_action_create)
|
||||
setIcon("close_automatically", R.drawable.ic_action_remove)
|
||||
setIcon("default_search_url", R.drawable.ic_action_search)
|
||||
setIcon("open_with_url", R.drawable.ic_action_link)
|
||||
setIcon(AUTOMATED_ACTIONS, R.drawable.ic_action_create)
|
||||
setIcon("clear_network_suggestions", R.drawable.ic_action_wifi)
|
||||
setIcon("brighten_screen", R.drawable.ic_action_bright)
|
||||
setIcon("custom_locale", R.drawable.ic_action_preferences)
|
||||
}
|
||||
|
||||
private fun setIcon(key: String, iconResId: Int) {
|
||||
findPreference<Preference>(key)?.setIcon(iconResId)
|
||||
}
|
||||
|
||||
private fun updateAutomatedActionsSummary(preference: Preference) {
|
||||
val count = prefs.automatedActions.size
|
||||
preference.summary = if (count == 0) {
|
||||
getString(R.string.automated_actions_none)
|
||||
} else {
|
||||
resources.getQuantityString(
|
||||
R.plurals.automated_actions_count,
|
||||
count,
|
||||
count
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateProfileSummary(preference: Preference) {
|
||||
preference.summary = prefs.profile ?: getString(R.string.profile_default)
|
||||
lastProfile = prefs.profile
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val PROFILE = "profile"
|
||||
private const val AUTOMATED_ACTIONS = "automated_actions"
|
||||
}
|
||||
}
|
||||
|
||||
private 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
|
||||
)
|
||||
}
|
||||
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.
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
package de.markusfisch.android.binaryeye.fragment
|
||||
|
||||
import android.os.Bundle
|
||||
import android.support.v7.preference.PreferenceDialogFragmentCompat
|
||||
import android.view.View
|
||||
import android.widget.TextView
|
||||
import androidx.preference.PreferenceDialogFragmentCompat
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.app.prefs
|
||||
import de.markusfisch.android.binaryeye.database.Scan
|
||||
@@ -15,10 +15,10 @@ class UrlDialogFragment : PreferenceDialogFragmentCompat() {
|
||||
private var urlView: TextView? = null
|
||||
private var testButton: TextView? = null
|
||||
|
||||
override fun onBindDialogView(view: View?) {
|
||||
override fun onBindDialogView(view: View) {
|
||||
super.onBindDialogView(view)
|
||||
urlView = view?.findViewById(R.id.url)
|
||||
testButton = view?.findViewById(R.id.test_url)
|
||||
urlView = view.findViewById(R.id.url)
|
||||
testButton = view.findViewById(R.id.test_url)
|
||||
testButton?.setOnClickListener {
|
||||
testUrl(testButton)
|
||||
}
|
||||
@@ -51,7 +51,7 @@ class UrlDialogFragment : PreferenceDialogFragmentCompat() {
|
||||
Scan(
|
||||
"test",
|
||||
null,
|
||||
BarcodeFormat.NONE
|
||||
BarcodeFormat.None
|
||||
).sendAsync(url, prefs.sendScanType) { code, body ->
|
||||
textView.text = when {
|
||||
code != null -> "$code"
|
||||
@@ -80,10 +80,5 @@ private fun completeUrl(template: String): String {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ fun ContentResolver.loadImageUri(uri: Uri): Bitmap? = try {
|
||||
openInputStream(uri)?.use {
|
||||
BitmapFactory.decodeStream(it, null, options)
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
} catch (_: IOException) {
|
||||
null
|
||||
}
|
||||
|
||||
@@ -55,17 +55,17 @@ fun Bitmap.crop(
|
||||
pivotX: Float,
|
||||
pivotY: Float
|
||||
) = try {
|
||||
val erected = erect(rotation, pivotX, pivotY)
|
||||
val w = erected.width
|
||||
val h = erected.height
|
||||
val b = rotate(rotation, pivotX, pivotY)
|
||||
val w = b.width
|
||||
val h = b.height
|
||||
val x = max(0, (rect.left * w).roundToInt())
|
||||
val y = max(0, (rect.top * h).roundToInt())
|
||||
Bitmap.createBitmap(
|
||||
erected,
|
||||
b,
|
||||
x,
|
||||
y,
|
||||
min(w - x, (rect.right * w).roundToInt() - x),
|
||||
min(h - y, (rect.bottom * h).roundToInt() - y)
|
||||
min(w, (rect.right * w).roundToInt()) - x,
|
||||
min(h, (rect.bottom * h).roundToInt()) - y
|
||||
)
|
||||
} catch (e: OutOfMemoryError) {
|
||||
null
|
||||
@@ -73,13 +73,11 @@ fun Bitmap.crop(
|
||||
null
|
||||
}
|
||||
|
||||
private fun Bitmap.erect(
|
||||
private fun Bitmap.rotate(
|
||||
rotation: Float,
|
||||
pivotX: Float,
|
||||
pivotY: Float
|
||||
): Bitmap = if (
|
||||
rotation % 360f != 0f
|
||||
) {
|
||||
): Bitmap = if (rotation % 360f != 0f) {
|
||||
Bitmap.createBitmap(
|
||||
this,
|
||||
0,
|
||||
|
||||
@@ -3,7 +3,7 @@ package de.markusfisch.android.binaryeye.graphics
|
||||
import android.content.Context
|
||||
import android.graphics.DashPathEffect
|
||||
import android.graphics.Paint
|
||||
import android.support.v4.content.ContextCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
|
||||
fun Context.getDashedBorderPaint() = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
|
||||
@@ -5,26 +5,22 @@ import android.graphics.Bitmap
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.drawable.BitmapDrawable
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.os.Build
|
||||
import androidx.core.content.res.ResourcesCompat
|
||||
import androidx.core.graphics.createBitmap
|
||||
|
||||
fun Resources.getBitmapFromDrawable(
|
||||
resId: Int
|
||||
): Bitmap = getBitmapFromDrawable(getDrawableCompat(resId))
|
||||
|
||||
fun Resources.getDrawableCompat(
|
||||
resId: Int
|
||||
): Drawable = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
getDrawable(resId, null)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
getDrawable(resId)
|
||||
}
|
||||
): Bitmap = getBitmapFromDrawable(
|
||||
ResourcesCompat.getDrawable(this, resId, null) ?: throw Resources.NotFoundException(
|
||||
"Drawable resource ID #0x${resId.toString(16)}"
|
||||
)
|
||||
)
|
||||
|
||||
private fun getBitmapFromDrawable(drawable: Drawable): Bitmap {
|
||||
if (drawable is BitmapDrawable) {
|
||||
return drawable.bitmap
|
||||
}
|
||||
val bitmap = Bitmap.createBitmap(
|
||||
val bitmap = createBitmap(
|
||||
drawable.intrinsicWidth,
|
||||
drawable.intrinsicHeight,
|
||||
Bitmap.Config.ARGB_8888
|
||||
|
||||
@@ -10,7 +10,9 @@ data class FrameMetrics(
|
||||
var width: Int = 0,
|
||||
var height: Int = 0,
|
||||
var orientation: Int = 0
|
||||
)
|
||||
) {
|
||||
fun isValid() = width > 0 && height > 0
|
||||
}
|
||||
|
||||
fun Rect.setFrameRoi(
|
||||
frameMetrics: FrameMetrics,
|
||||
|
||||
@@ -2,14 +2,16 @@ package de.markusfisch.android.binaryeye.graphics
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Canvas
|
||||
import androidx.core.graphics.createBitmap
|
||||
|
||||
fun Bitmap.fixTransparency(): Bitmap {
|
||||
val result = analyzeTransparency()
|
||||
return if (result.hasTransparentPixels) {
|
||||
val copy = Bitmap.createBitmap(
|
||||
val cfg = config
|
||||
return if (cfg != null && result.hasTransparentPixels) {
|
||||
val copy = createBitmap(
|
||||
width,
|
||||
height,
|
||||
config
|
||||
cfg
|
||||
)
|
||||
Canvas(copy).apply {
|
||||
drawColor(result.backgroundColor)
|
||||
|
||||
@@ -6,8 +6,8 @@ import android.content.Context
|
||||
import android.os.Build
|
||||
import android.os.Environment
|
||||
import android.provider.MediaStore
|
||||
import android.support.annotation.MainThread
|
||||
import android.widget.EditText
|
||||
import androidx.annotation.MainThread
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.app.alertDialog
|
||||
import java.io.File
|
||||
@@ -53,7 +53,7 @@ fun Context.writeExternalFile(
|
||||
): Boolean = try {
|
||||
openExternalOutputStream(fileName, mimeType).use { write(it) }
|
||||
true
|
||||
} catch (e: IOException) {
|
||||
} catch (_: IOException) {
|
||||
false
|
||||
}
|
||||
|
||||
@@ -61,7 +61,6 @@ private fun Context.openExternalOutputStream(
|
||||
fileName: String,
|
||||
mimeType: String
|
||||
): OutputStream = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
||||
@Suppress("DEPRECATION")
|
||||
val file = File(
|
||||
Environment.getExternalStoragePublicDirectory(
|
||||
Environment.DIRECTORY_DOWNLOADS
|
||||
|
||||
@@ -22,7 +22,7 @@ fun beepError() {
|
||||
ToneGenerator.MAX_VOLUME
|
||||
)
|
||||
errorToneGenerator = tg
|
||||
tg.startTone(ToneGenerator.TONE_SUP_ERROR)
|
||||
tg.startTone(ToneGenerator.TONE_SUP_ERROR, 1000)
|
||||
}
|
||||
|
||||
fun releaseToneGenerators() {
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
package de.markusfisch.android.binaryeye.net
|
||||
|
||||
import android.net.Uri
|
||||
|
||||
private const val SCHEME = "binaryeye"
|
||||
private const val HOSTNAME = "markusfisch.de"
|
||||
private const val ENCODE_PATH = "encode"
|
||||
|
||||
fun isScanDeeplink(text: String) = listOf(
|
||||
"$SCHEME://scan",
|
||||
"http://$HOSTNAME/BinaryEye",
|
||||
"https://$HOSTNAME/BinaryEye"
|
||||
).any { text.startsWith(it) }
|
||||
|
||||
fun isEncodeDeeplink(text: String) = listOf(
|
||||
"$SCHEME://$ENCODE_PATH",
|
||||
"http://$HOSTNAME/$ENCODE_PATH",
|
||||
"https://$HOSTNAME/$ENCODE_PATH"
|
||||
).any { text.startsWith(it) }
|
||||
|
||||
fun createEncodeDeeplink(format: String, content: String): String {
|
||||
return Uri.Builder()
|
||||
.scheme("https")
|
||||
.authority(HOSTNAME)
|
||||
.appendPath(ENCODE_PATH)
|
||||
.appendQueryParameter("format", format)
|
||||
.appendQueryParameter("content", content)
|
||||
.build()
|
||||
.toString()
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
package de.markusfisch.android.binaryeye.net
|
||||
|
||||
import de.markusfisch.android.binaryeye.content.toHexString
|
||||
import de.markusfisch.android.binaryeye.database.Scan
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -36,7 +35,10 @@ private fun Scan.send(url: String, type: String): Response {
|
||||
|
||||
"2" -> request(url) { con ->
|
||||
con.requestMethod = "POST"
|
||||
con.setRequestProperty("Content-Type", "application/x-www-form-urlencoded")
|
||||
con.setRequestProperty(
|
||||
"Content-Type",
|
||||
"application/x-www-form-urlencoded"
|
||||
)
|
||||
con.outputStream.apply {
|
||||
write(asUrlArguments().toByteArray())
|
||||
close()
|
||||
@@ -53,7 +55,7 @@ private fun Scan.send(url: String, type: String): Response {
|
||||
}
|
||||
|
||||
else -> request(
|
||||
url + content.urlEncode()
|
||||
url + text.urlEncode()
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -67,7 +69,7 @@ private fun Scan.asJson() = JSONObject().apply {
|
||||
}
|
||||
|
||||
private fun Scan.getMap(): Map<String, String> = mapOf(
|
||||
"content" to content,
|
||||
"content" to text,
|
||||
"raw" to raw?.toHexString(),
|
||||
"format" to format.name,
|
||||
"errorCorrectionLevel" to errorCorrectionLevel,
|
||||
|
||||
@@ -5,6 +5,6 @@ import java.net.URLEncoder
|
||||
|
||||
fun String.urlEncode(): String = try {
|
||||
URLEncoder.encode(this, "UTF-8")
|
||||
} catch (e: UnsupportedEncodingException) {
|
||||
} catch (_: UnsupportedEncodingException) {
|
||||
this
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
package de.markusfisch.android.binaryeye.os
|
||||
|
||||
import android.app.Activity
|
||||
import android.view.WindowManager
|
||||
|
||||
fun Activity.getScreenBrightness(): Float {
|
||||
val layoutParams = window.attributes
|
||||
return if (layoutParams.screenBrightness ==
|
||||
WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_NONE
|
||||
) {
|
||||
android.provider.Settings.System.getInt(
|
||||
contentResolver,
|
||||
android.provider.Settings.System.SCREEN_BRIGHTNESS
|
||||
) / 255.0f
|
||||
} else {
|
||||
layoutParams.screenBrightness
|
||||
}
|
||||
}
|
||||
|
||||
fun Activity.setScreenBrightness(brightness: Float) {
|
||||
val layoutParams = window.attributes
|
||||
layoutParams.screenBrightness = brightness
|
||||
window.attributes = layoutParams
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package de.markusfisch.android.binaryeye.preference
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import android.graphics.Point
|
||||
import androidx.core.content.edit
|
||||
import org.json.JSONObject
|
||||
|
||||
fun SharedPreferences.storeCropHandle(
|
||||
name: String,
|
||||
viewWidth: Int,
|
||||
viewHeight: Int,
|
||||
cropHandle: Point
|
||||
) {
|
||||
edit {
|
||||
putString(
|
||||
cropHandleKey(name, viewWidth, viewHeight),
|
||||
JSONObject().apply {
|
||||
put("x", cropHandle.x)
|
||||
put("y", cropHandle.y)
|
||||
}.toString()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun SharedPreferences.restoreCropHandle(
|
||||
name: String,
|
||||
viewWidth: Int,
|
||||
viewHeight: Int,
|
||||
default: Point = Point(-2, -2) // -2 means set default roi.
|
||||
): Point {
|
||||
val s = getString(
|
||||
cropHandleKey(name, viewWidth, viewHeight), null
|
||||
) ?: return default
|
||||
return try {
|
||||
val json = JSONObject(s)
|
||||
Point(
|
||||
json.getInt("x"),
|
||||
json.getInt("y")
|
||||
)
|
||||
} catch (_: Exception) {
|
||||
return default
|
||||
}
|
||||
}
|
||||
|
||||
private fun cropHandleKey(
|
||||
name: String,
|
||||
viewWidth: Int,
|
||||
viewHeight: Int
|
||||
) = "${name}:${viewWidth}:${viewHeight}"
|
||||
@@ -3,55 +3,62 @@ package de.markusfisch.android.binaryeye.preference
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import android.graphics.Point
|
||||
import android.media.ToneGenerator
|
||||
import android.os.Build
|
||||
import android.preference.PreferenceManager
|
||||
import android.support.annotation.RequiresApi
|
||||
import androidx.core.content.edit
|
||||
import de.markusfisch.android.binaryeye.automation.AutomatedAction
|
||||
import de.markusfisch.android.binaryeye.automation.AutomatedAction.Companion.fromJsonArray
|
||||
import de.markusfisch.android.binaryeye.automation.AutomatedAction.Companion.toJsonArray
|
||||
import de.markusfisch.android.binaryeye.zxingcpp.migrateBarcodeFormatName
|
||||
import de.markusfisch.android.zxingcpp.ZxingCpp.BarcodeFormat
|
||||
import org.json.JSONArray
|
||||
|
||||
class Preferences {
|
||||
lateinit var defaultPreferences: SharedPreferences
|
||||
lateinit var preferences: SharedPreferences
|
||||
|
||||
val profiles = ArrayList<String>()
|
||||
|
||||
var profile: String? = null
|
||||
set(value) {
|
||||
defaultPreferences.edit {
|
||||
putString(PROFILE, value)
|
||||
}
|
||||
field = value
|
||||
}
|
||||
var barcodeFormats = setOf(
|
||||
BarcodeFormat.AZTEC.name,
|
||||
BarcodeFormat.CODABAR.name,
|
||||
BarcodeFormat.CODE_39.name,
|
||||
BarcodeFormat.CODE_93.name,
|
||||
BarcodeFormat.CODE_128.name,
|
||||
BarcodeFormat.DATA_BAR.name,
|
||||
BarcodeFormat.DATA_BAR_EXPANDED.name,
|
||||
BarcodeFormat.DATA_MATRIX.name,
|
||||
BarcodeFormat.EAN_8.name,
|
||||
BarcodeFormat.EAN_13.name,
|
||||
BarcodeFormat.Aztec.name,
|
||||
BarcodeFormat.Codabar.name,
|
||||
BarcodeFormat.Code39.name,
|
||||
BarcodeFormat.Code39Ext.name,
|
||||
BarcodeFormat.Code32.name,
|
||||
BarcodeFormat.PZN.name,
|
||||
BarcodeFormat.Code93.name,
|
||||
BarcodeFormat.Code128.name,
|
||||
BarcodeFormat.DataBar.name,
|
||||
BarcodeFormat.DataBarOmni.name,
|
||||
BarcodeFormat.DataBarStk.name,
|
||||
BarcodeFormat.DataBarLtd.name,
|
||||
BarcodeFormat.DataBarExp.name,
|
||||
BarcodeFormat.DataBarExpStk.name,
|
||||
BarcodeFormat.DataMatrix.name,
|
||||
BarcodeFormat.DXFilmEdge.name,
|
||||
BarcodeFormat.EAN8.name,
|
||||
BarcodeFormat.EAN13.name,
|
||||
BarcodeFormat.ITF.name,
|
||||
BarcodeFormat.MAXICODE.name,
|
||||
BarcodeFormat.PDF_417.name,
|
||||
BarcodeFormat.QR_CODE.name,
|
||||
BarcodeFormat.MICRO_QR_CODE.name,
|
||||
BarcodeFormat.RMQR_CODE.name,
|
||||
BarcodeFormat.UPC_A.name,
|
||||
BarcodeFormat.UPC_E.name,
|
||||
BarcodeFormat.MaxiCode.name,
|
||||
BarcodeFormat.PDF417.name,
|
||||
BarcodeFormat.QRCode.name,
|
||||
BarcodeFormat.MicroQRCode.name,
|
||||
BarcodeFormat.RMQRCode.name,
|
||||
BarcodeFormat.UPCA.name,
|
||||
BarcodeFormat.UPCE.name,
|
||||
)
|
||||
@RequiresApi(Build.VERSION_CODES.HONEYCOMB)
|
||||
set(value) {
|
||||
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 = -2
|
||||
set(value) {
|
||||
apply(CROP_HANDLE_Y, value)
|
||||
field = value
|
||||
}
|
||||
var cropHandleOrientation = 0
|
||||
set(value) {
|
||||
apply(CROP_HANDLE_ORIENTATION, value)
|
||||
field = value
|
||||
}
|
||||
var showCropHandle = true
|
||||
set(value) {
|
||||
apply(SHOW_CROP_HANDLE, value)
|
||||
@@ -107,9 +114,9 @@ class Preferences {
|
||||
apply(USE_HISTORY, value)
|
||||
field = value
|
||||
}
|
||||
var ignoreConsecutiveDuplicates = true
|
||||
var ignoreDuplicatesName = "ignore_consecutive_duplicates"
|
||||
set(value) {
|
||||
apply(IGNORE_CONSECUTIVE_DUPLICATES, value)
|
||||
apply(IGNORE_DUPLICATES_NAME, value)
|
||||
field = value
|
||||
}
|
||||
var copyImmediately = false
|
||||
@@ -132,6 +139,11 @@ class Preferences {
|
||||
apply(SHOW_HEX_DUMP, value)
|
||||
field = value
|
||||
}
|
||||
var showChecksum = ""
|
||||
set(value) {
|
||||
apply(SHOW_CHECKSUM, value)
|
||||
field = value
|
||||
}
|
||||
var showRecreation = true
|
||||
set(value) {
|
||||
apply(SHOW_RECREATION, value)
|
||||
@@ -152,7 +164,7 @@ class Preferences {
|
||||
apply(OPEN_WITH_URL, value)
|
||||
field = value
|
||||
}
|
||||
var sendScanActive = true
|
||||
var sendScanActive = false
|
||||
set(value) {
|
||||
apply(SEND_SCAN_ACTIVE, value)
|
||||
field = value
|
||||
@@ -194,12 +206,7 @@ class Preferences {
|
||||
apply(INDEX_OF_LAST_SELECTED_EC_LEVEL, value)
|
||||
field = value
|
||||
}
|
||||
var lastMargin: Int = -1
|
||||
set(value) {
|
||||
apply(LAST_MARGIN, value)
|
||||
field = value
|
||||
}
|
||||
var freeRotation = true
|
||||
var freeRotation = false
|
||||
set(value) {
|
||||
apply(FREE_ROTATION, value)
|
||||
field = value
|
||||
@@ -209,24 +216,103 @@ class Preferences {
|
||||
apply(EXPAND_ESCAPE_SEQUENCES, value)
|
||||
field = value
|
||||
}
|
||||
var addQuietZone = true
|
||||
set(value) {
|
||||
apply(ADD_QUIET_ZONE, value)
|
||||
field = value
|
||||
}
|
||||
var brightenScreen = false
|
||||
set(value) {
|
||||
apply(BRIGHTEN_SCREEN, value)
|
||||
field = value
|
||||
}
|
||||
var previewScale = 0f
|
||||
set(value) {
|
||||
apply(PREVIEW_SCALE, value)
|
||||
field = value
|
||||
}
|
||||
var automatedActions = mutableListOf<AutomatedAction>()
|
||||
private set
|
||||
|
||||
fun init(context: Context) {
|
||||
preferences = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
// I'm not including a support library just to get the
|
||||
// default preferences. Dependencies are a burden.
|
||||
@Suppress("DEPRECATION")
|
||||
defaultPreferences = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
|
||||
loadProfiles()
|
||||
load(context, defaultPreferences.getString(PROFILE, null))
|
||||
}
|
||||
|
||||
fun load(context: Context, profileName: String?) {
|
||||
preferences = if (profileName.isNullOrEmpty()) {
|
||||
profile = null
|
||||
defaultPreferences
|
||||
} else {
|
||||
profile = profileName
|
||||
context.getSharedPreferences(
|
||||
profileName,
|
||||
Context.MODE_PRIVATE
|
||||
)
|
||||
}
|
||||
update()
|
||||
}
|
||||
|
||||
fun update() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
|
||||
preferences.getStringSet(BARCODE_FORMATS, barcodeFormats)?.let {
|
||||
barcodeFormats = it
|
||||
}
|
||||
fun addProfile(name: String): Boolean {
|
||||
if (!name.matches("^[a-zA-Z0-9_-]+$".toRegex()) ||
|
||||
profiles.contains(name)
|
||||
) {
|
||||
return false
|
||||
}
|
||||
cropHandleX = preferences.getInt(CROP_HANDLE_X, cropHandleX)
|
||||
cropHandleY = preferences.getInt(CROP_HANDLE_Y, cropHandleY)
|
||||
cropHandleOrientation = preferences.getInt(
|
||||
CROP_HANDLE_ORIENTATION,
|
||||
cropHandleOrientation
|
||||
)
|
||||
profiles.add(name)
|
||||
saveProfiles()
|
||||
return true
|
||||
}
|
||||
|
||||
fun removeProfile(name: String): Boolean {
|
||||
if (!profiles.remove(name)) {
|
||||
return false
|
||||
}
|
||||
saveProfiles()
|
||||
return true
|
||||
}
|
||||
|
||||
fun saveProfiles() {
|
||||
defaultPreferences.edit {
|
||||
putString(PROFILES, JSONArray(profiles).toString())
|
||||
}
|
||||
}
|
||||
|
||||
fun loadProfiles() {
|
||||
profiles.clear()
|
||||
JSONArray(
|
||||
defaultPreferences.getString(PROFILES, "[]")
|
||||
).let {
|
||||
profiles.addAll(Array(it.length()) { i -> it.getString(i) })
|
||||
}
|
||||
}
|
||||
|
||||
fun update() {
|
||||
preferences.getStringSet(BARCODE_FORMATS, barcodeFormats)?.let {
|
||||
val migrated = migrateBarcodeFormats(it)
|
||||
if (migrated != it) {
|
||||
apply(BARCODE_FORMATS, migrated)
|
||||
}
|
||||
barcodeFormats = addFormatsOnUpdate(
|
||||
migrated,
|
||||
BarcodeFormat.Code39Ext,
|
||||
BarcodeFormat.Code32,
|
||||
BarcodeFormat.PZN,
|
||||
BarcodeFormat.DataBarOmni,
|
||||
BarcodeFormat.DataBarStk,
|
||||
BarcodeFormat.DataBarLtd,
|
||||
BarcodeFormat.DataBarExp,
|
||||
BarcodeFormat.DataBarExpStk,
|
||||
BarcodeFormat.RMQRCode,
|
||||
BarcodeFormat.DXFilmEdge
|
||||
)
|
||||
}
|
||||
|
||||
showCropHandle = preferences.getBoolean(
|
||||
SHOW_CROP_HANDLE,
|
||||
showCropHandle
|
||||
@@ -249,10 +335,22 @@ class Preferences {
|
||||
beepToneName = it
|
||||
}
|
||||
useHistory = preferences.getBoolean(USE_HISTORY, useHistory)
|
||||
ignoreConsecutiveDuplicates = preferences.getBoolean(
|
||||
IGNORE_CONSECUTIVE_DUPLICATES,
|
||||
ignoreConsecutiveDuplicates
|
||||
)
|
||||
|
||||
// Map old setting to new one if it wasn't on default value.
|
||||
val ignoreConsecutiveDuplicates = "ignore_consecutive_duplicates"
|
||||
if (!preferences.getBoolean(ignoreConsecutiveDuplicates, true)) {
|
||||
preferences.edit {
|
||||
remove(ignoreConsecutiveDuplicates)
|
||||
}
|
||||
ignoreDuplicatesName = "accept_duplicates"
|
||||
}
|
||||
|
||||
preferences.getString(
|
||||
IGNORE_DUPLICATES_NAME,
|
||||
ignoreDuplicatesName
|
||||
)?.also {
|
||||
ignoreDuplicatesName = it
|
||||
}
|
||||
copyImmediately = preferences.getBoolean(
|
||||
COPY_IMMEDIATELY,
|
||||
copyImmediately
|
||||
@@ -263,6 +361,9 @@ class Preferences {
|
||||
)
|
||||
showMetaData = preferences.getBoolean(SHOW_META_DATA, showMetaData)
|
||||
showHexDump = preferences.getBoolean(SHOW_HEX_DUMP, showHexDump)
|
||||
preferences.getString(SHOW_CHECKSUM, showChecksum)?.also {
|
||||
showChecksum = it
|
||||
}
|
||||
showRecreation = preferences.getBoolean(SHOW_RECREATION, showRecreation)
|
||||
closeAutomatically = preferences.getBoolean(
|
||||
CLOSE_AUTOMATICALLY,
|
||||
@@ -305,17 +406,64 @@ class Preferences {
|
||||
INDEX_OF_LAST_SELECTED_EC_LEVEL,
|
||||
indexOfLastSelectedEcLevel
|
||||
)
|
||||
lastMargin = preferences.getInt(
|
||||
LAST_MARGIN,
|
||||
lastMargin
|
||||
)
|
||||
freeRotation = preferences.getBoolean(FREE_ROTATION, freeRotation)
|
||||
expandEscapeSequences = preferences.getBoolean(
|
||||
EXPAND_ESCAPE_SEQUENCES,
|
||||
expandEscapeSequences
|
||||
)
|
||||
addQuietZone = preferences.getBoolean(ADD_QUIET_ZONE, addQuietZone)
|
||||
brightenScreen = preferences.getBoolean(
|
||||
BRIGHTEN_SCREEN,
|
||||
brightenScreen
|
||||
)
|
||||
previewScale = preferences.getFloat(
|
||||
PREVIEW_SCALE,
|
||||
previewScale
|
||||
)
|
||||
automatedActions = fromJsonArray(
|
||||
preferences.getString(AUTOMATED_ACTIONS, "[]") ?: "[]"
|
||||
)
|
||||
}
|
||||
|
||||
fun setAutomatedActions(actions: List<AutomatedAction>) {
|
||||
automatedActions = actions.toMutableList()
|
||||
apply(AUTOMATED_ACTIONS, toJsonArray(actions))
|
||||
}
|
||||
|
||||
private fun addFormatsOnUpdate(
|
||||
restored: Set<String>,
|
||||
vararg formats: BarcodeFormat
|
||||
) = restored.toMutableSet().apply {
|
||||
for (format in formats) {
|
||||
val name = "${format.name}_added"
|
||||
if (!preferences.getBoolean(name, false)) {
|
||||
preferences.edit {
|
||||
putBoolean(name, true)
|
||||
}
|
||||
add(format.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun migrateBarcodeFormats(formats: Set<String>): Set<String> {
|
||||
return formats.mapTo(mutableSetOf()) { format ->
|
||||
format.migrateBarcodeFormatName()
|
||||
}
|
||||
}
|
||||
|
||||
fun restoreCropHandle(
|
||||
name: String,
|
||||
viewWidth: Int,
|
||||
viewHeight: Int
|
||||
) = preferences.restoreCropHandle(name, viewWidth, viewHeight)
|
||||
|
||||
fun storeCropHandle(
|
||||
name: String,
|
||||
viewWidth: Int,
|
||||
viewHeight: Int,
|
||||
cropHandle: Point
|
||||
) = preferences.storeCropHandle(name, viewWidth, viewHeight, cropHandle)
|
||||
|
||||
fun beepTone() = when (beepToneName) {
|
||||
"tone_cdma_confirm" -> ToneGenerator.TONE_CDMA_CONFIRM
|
||||
"tone_sup_radio_ack" -> ToneGenerator.TONE_SUP_RADIO_ACK
|
||||
@@ -325,39 +473,57 @@ class Preferences {
|
||||
else -> ToneGenerator.TONE_PROP_BEEP
|
||||
}
|
||||
|
||||
private fun put(label: String, value: Boolean) =
|
||||
preferences.edit().putBoolean(label, value)
|
||||
|
||||
private fun put(label: String, value: String) =
|
||||
preferences.edit().putString(label, value)
|
||||
fun ignoreDuplicates() = when (ignoreDuplicatesName) {
|
||||
"ignore_consecutive_duplicates" -> IgnoreDuplicates.Consecutive
|
||||
"ignore_any_duplicates" -> IgnoreDuplicates.Any
|
||||
else -> IgnoreDuplicates.Never
|
||||
}
|
||||
|
||||
private fun apply(label: String, value: Boolean) {
|
||||
put(label, value).apply()
|
||||
preferences.edit {
|
||||
putBoolean(label, value)
|
||||
}
|
||||
}
|
||||
|
||||
private fun apply(label: String, value: String) {
|
||||
preferences.edit().putString(label, value).apply()
|
||||
preferences.edit {
|
||||
putString(label, value)
|
||||
}
|
||||
}
|
||||
|
||||
private fun apply(label: String, value: Int) {
|
||||
preferences.edit().putInt(label, value).apply()
|
||||
preferences.edit {
|
||||
putInt(label, value)
|
||||
}
|
||||
}
|
||||
|
||||
private fun apply(label: String, value: Float) {
|
||||
preferences.edit {
|
||||
putFloat(label, value)
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("ApplySharedPref")
|
||||
private fun commit(label: String, value: String) {
|
||||
preferences.edit().putString(label, value).commit()
|
||||
preferences.edit(true) {
|
||||
putString(label, value)
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.HONEYCOMB)
|
||||
private fun apply(label: String, value: Set<String>) {
|
||||
preferences.edit().putStringSet(label, value).apply()
|
||||
preferences.edit {
|
||||
putStringSet(label, value)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
enum class IgnoreDuplicates {
|
||||
Consecutive, Any, Never
|
||||
}
|
||||
|
||||
private const val PROFILES = "profiles"
|
||||
private const val PROFILE = "profile"
|
||||
private const val BARCODE_FORMATS = "formats"
|
||||
private const val 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"
|
||||
@@ -369,11 +535,12 @@ class Preferences {
|
||||
private const val BEEP = "beep"
|
||||
private const val BEEP_TONE_NAME = "beep_tone_name"
|
||||
private const val USE_HISTORY = "use_history"
|
||||
private const val IGNORE_CONSECUTIVE_DUPLICATES = "ignore_consecutive_duplicates"
|
||||
private const val IGNORE_DUPLICATES_NAME = "ignore_duplicates_name"
|
||||
private const val 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_CHECKSUM = "show_checksum"
|
||||
private const val SHOW_RECREATION = "show_recreation"
|
||||
private const val CLOSE_AUTOMATICALLY = "close_automatically"
|
||||
private const val DEFAULT_SEARCH_URL = "default_search_url"
|
||||
@@ -386,8 +553,11 @@ class Preferences {
|
||||
private const val CUSTOM_LOCALE = "custom_locale"
|
||||
private const val INDEX_OF_LAST_SELECTED_FORMAT = "index_of_last_selected_format"
|
||||
private const val INDEX_OF_LAST_SELECTED_EC_LEVEL = "index_of_last_selected_ec_level"
|
||||
private const val LAST_MARGIN = "last_margin"
|
||||
private const val FREE_ROTATION = "free_rotation"
|
||||
private const val EXPAND_ESCAPE_SEQUENCES = "expand_escape_sequences"
|
||||
private const val ADD_QUIET_ZONE = "add_quiet_zone"
|
||||
private const val BRIGHTEN_SCREEN = "brighten_screen"
|
||||
private const val PREVIEW_SCALE = "preview_scale"
|
||||
private const val AUTOMATED_ACTIONS = "automated_actions"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
package de.markusfisch.android.binaryeye.preference
|
||||
|
||||
import android.content.Context
|
||||
import android.support.v7.preference.DialogPreference
|
||||
import android.util.AttributeSet
|
||||
import androidx.preference.DialogPreference
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
|
||||
class UrlPreference(
|
||||
context: Context?,
|
||||
context: Context,
|
||||
attrs: AttributeSet?
|
||||
) : DialogPreference(context, attrs) {
|
||||
private var url: String? = null
|
||||
|
||||
@@ -5,7 +5,7 @@ import android.app.PendingIntent
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.service.quicksettings.TileService
|
||||
import android.support.annotation.RequiresApi
|
||||
import androidx.annotation.RequiresApi
|
||||
import de.markusfisch.android.binaryeye.activity.CameraActivity
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.N)
|
||||
|
||||
@@ -70,7 +70,13 @@ fun String.unescape(): String {
|
||||
val sb = StringBuilder()
|
||||
while (i < len) {
|
||||
val ch = get(i++)
|
||||
sb.append(if (ch == '\\' && i < len) expand() else ch)
|
||||
sb.append(
|
||||
if (ch == '\\' && i < len) {
|
||||
expand()
|
||||
} else {
|
||||
ch
|
||||
}
|
||||
)
|
||||
}
|
||||
return sb.toString()
|
||||
}
|
||||
|
||||
@@ -2,15 +2,16 @@ package de.markusfisch.android.binaryeye.view
|
||||
|
||||
import android.content.Context
|
||||
import android.content.ContextWrapper
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
import android.os.Build
|
||||
import android.support.v4.content.ContextCompat
|
||||
import android.support.v7.app.AppCompatActivity
|
||||
import android.support.v7.widget.LinearLayoutManager
|
||||
import android.support.v7.widget.RecyclerView
|
||||
import android.view.View
|
||||
import android.view.WindowManager
|
||||
import android.widget.AbsListView
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.graphics.drawable.toDrawable
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
|
||||
val systemBarListViewScrollListener = object : AbsListView.OnScrollListener {
|
||||
@@ -25,7 +26,8 @@ val systemBarListViewScrollListener = object : AbsListView.OnScrollListener {
|
||||
view.post {
|
||||
val scrolled = firstVisibleItem > 0 ||
|
||||
(totalItemCount > 0 && firstChildScrolled(view))
|
||||
val scrollable = scrolled || totalItemCount > 0 && lastChildOutOfView(view)
|
||||
val scrollable = scrolled ||
|
||||
totalItemCount > 0 && lastChildOutOfView(view)
|
||||
colorSystemAndToolBars(view.context, scrolled, scrollable)
|
||||
}
|
||||
}
|
||||
@@ -37,9 +39,11 @@ val systemBarListViewScrollListener = object : AbsListView.OnScrollListener {
|
||||
val systemBarRecyclerViewScrollListener = object : RecyclerView.OnScrollListener() {
|
||||
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
||||
val layoutManager = recyclerView.layoutManager as LinearLayoutManager
|
||||
val scrolled = layoutManager.findFirstCompletelyVisibleItemPosition() != 0
|
||||
val scrollable = scrolled || layoutManager.findLastVisibleItemPosition() <
|
||||
recyclerView.adapter.itemCount - 1
|
||||
val scrolled =
|
||||
layoutManager.findFirstCompletelyVisibleItemPosition() != 0
|
||||
val scrollable = scrolled ||
|
||||
layoutManager.findLastVisibleItemPosition() <
|
||||
(recyclerView.adapter?.itemCount ?: 0) - 1
|
||||
colorSystemAndToolBars(recyclerView.context, scrolled, scrollable)
|
||||
}
|
||||
|
||||
@@ -57,19 +61,25 @@ private fun lastChildOutOfView(listView: AbsListView): Boolean {
|
||||
return child != null && child.bottom >= listView.height
|
||||
}
|
||||
|
||||
fun initSystemBars(activity: AppCompatActivity) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
// Keeps the soft keyboard from repositioning the layout.
|
||||
val window = activity.window
|
||||
window.setSoftInputMode(
|
||||
WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE
|
||||
)
|
||||
window.decorView.systemUiVisibility =
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_STABLE or
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
|
||||
}
|
||||
colorSystemAndToolBars(activity)
|
||||
fun AppCompatActivity.initBars() {
|
||||
// Keeps the soft keyboard from repositioning the layout.
|
||||
window.setSoftInputMode(
|
||||
WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE
|
||||
)
|
||||
window.decorView.systemUiVisibility =
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_STABLE or
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
|
||||
colorSystemAndToolBars(this)
|
||||
setPaddingFromWindowInsets(
|
||||
findViewById(R.id.main),
|
||||
(findViewById(R.id.toolbar) as Toolbar).apply {
|
||||
setSupportActionBar(this)
|
||||
},
|
||||
findViewById<View>(R.id.navbar)?.apply {
|
||||
setBackgroundColor(translucentPrimaryColor)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private var statusBarColorLocked = false
|
||||
@@ -82,7 +92,6 @@ fun unlockStatusBarColor() {
|
||||
}
|
||||
|
||||
private var translucentPrimaryColor = 0
|
||||
private val actionBarBackground = ColorDrawable()
|
||||
fun colorSystemAndToolBars(
|
||||
context: Context,
|
||||
scrolled: Boolean = false,
|
||||
@@ -96,27 +105,29 @@ fun colorSystemAndToolBars(
|
||||
}
|
||||
val topColor = if (scrolled) translucentPrimaryColor else 0
|
||||
val activity = getAppCompatActivity(context) ?: return
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
// System bars no longer have a background from SDK35+.
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.VANILLA_ICE_CREAM) {
|
||||
val window = activity.window
|
||||
if (!statusBarColorLocked) {
|
||||
@Suppress("DEPRECATION")
|
||||
window.statusBarColor = topColor
|
||||
}
|
||||
@Suppress("DEPRECATION")
|
||||
window.navigationBarColor = if (scrolled || scrollable) {
|
||||
translucentPrimaryColor
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
activity.supportActionBar?.setBackgroundDrawable(
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
|
||||
// Avoid allocation on Honeycomb and better.
|
||||
actionBarBackground.color = topColor
|
||||
actionBarBackground
|
||||
} else {
|
||||
// ColorDrawable.setColor() doesn't exist pre Honeycomb.
|
||||
ColorDrawable(topColor)
|
||||
} else {
|
||||
activity.findViewById<View>(R.id.navbar)?.apply {
|
||||
visibility = if (scrolled || scrollable) {
|
||||
View.VISIBLE
|
||||
} else {
|
||||
View.GONE
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
activity.supportActionBar?.setBackgroundDrawable(topColor.toDrawable())
|
||||
}
|
||||
|
||||
private fun getAppCompatActivity(context: Context): AppCompatActivity? {
|
||||
|
||||
@@ -2,15 +2,45 @@ package de.markusfisch.android.binaryeye.view
|
||||
|
||||
import android.graphics.Rect
|
||||
import android.os.Build
|
||||
import android.support.annotation.RequiresApi
|
||||
import android.support.v4.view.ViewCompat
|
||||
import android.support.v4.view.WindowInsetsCompat
|
||||
import android.support.v7.widget.Toolbar
|
||||
import android.view.View
|
||||
import android.view.WindowInsets
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
|
||||
private var toolbarHeight = 0
|
||||
fun recordToolbarHeight(toolbar: Toolbar) {
|
||||
fun setPaddingFromWindowInsets(
|
||||
mainLayout: View,
|
||||
toolbar: Toolbar,
|
||||
navbar: View?
|
||||
) {
|
||||
toolbarHeight = toolbar.layoutParams.height
|
||||
// SDK 35+ is edge-to-edge by default, so the toolbar needs to extend
|
||||
// below the status bar.
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.VANILLA_ICE_CREAM) {
|
||||
return
|
||||
}
|
||||
mainLayout.setOnApplyWindowInsetsListener { _, insets ->
|
||||
val systemBarInsets = insets.getInsets(
|
||||
WindowInsets.Type.systemBars()
|
||||
)
|
||||
|
||||
val statusBarTop = systemBarInsets.top
|
||||
if (toolbar.paddingTop == 0 && statusBarTop > 0) {
|
||||
toolbar.setPadding(0, statusBarTop, 0, 0)
|
||||
toolbar.layoutParams.height += statusBarTop
|
||||
}
|
||||
|
||||
val navBarBottom = systemBarInsets.bottom
|
||||
if (navBarBottom > 0) {
|
||||
mainLayout.setPadding(0, 0, 0, navBarBottom);
|
||||
navbar?.apply {
|
||||
layoutParams.height = navBarBottom
|
||||
}
|
||||
}
|
||||
|
||||
insets
|
||||
}
|
||||
}
|
||||
|
||||
fun View.setPaddingFromWindowInsets() {
|
||||
@@ -22,19 +52,15 @@ fun View.setPaddingFromWindowInsets() {
|
||||
// For the original post see here:
|
||||
// https://medium.com/androiddevelopers/windowinsets-listeners-to-layouts-8f9ccc8fa4d1
|
||||
fun View.doOnApplyWindowInsets(f: (View, Rect) -> Unit) {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
|
||||
f(this, insetsWithToolbar())
|
||||
} else {
|
||||
ViewCompat.setOnApplyWindowInsetsListener(this) { v, insets ->
|
||||
f(v, insetsWithToolbar(insets))
|
||||
insets
|
||||
}
|
||||
// It's important to explicitly request the insets (again) in
|
||||
// case the view was created in Fragment.onCreateView() because
|
||||
// setOnApplyWindowInsetsListener() won't fire when the view
|
||||
// isn't attached.
|
||||
requestApplyInsetsWhenAttached()
|
||||
ViewCompat.setOnApplyWindowInsetsListener(this) { v, insets ->
|
||||
f(v, insetsWithToolbar(insets))
|
||||
insets
|
||||
}
|
||||
// It's important to explicitly request the insets (again) in
|
||||
// case the view was created in Fragment.onCreateView() because
|
||||
// setOnApplyWindowInsetsListener() won't fire when the view
|
||||
// isn't attached.
|
||||
requestApplyInsetsWhenAttached()
|
||||
}
|
||||
|
||||
private fun insetsWithToolbar(insets: WindowInsetsCompat? = null) = Rect(
|
||||
@@ -44,7 +70,6 @@ private fun insetsWithToolbar(insets: WindowInsetsCompat? = null) = Rect(
|
||||
insets?.systemWindowInsetBottom ?: 0
|
||||
)
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.KITKAT)
|
||||
private fun View.requestApplyInsetsWhenAttached() {
|
||||
if (isAttachedToWindow) {
|
||||
ViewCompat.requestApplyInsets(this)
|
||||
|
||||
+5
@@ -10,6 +10,8 @@ import de.markusfisch.android.scalingimageview.widget.ScalingImageView
|
||||
open class ConfinedScalingImageView : ScalingImageView {
|
||||
val insets = Rect()
|
||||
|
||||
var runAfterLayout: (() -> Unit)? = null
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet, defStyle: Int) :
|
||||
super(context, attrs, defStyle)
|
||||
|
||||
@@ -37,5 +39,8 @@ open class ConfinedScalingImageView : ScalingImageView {
|
||||
)
|
||||
}
|
||||
centerRemap()
|
||||
runAfterLayout?.invoke()
|
||||
}
|
||||
|
||||
fun isInBounds(): Boolean = inBounds()
|
||||
}
|
||||
|
||||
@@ -27,7 +27,8 @@ class ConfinedScrollView : ScrollView {
|
||||
// not putting it on the queue makes it only work sometimes.
|
||||
post {
|
||||
getChildAt(0)?.also { child ->
|
||||
scrollable = height < child.height + paddingTop + paddingBottom
|
||||
scrollable = height <
|
||||
child.height + paddingTop + paddingBottom
|
||||
colorSystemAndToolBars(
|
||||
context,
|
||||
scrollY > 0,
|
||||
|
||||
@@ -2,18 +2,21 @@ package de.markusfisch.android.binaryeye.widget
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.graphics.*
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Paint
|
||||
import android.graphics.Path
|
||||
import android.graphics.Point
|
||||
import android.graphics.Rect
|
||||
import android.graphics.Region
|
||||
import android.os.Build
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
import android.support.v4.content.ContextCompat
|
||||
import android.util.AttributeSet
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.graphics.withSave
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.app.prefs
|
||||
import de.markusfisch.android.binaryeye.graphics.getBitmapFromDrawable
|
||||
import de.markusfisch.android.binaryeye.graphics.getDashedBorderPaint
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
@@ -23,15 +26,14 @@ class DetectorView : View {
|
||||
val coordinates = FloatArray(32)
|
||||
val roi = Rect()
|
||||
|
||||
var cropHandleName: String? = null
|
||||
var onRoiChange: (() -> Unit)? = null
|
||||
var onRoiChanged: (() -> Unit)? = null
|
||||
|
||||
private val currentOrientation = resources.configuration.orientation
|
||||
private val invalidateRunnable: Runnable = Runnable {
|
||||
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
|
||||
@@ -42,7 +44,8 @@ class DetectorView : View {
|
||||
)
|
||||
private val handleXRadius = handleBitmap.width / 2
|
||||
private val handleYRadius = handleBitmap.height / 2
|
||||
private val handlePos = Point(-1, -1)
|
||||
private val inactiveHandlePos = Point(-1, -1)
|
||||
private val handlePos = Point(inactiveHandlePos)
|
||||
private val handleHome = Point()
|
||||
private val center = Point()
|
||||
private val touchDown = Point()
|
||||
@@ -52,6 +55,8 @@ class DetectorView : View {
|
||||
private val fabHeight: Int
|
||||
private val padding: Int
|
||||
|
||||
private var viewWidth = 0
|
||||
private var viewHeight = 0
|
||||
private var coordinatesLast = 0
|
||||
private var handleGrabbed = false
|
||||
private var handleActive = false
|
||||
@@ -78,37 +83,38 @@ class DetectorView : View {
|
||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) :
|
||||
super(context, attrs, defStyleAttr)
|
||||
|
||||
fun saveCropHandlePos() {
|
||||
val pos = getCropHandlePos()
|
||||
prefs.cropHandleX = pos.x
|
||||
prefs.cropHandleY = pos.y
|
||||
prefs.cropHandleOrientation = currentOrientation
|
||||
}
|
||||
|
||||
private fun getCropHandlePos() = if (handleActive) {
|
||||
handlePos
|
||||
} else {
|
||||
Point(-1, -1)
|
||||
}
|
||||
|
||||
fun restoreCropHandlePos() {
|
||||
setCropHandlePos(
|
||||
prefs.cropHandleX,
|
||||
prefs.cropHandleY,
|
||||
prefs.cropHandleOrientation
|
||||
fun storeCropHandlePos() {
|
||||
val name = cropHandleName ?: return
|
||||
val pos = if (handleActive) {
|
||||
handlePos
|
||||
} else {
|
||||
inactiveHandlePos
|
||||
}
|
||||
prefs.storeCropHandle(
|
||||
name,
|
||||
viewWidth,
|
||||
viewHeight,
|
||||
pos
|
||||
)
|
||||
}
|
||||
|
||||
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)
|
||||
fun updateCropHandlePos() {
|
||||
val name = cropHandleName ?: return
|
||||
if (viewWidth < 1 || viewHeight < 1) {
|
||||
return
|
||||
}
|
||||
val cropHandle = prefs.restoreCropHandle(name, viewWidth, viewHeight)
|
||||
if (cropHandle.x == -2) {
|
||||
setHandleToDefaultRoi()
|
||||
} else {
|
||||
handlePos.set(cropHandle.x, cropHandle.y)
|
||||
handleActive = handlePos.x > -1
|
||||
}
|
||||
if (handleActive) {
|
||||
updateClipRect()
|
||||
} else {
|
||||
reset()
|
||||
}
|
||||
handleActive = handlePos.x > -1
|
||||
}
|
||||
|
||||
fun update(numberOfCoordinates: Int) {
|
||||
@@ -118,31 +124,6 @@ class DetectorView : View {
|
||||
postDelayed(invalidateRunnable, 500)
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(): Parcelable? {
|
||||
if (!handleActive) {
|
||||
return super.onSaveInstanceState()
|
||||
}
|
||||
return SavedState(super.onSaveInstanceState()).apply {
|
||||
savedHandlePos.set(getCropHandlePos())
|
||||
savedOrientation = currentOrientation
|
||||
}
|
||||
}
|
||||
|
||||
override fun onRestoreInstanceState(state: Parcelable) {
|
||||
super.onRestoreInstanceState(
|
||||
if (state is SavedState) {
|
||||
setCropHandlePos(
|
||||
state.savedHandlePos.x,
|
||||
state.savedHandlePos.y,
|
||||
state.savedOrientation
|
||||
)
|
||||
state.superState
|
||||
} else {
|
||||
state
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
override fun onTouchEvent(event: MotionEvent?): Boolean {
|
||||
event ?: return super.onTouchEvent(event)
|
||||
@@ -239,28 +220,27 @@ class DetectorView : View {
|
||||
roi.set(0, 0, 0, 0)
|
||||
}
|
||||
|
||||
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
|
||||
override fun onLayout(
|
||||
changed: Boolean,
|
||||
left: Int,
|
||||
top: Int,
|
||||
right: Int,
|
||||
bottom: Int
|
||||
) {
|
||||
super.onLayout(changed, left, top, right, bottom)
|
||||
val width = right - left
|
||||
val height = bottom - top
|
||||
viewWidth = right - left
|
||||
viewHeight = bottom - top
|
||||
center.set(
|
||||
left + (width / 2),
|
||||
top + (height / 2)
|
||||
left + (viewWidth / 2),
|
||||
top + (viewHeight / 2)
|
||||
)
|
||||
minY = padding * 2
|
||||
maxY = height - minY
|
||||
maxY = viewHeight - minY
|
||||
handleHome.set(
|
||||
width - handleXRadius - paddingRight - padding,
|
||||
height - handleYRadius - paddingBottom - fabHeight
|
||||
viewWidth - handleXRadius - paddingRight - padding,
|
||||
viewHeight - handleYRadius - paddingBottom - fabHeight
|
||||
)
|
||||
if (handlePos.x == -2) {
|
||||
setHandleToDefaultRoi()
|
||||
}
|
||||
if (handleActive) {
|
||||
updateClipRect()
|
||||
} else {
|
||||
reset()
|
||||
}
|
||||
updateCropHandlePos()
|
||||
}
|
||||
|
||||
override fun onDraw(canvas: Canvas) {
|
||||
@@ -289,10 +269,8 @@ class DetectorView : View {
|
||||
if (minDist < 1) {
|
||||
return
|
||||
}
|
||||
// canvas.clipRect() doesn't work reliably below KITKAT.
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||
val radius = min(minDist / 2, cornerRadius).toFloat()
|
||||
save()
|
||||
val radius = min(minDist / 2, cornerRadius).toFloat()
|
||||
withSave {
|
||||
clipOutPathCompat(
|
||||
calculateRoundedRectPath(
|
||||
roi.left.toFloat(),
|
||||
@@ -304,9 +282,6 @@ class DetectorView : View {
|
||||
)
|
||||
)
|
||||
drawColor(shadeColor)
|
||||
restore()
|
||||
} else {
|
||||
drawRect(roi, roiPaint)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -344,37 +319,6 @@ class DetectorView : View {
|
||||
private fun clampX(x: Int) = min(center.x * 2, max(0, x))
|
||||
|
||||
private fun clampY(y: Int) = min(maxY, max(minY, y))
|
||||
|
||||
internal class SavedState : BaseSavedState {
|
||||
val savedHandlePos = Point()
|
||||
|
||||
var savedOrientation = 0
|
||||
|
||||
constructor(superState: Parcelable?) : super(superState)
|
||||
|
||||
private constructor(parcel: Parcel) : super(parcel) {
|
||||
savedHandlePos.set(
|
||||
parcel.readInt(),
|
||||
parcel.readInt()
|
||||
)
|
||||
savedOrientation = parcel.readInt()
|
||||
}
|
||||
|
||||
override fun writeToParcel(out: Parcel, flags: Int) {
|
||||
super.writeToParcel(out, flags)
|
||||
out.writeInt(savedHandlePos.x)
|
||||
out.writeInt(savedHandlePos.y)
|
||||
out.writeInt(savedOrientation)
|
||||
}
|
||||
|
||||
companion object {
|
||||
@JvmField
|
||||
val CREATOR = object : Parcelable.Creator<SavedState> {
|
||||
override fun createFromParcel(source: Parcel) = SavedState(source)
|
||||
override fun newArray(size: Int): Array<SavedState?> = arrayOfNulls(size)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun distSq(a: Point, b: Point): Int {
|
||||
|
||||
@@ -3,19 +3,29 @@ package de.markusfisch.android.binaryeye.widget
|
||||
import android.content.Context
|
||||
import android.widget.Toast
|
||||
|
||||
fun Context.toast(message: Int) = Toast.makeText(
|
||||
applicationContext,
|
||||
message,
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
private var lastToast: Toast? = null
|
||||
|
||||
fun Context.toast(message: String) = Toast.makeText(
|
||||
applicationContext,
|
||||
message.ellipsize(128),
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
fun <T> Context.toast(message: T) {
|
||||
lastToast?.cancel()
|
||||
lastToast = when (message) {
|
||||
is String -> Toast.makeText(
|
||||
applicationContext,
|
||||
message.ellipsize(),
|
||||
Toast.LENGTH_LONG
|
||||
)
|
||||
|
||||
private fun String.ellipsize(max: Int) = if (length < max) {
|
||||
is Int -> Toast.makeText(
|
||||
applicationContext,
|
||||
message,
|
||||
Toast.LENGTH_LONG
|
||||
)
|
||||
|
||||
else -> return
|
||||
}
|
||||
lastToast?.show()
|
||||
}
|
||||
|
||||
private fun String.ellipsize(max: Int = 128) = if (length < max) {
|
||||
this
|
||||
} else {
|
||||
"${take(max)}…"
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
package de.markusfisch.android.binaryeye.zxingcpp
|
||||
|
||||
/**
|
||||
* Translate legacy barcode format names from ZXingC++ before 3.x
|
||||
* to current BarcodeFormat enum names.
|
||||
*/
|
||||
fun String.migrateBarcodeFormatName() = when (this) {
|
||||
"NONE" -> "None"
|
||||
"AZTEC" -> "Aztec"
|
||||
"CODABAR" -> "Codabar"
|
||||
"CODE_39" -> "Code39"
|
||||
"CODE_39_EXT" -> "Code39Ext"
|
||||
"CODE_32" -> "Code32"
|
||||
"PZN" -> "PZN"
|
||||
"CODE_93" -> "Code93"
|
||||
"CODE_128" -> "Code128"
|
||||
"DATA_BAR" -> "DataBar"
|
||||
"DATA_BAR_OMNI" -> "DataBarOmni"
|
||||
"DATA_BAR_STK" -> "DataBarStk"
|
||||
"DATA_BAR_LTD" -> "DataBarLtd"
|
||||
"DATA_BAR_EXPANDED" -> "DataBarExp"
|
||||
"DATA_BAR_EXP_STK" -> "DataBarExpStk"
|
||||
"DATA_MATRIX" -> "DataMatrix"
|
||||
"DX_FILM_EDGE" -> "DXFilmEdge"
|
||||
"EAN_8" -> "EAN8"
|
||||
"EAN_13" -> "EAN13"
|
||||
"ITF" -> "ITF"
|
||||
"MAXICODE" -> "MaxiCode"
|
||||
"PDF_417" -> "PDF417"
|
||||
"QR_CODE" -> "QRCode"
|
||||
"MICRO_QR_CODE" -> "MicroQRCode"
|
||||
"RMQR_CODE" -> "RMQRCode"
|
||||
"UPC_A" -> "UPCA"
|
||||
"UPC_E" -> "UPCE"
|
||||
else -> this
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/accent"/>
|
||||
<foreground android:drawable="@drawable/ic_shortcut_history_foreground"/>
|
||||
</adaptive-icon>
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 835 B |
Binary file not shown.
|
After Width: | Height: | Size: 471 B |
Binary file not shown.
|
After Width: | Height: | Size: 590 B |
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.6 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 2.2 KiB |
@@ -0,0 +1,16 @@
|
||||
<selector
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:constantSize="true">
|
||||
<item android:state_pressed="false">
|
||||
<shape android:shape="rectangle">
|
||||
<corners android:radius="2dp"/>
|
||||
<solid android:color="@color/selected_row"/>
|
||||
</shape>
|
||||
</item>
|
||||
<item android:state_pressed="true">
|
||||
<shape android:shape="rectangle">
|
||||
<corners android:radius="2dp"/>
|
||||
<solid android:color="@color/pressed_row"/>
|
||||
</shape>
|
||||
</item>
|
||||
</selector>
|
||||
@@ -0,0 +1,3 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#FFFFFF" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
|
||||
<path android:fillColor="#fff" android:pathData="M20 8.69V4h-4.69L12 0.69 8.69 4H4v4.69L0.69 12 4 15.31V20h4.69L12 23.31 15.31 20H20v-4.69L23.31 12 20 8.69zM12 18c-3.31 0-6-2.69-6-6s2.69-6 6-6 6 2.69 6 6-2.69 6-6 6zm0-10c-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4-1.79-4-4-4z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,3 @@
|
||||
<vector android:height="24dp" android:tint="#FFFFFF" android:viewportHeight="24.0" android:viewportWidth="24.0" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#FF000000" android:pathData="M14.54 18.49v-1.98h2.97v-2.97h1.98v2.97h2.97v1.98l-2.952 0.018-0.018 2.952h-1.98l-0.018-2.952-2.952-0.018zm-1.19-3.74L11 12.4V7h2v4.6l1.4 1.4-1.05 1.75zM3 10V4h2v2.35c0.85-1.067 1.888-1.892 3.113-2.475C9.338 3.292 10.633 3 12 3c2.5 0 4.625 0.875 6.375 2.625S21 9.5 21 12h-2c0-1.95-0.679-3.604-2.037-4.963C15.604 5.679 13.95 5 12 5c-1.15 0-2.225 0.267-3.225 0.8S6.933 7.067 6.25 8H9v2H3zm9 11c-2.3 0-4.304-0.763-6.012-2.288C4.279 17.187 3.3 15.283 3.05 13H5.1c0.233 1.733 1.004 3.167 2.313 4.3C8.721 18.433 10.25 19 12 19c0.183 0 0.354-0.004 0.513-0.013 0.158-0.008 0.32-0.029 0.487-0.062v2.025c-0.167 0.017-0.329 0.029-0.487 0.037C12.354 20.996 12.183 21 12 21z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,3 @@
|
||||
<vector android:height="24dp" android:tint="#FFFFFF" android:viewportHeight="24.0" android:viewportWidth="24.0" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#FF000000" android:pathData="M3.9,12c0,-1.71 1.39,-3.1 3.1,-3.1h4L11,7L7,7c-2.76,0 -5,2.24 -5,5s2.24,5 5,5h4v-1.9L7,15.1c-1.71,0 -3.1,-1.39 -3.1,-3.1zM8,13h8v-2L8,11v2zM17,7h-4v1.9h4c1.71,0 3.1,1.39 3.1,3.1s-1.39,3.1 -3.1,3.1h-4L13,17h4c2.76,0 5,-2.24 5,-5s-2.24,-5 -5,-5z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,3 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#FFFFFF" android:viewportHeight="960" android:viewportWidth="960" android:width="24dp">
|
||||
<path android:fillColor="#fff" android:pathData="M480,480Q414,480 367,433Q320,386 320,320Q320,254 367,207Q414,160 480,160Q546,160 593,207Q640,254 640,320Q640,386 593,433Q546,480 480,480ZM160,800L160,688Q160,654 177.5,625.5Q195,597 224,582Q286,551 350,535.5Q414,520 480,520Q546,520 610,535.5Q674,551 736,582Q765,597 782.5,625.5Q800,654 800,688L800,800L160,800ZM240,720L720,720L720,688Q720,677 714.5,668Q709,659 700,654Q646,627 591,613.5Q536,600 480,600Q424,600 369,613.5Q314,627 260,654Q251,659 245.5,668Q240,677 240,688L240,720ZM480,400Q513,400 536.5,376.5Q560,353 560,320Q560,287 536.5,263.5Q513,240 480,240Q447,240 423.5,263.5Q400,287 400,320Q400,353 423.5,376.5Q447,400 480,400ZM480,320Q480,320 480,320Q480,320 480,320Q480,320 480,320Q480,320 480,320Q480,320 480,320Q480,320 480,320Q480,320 480,320Q480,320 480,320ZM480,720L480,720Q480,720 480,720Q480,720 480,720Q480,720 480,720Q480,720 480,720Q480,720 480,720Q480,720 480,720Q480,720 480,720Q480,720 480,720L480,720Z"/>
|
||||
</vector>
|
||||
@@ -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="M54 72.9c-4.83 0-9.04-1.6-12.63-4.8-3.59-3.21-5.64-7.2-6.17-12h4.31c0.49 3.64 2.11 6.65 4.86 9.03 2.74 2.38 5.95 3.57 9.63 3.57 4.1 0 7.57-1.42 10.42-4.28C67.28 61.57 68.7 58.1 68.7 54s-1.42-7.57-4.28-10.42C61.57 40.72 58.1 39.3 54 39.3c-2.42 0-4.67 0.56-6.77 1.68-2.1 1.12-3.87 2.66-5.31 4.62h5.78v4.2H35.1V37.2h4.2v4.93c1.78-2.24 3.96-3.97 6.54-5.19 2.57-1.23 5.29-1.84 8.16-1.84 2.63 0 5.08 0.5 7.38 1.49 2.29 1 4.29 2.35 5.98 4.05 1.7 1.69 3.05 3.69 4.05 5.98 0.99 2.3 1.49 4.75 1.49 7.38s-0.5 5.08-1.49 7.38c-1 2.29-2.35 4.29-4.05 5.98-1.69 1.7-3.69 3.05-5.98 4.05-2.3 0.99-4.75 1.49-7.38 1.49zm5.88-10.08l-7.98-7.98V43.5h4.2v9.66l6.72 6.72-2.94 2.94z" android:fillColor="#FFFFFF"/>
|
||||
</vector>
|
||||
@@ -1,16 +1,20 @@
|
||||
<merge xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<de.markusfisch.android.cameraview.widget.CameraView
|
||||
<merge
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
<androidx.camera.view.PreviewView
|
||||
android:id="@+id/camera_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"/>
|
||||
android:layout_height="match_parent"
|
||||
app:scaleType="fillCenter"/>
|
||||
<de.markusfisch.android.binaryeye.widget.DetectorView
|
||||
android:id="@+id/detector_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"/>
|
||||
<android.support.design.widget.CoordinatorLayout
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
android:id="@+id/main"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:fitsSystemWindows="true">
|
||||
android:fitsSystemWindows="@bool/fitsSystemWindows">
|
||||
<include layout="@layout/toolbar"/>
|
||||
<SeekBar
|
||||
android:id="@+id/zoom"
|
||||
@@ -18,15 +22,13 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom|start"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:layout_marginRight="88dp"
|
||||
android:layout_marginEnd="88dp"
|
||||
android:layout_marginLeft="16dp"
|
||||
android:layout_marginStart="16dp"
|
||||
android:padding="24dp"/>
|
||||
<android.support.design.widget.FloatingActionButton
|
||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
style="@style/FAB"
|
||||
android:id="@+id/flash"
|
||||
android:contentDescription="@string/toggle_flash"
|
||||
android:src="@drawable/ic_action_flash"/>
|
||||
</android.support.design.widget.CoordinatorLayout>
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
</merge>
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
<merge xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<android.support.design.widget.CoordinatorLayout
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
android:id="@+id/content_frame"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"/>
|
||||
<FrameLayout
|
||||
android:id="@+id/main"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:fitsSystemWindows="true">
|
||||
android:fitsSystemWindows="@bool/fitsSystemWindows">
|
||||
<include layout="@layout/toolbar"/>
|
||||
</FrameLayout>
|
||||
<include layout="@layout/navbar"/>
|
||||
</merge>
|
||||
|
||||
@@ -8,15 +8,17 @@
|
||||
android:id="@+id/detector_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"/>
|
||||
<android.support.design.widget.CoordinatorLayout
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
android:id="@+id/main"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:fitsSystemWindows="true">
|
||||
android:fitsSystemWindows="@bool/fitsSystemWindows">
|
||||
<include layout="@layout/toolbar"/>
|
||||
<android.support.design.widget.FloatingActionButton
|
||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
style="@style/FAB"
|
||||
android:id="@+id/scan"
|
||||
android:contentDescription="@string/scan_code"
|
||||
android:src="@drawable/ic_action_next"/>
|
||||
</android.support.design.widget.CoordinatorLayout>
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
<include layout="@layout/navbar"/>
|
||||
</merge>
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
<ScrollView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:fillViewport="true">
|
||||
<FrameLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:fitsSystemWindows="true">
|
||||
<!-- The FrameLayout is necessary because this LinearLayout
|
||||
should have a padding which would be reset by the window
|
||||
insets from `fitsSystemWindows`. -->
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="bottom"
|
||||
android:padding="24dp"
|
||||
android:orientation="vertical"
|
||||
tools:ignore="UselessParent">
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="32dp"
|
||||
android:drawablePadding="16dp"
|
||||
android:gravity="center"
|
||||
android:text="@string/welcome"
|
||||
android:textAppearance="?android:attr/textAppearanceLarge"
|
||||
app:drawableTopCompat="@drawable/logo"/>
|
||||
|
||||
<LinearLayout
|
||||
style="@style/WelcomeButton"
|
||||
android:id="@+id/expert"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:orientation="vertical">
|
||||
<TextView
|
||||
style="@style/WelcomeButtonText"
|
||||
android:text="@string/mode_expert"/>
|
||||
<TextView
|
||||
style="@style/WelcomeButtonSubText"
|
||||
android:text="@string/mode_expert_description"/>
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
style="@style/WelcomeButton"
|
||||
android:id="@+id/simple"
|
||||
android:orientation="vertical">
|
||||
<TextView
|
||||
style="@style/WelcomeButtonText"
|
||||
android:text="@string/mode_simple"/>
|
||||
<TextView
|
||||
style="@style/WelcomeButtonSubText"
|
||||
android:text="@string/mode_simple_description"/>
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
</FrameLayout>
|
||||
</ScrollView>
|
||||
@@ -0,0 +1,76 @@
|
||||
<ScrollView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/automated_action_regex"/>
|
||||
<EditText
|
||||
android:id="@+id/regex"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/automated_action_regex_hint"
|
||||
android:inputType="text"/>
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingTop="12dp"
|
||||
android:text="@string/automated_action_type"/>
|
||||
<RadioGroup
|
||||
android:id="@+id/action_type"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
<RadioButton
|
||||
android:id="@+id/action_type_intent"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/automated_action_type_intent"/>
|
||||
<RadioButton
|
||||
android:id="@+id/action_type_custom_intent"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/automated_action_type_custom_intent"/>
|
||||
</RadioGroup>
|
||||
<LinearLayout
|
||||
android:id="@+id/url_section"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingTop="8dp"
|
||||
android:text="@string/automated_action_url_template"/>
|
||||
<EditText
|
||||
android:id="@+id/url_template"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/automated_action_url_hint"
|
||||
android:inputType="textUri"/>
|
||||
</LinearLayout>
|
||||
<LinearLayout
|
||||
android:id="@+id/intent_section"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:visibility="gone">
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingTop="8dp"
|
||||
android:text="@string/automated_action_intent_uri_template"/>
|
||||
<EditText
|
||||
android:id="@+id/intent_uri_template"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/automated_action_intent_uri_hint"
|
||||
android:inputType="textUri"/>
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
||||
@@ -0,0 +1,22 @@
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="?android:attr/textAppearanceMedium"
|
||||
android:text="@string/find_code_with_content"/>
|
||||
<EditText
|
||||
android:id="@+id/term"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="textShortMessage"
|
||||
android:imeOptions="actionDone"
|
||||
android:hint="@string/find_code_hint"
|
||||
android:maxLines="1"
|
||||
android:maxLength="1024"
|
||||
android:importantForAutofill="no"/>
|
||||
</LinearLayout>
|
||||
@@ -0,0 +1,22 @@
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="?android:attr/textAppearanceMedium"
|
||||
android:text="@string/profile_enter_name"/>
|
||||
<EditText
|
||||
android:id="@+id/name"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="textShortMessage|textAutoCorrect|textCapSentences"
|
||||
android:imeOptions="actionDone"
|
||||
android:hint="@string/profile_enter_name_hint"
|
||||
android:maxLines="1"
|
||||
android:maxLength="1024"
|
||||
android:importantForAutofill="no"/>
|
||||
</LinearLayout>
|
||||
@@ -0,0 +1,23 @@
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
<TextView
|
||||
android:id="@+id/size_display"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="?android:attr/textAppearanceMedium"
|
||||
android:layout_marginBottom="8dp"/>
|
||||
<SeekBar
|
||||
style="@style/Widget.AppCompat.SeekBar.Discrete"
|
||||
android:id="@+id/size_bar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:paddingTop="16dp"
|
||||
android:paddingBottom="16dp"
|
||||
android:progress="2"
|
||||
android:max="4"/>
|
||||
</LinearLayout>
|
||||
@@ -0,0 +1,31 @@
|
||||
<FrameLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
<ListView
|
||||
android:id="@+id/actions"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:clipToPadding="false"
|
||||
android:divider="@color/separator"
|
||||
android:dividerHeight="1px"/>
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
android:id="@+id/inset_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
<TextView
|
||||
android:visibility="gone"
|
||||
android:id="@+id/no_actions"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:gravity="center"
|
||||
android:padding="32dp"
|
||||
android:text="@string/automated_actions_empty"/>
|
||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
style="@style/FAB"
|
||||
android:id="@+id/add"
|
||||
android:contentDescription="@string/automated_action_add"
|
||||
android:src="@drawable/ic_action_create"/>
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
</FrameLayout>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user