Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
de6a2863f3 | ||
|
|
4b2eb80612 | ||
|
|
2de40d2277 | ||
|
|
7673b7cb0d | ||
|
|
097c5e4453 | ||
|
|
1fab30089e | ||
|
|
f9bf16dd47 | ||
|
|
7a59126312 | ||
|
|
694c854650 | ||
|
|
b1cd12ec77 | ||
|
|
0c27b15025 | ||
|
|
5de950bdbf | ||
|
|
ae9e304ef6 | ||
|
|
c4cc14554b | ||
|
|
d6afccf562 | ||
|
|
a90bdad117 | ||
|
|
52d4f082f2 | ||
|
|
1c63f5b3ac | ||
|
|
21985b4533 | ||
|
|
012ac644aa | ||
|
|
19f19739fd | ||
|
|
f01f9fcd91 | ||
|
|
4177b248c3 | ||
|
|
e4182a265b | ||
|
|
98df5a447c | ||
|
|
a0a1405359 | ||
|
|
76922c421c | ||
|
|
cac9dd16da | ||
|
|
a1f9ba1056 | ||
|
|
0b67c272b6 | ||
|
|
9b8be5305d | ||
|
|
a026ade13b | ||
|
|
b1771bc5ea | ||
|
|
5199460669 | ||
|
|
fd040e1660 | ||
|
|
2e1528364e | ||
|
|
d21af142bb | ||
|
|
8395e135f9 | ||
|
|
c0caa0ce44 | ||
|
|
96927ef9b2 | ||
|
|
84d9a3b55a | ||
|
|
ffe8e4c5ba | ||
|
|
bc7c5dca97 | ||
|
|
d9737f4df1 | ||
|
|
0e8147713a | ||
|
|
3d425a097a | ||
|
|
c26765cc63 | ||
|
|
fb6dc8ba55 | ||
|
|
70f4d166a0 | ||
|
|
6c136ce719 | ||
|
|
c6dee80c0a | ||
|
|
1f43dd4827 | ||
|
|
a366157977 | ||
|
|
6bb72672a3 | ||
|
|
04774527a0 | ||
|
|
6f3e644aa8 | ||
|
|
5e3176b703 | ||
|
|
0f0aaff284 | ||
|
|
f0d09e81b4 | ||
|
|
771258cb5c | ||
|
|
9482086666 | ||
|
|
bcad50c3e8 | ||
|
|
dbd055699e | ||
|
|
d67f8273d6 | ||
|
|
649be0ceb6 | ||
|
|
f5eb5dd8d2 | ||
|
|
eb805dad2f | ||
|
|
202bb32b29 | ||
|
|
50b8a4d329 | ||
|
|
fa15478fb4 | ||
|
|
20c2a5a8aa | ||
|
|
00b95678ed | ||
|
|
5df3517563 | ||
|
|
fd6946d2c2 | ||
|
|
c74d229e1f | ||
|
|
fa99a19db5 | ||
|
|
b083fcf213 | ||
|
|
46930ddea6 | ||
|
|
7bef77b96e | ||
|
|
c5f4c088b4 | ||
|
|
73f7210e18 | ||
|
|
469429e663 | ||
|
|
1a7ac59740 | ||
|
|
c13c59e728 | ||
|
|
bc17e25717 | ||
|
|
5017bd4c8d | ||
|
|
a44a913eca | ||
|
|
007e83e243 | ||
|
|
de8f97c246 | ||
|
|
1287591e53 | ||
|
|
8c17238572 | ||
|
|
5dbb36e6a9 | ||
|
|
ca7dfb3a33 | ||
|
|
419b7d5765 | ||
|
|
fa38866669 | ||
|
|
19ca521f59 | ||
|
|
be657d86cf | ||
|
|
5199a4a526 | ||
|
|
ff48a543c9 | ||
|
|
2eadd01a87 | ||
|
|
dd2abe7d89 | ||
|
|
8e05caac70 | ||
|
|
46aeced09f | ||
|
|
3681c5d011 | ||
|
|
20b4f7aac5 | ||
|
|
bc6e350271 | ||
|
|
c9c1b2f5bf | ||
|
|
1dbc0f0fa1 | ||
|
|
ab8170ee72 | ||
|
|
551464dc6f | ||
|
|
c3aac0a1e1 | ||
|
|
d678e1cfd0 | ||
|
|
a445ade877 | ||
|
|
1fd0b7637f | ||
|
|
fdc88df897 | ||
|
|
d0f133ae00 | ||
|
|
67646261e7 | ||
|
|
faeae9e904 | ||
|
|
77b21a9e83 | ||
|
|
cbe7860a3d | ||
|
|
fc29267d3d | ||
|
|
890506328b | ||
|
|
04036b6cd6 | ||
|
|
2602021c1c | ||
|
|
17a0bdc0a9 | ||
|
|
d71a220eca | ||
|
|
0c2e711d79 | ||
|
|
ca342a83d3 | ||
|
|
3c44ff56eb | ||
|
|
bcae64c0d0 | ||
|
|
c2f673729b | ||
|
|
2043e0f349 | ||
|
|
b93eb5963e | ||
|
|
5189fc9c5a | ||
|
|
2d311ff92a | ||
|
|
14bb08003d | ||
|
|
42f9887030 | ||
|
|
a59d241b50 | ||
|
|
9f31160c7b | ||
|
|
2eb4e51c17 | ||
|
|
53a554e7a2 | ||
|
|
7f6887716a | ||
|
|
5ef93802ce | ||
|
|
c1eff08bb9 | ||
|
|
8798d44ec7 | ||
|
|
d4e31ff97e | ||
|
|
ae8b08a85a | ||
|
|
09430d776e | ||
|
|
ef88c3179d | ||
|
|
c732ec726b | ||
|
|
801c768df4 | ||
|
|
552a2f6d57 | ||
|
|
7fa8810197 | ||
|
|
254af25bbb | ||
|
|
a27656c174 | ||
|
|
2ca55ebccc | ||
|
|
ec1d40942d | ||
|
|
7852e5ce9d | ||
|
|
f36a81d4a0 | ||
|
|
d86ea4073c | ||
|
|
a69530621d | ||
|
|
3c0f29a768 | ||
|
|
ccf5157fa7 | ||
|
|
c1ebb1c154 | ||
|
|
a4778bcc02 | ||
|
|
24d2c6d8d6 | ||
|
|
0f3a07f602 | ||
|
|
751d8932f8 | ||
|
|
9c2d063c24 | ||
|
|
0f9bbf5b2f | ||
|
|
530280981e | ||
|
|
ddfbb305c7 | ||
|
|
8d1a29922b | ||
|
|
c710f7c6ab | ||
|
|
111c587eef | ||
|
|
264e494487 | ||
|
|
8135b2a3dc | ||
|
|
dce62b70d3 | ||
|
|
533812c638 | ||
|
|
5dc5bed281 | ||
|
|
2a13561962 | ||
|
|
1476f41cda | ||
|
|
0da1ffbe02 | ||
|
|
980679b6a3 | ||
|
|
28cbb55299 | ||
|
|
1a4e7d100a | ||
|
|
265e43b439 | ||
|
|
03a58c3412 | ||
|
|
327cad48f0 | ||
|
|
8e64fe2f56 | ||
|
|
368d4dbb66 | ||
|
|
c8c3c094db | ||
|
|
aa4aefe35f | ||
|
|
536b3d77c1 | ||
|
|
bbf2a80981 | ||
|
|
ae99214d6c | ||
|
|
73b5a0969c | ||
|
|
bf5dc96ac9 | ||
|
|
301d9c2513 | ||
|
|
0a11611b8f | ||
|
|
249e310e43 | ||
|
|
6279c5731a | ||
|
|
df98637951 | ||
|
|
08b3558e46 | ||
|
|
648ef6fdf8 | ||
|
|
807f075a51 | ||
|
|
27789beeed | ||
|
|
97bee27827 | ||
|
|
3fee49301e | ||
|
|
8a6073291b | ||
|
|
47151de4a4 | ||
|
|
248c4edca1 | ||
|
|
2c7386d536 | ||
|
|
e19cad4c87 | ||
|
|
585e0f3372 | ||
|
|
2b7f611774 | ||
|
|
05b2a0d9c9 | ||
|
|
e7287eb088 | ||
|
|
4b45ad3fe7 | ||
|
|
449d8fdbbd | ||
|
|
f44cff9912 | ||
|
|
24730dfde3 | ||
|
|
be6539d41f | ||
|
|
60dd40554e | ||
|
|
837119b6ec | ||
|
|
abe11b82e5 | ||
|
|
4fc1e36d04 | ||
|
|
54aa15e1be | ||
|
|
876d8fc7b3 | ||
|
|
7bdcdaaa5d | ||
|
|
fd44728e89 | ||
|
|
8242520a0b | ||
|
|
2e7e9a942b | ||
|
|
dcc9d5b3ff | ||
|
|
607aab854c | ||
|
|
cc2b2f076b | ||
|
|
90e9723de5 | ||
|
|
ab6787e60b | ||
|
|
698686a45a | ||
|
|
95bdaa1252 | ||
|
|
f0c02e5156 | ||
|
|
a8c50308d0 | ||
|
|
6b359d4c65 | ||
|
|
863caed387 | ||
|
|
e4f17b9bdf | ||
|
|
c2071d8d81 | ||
|
|
4758c2d337 | ||
|
|
91336fe15c | ||
|
|
2c437f3771 | ||
|
|
cb082b20fe | ||
|
|
bce3630686 | ||
|
|
4feb4d066c | ||
|
|
e7d84cd916 | ||
|
|
84e2917694 | ||
|
|
b87464ec40 | ||
|
|
269e0f41a6 | ||
|
|
aefbc4a296 | ||
|
|
c64984dc6a | ||
|
|
24c7ad0ba4 | ||
|
|
643a935852 | ||
|
|
4fcb836b53 | ||
|
|
48ed5f1a2c | ||
|
|
3aa2371eeb | ||
|
|
88f92943c0 | ||
|
|
39710d4ab9 | ||
|
|
93f7cd3cb8 | ||
|
|
69baa3b824 | ||
|
|
bb05e38278 | ||
|
|
554f5a5e73 | ||
|
|
2dcf7772f2 | ||
|
|
3c7012e3c4 | ||
|
|
08e11c3c40 | ||
|
|
31854dab0c | ||
|
|
4f12c3e270 | ||
|
|
3ec43dabce | ||
|
|
0d4f536f71 | ||
|
|
b064a293fb | ||
|
|
d124c29919 | ||
|
|
2e89f1530a | ||
|
|
ef7e2a4b0d | ||
|
|
f680533819 | ||
|
|
a8c977c40e | ||
|
|
1e89b22953 | ||
|
|
11f028e0f3 | ||
|
|
1f7f4a23b5 | ||
|
|
41581d07ec | ||
|
|
dc819a6e1a | ||
|
|
5b51072f1a | ||
|
|
1b10e82cb2 | ||
|
|
eeae1eb0ad | ||
|
|
21c29ca373 | ||
|
|
8bcb72e91b | ||
|
|
8f345fbfbe | ||
|
|
2c88dbca02 | ||
|
|
bd1c629c85 | ||
|
|
f89e6612c4 | ||
|
|
7a0cd0ce19 | ||
|
|
8544affa1e | ||
|
|
48d3588341 | ||
|
|
783f6e4892 | ||
|
|
032cd64daf | ||
|
|
96626e6b00 | ||
|
|
11ae65ec3e | ||
|
|
91e3acb647 | ||
|
|
40c2f5947b | ||
|
|
58b9466cfc | ||
|
|
12210e1768 | ||
|
|
ad2ac457c9 | ||
|
|
634b6e8d2b | ||
|
|
6dd47f4dd0 | ||
|
|
8426f66cfb | ||
|
|
3dfa84a33e | ||
|
|
38f658e253 | ||
|
|
287d99e2b4 | ||
|
|
5cfcde9d4a | ||
|
|
b2730259fc | ||
|
|
61a2d3023c | ||
|
|
239ac7bd1a | ||
|
|
4048314cf7 | ||
|
|
3dbd772847 | ||
|
|
3a08465c35 | ||
|
|
92e3669ee5 | ||
|
|
8b297f4768 | ||
|
|
42b7f1179b | ||
|
|
9d6854c282 | ||
|
|
d943b7deff | ||
|
|
a138da9d9b | ||
|
|
194338a0d2 | ||
|
|
7eb8fdcce1 | ||
|
|
2bf1796340 | ||
|
|
cc42015da2 | ||
|
|
648c39c439 | ||
|
|
8c25b05cda | ||
|
|
780bebc9d1 | ||
|
|
bf7eb7efd5 | ||
|
|
d00d08bae3 | ||
|
|
379256a7fe |
+167
@@ -1,5 +1,172 @@
|
||||
# Change Log
|
||||
|
||||
## 1.68.4
|
||||
* Fix generating 1D barcodes
|
||||
|
||||
## 1.68.3
|
||||
* Stability improvement
|
||||
|
||||
## 1.68.2
|
||||
* Fix barcode regeneration
|
||||
|
||||
## 1.68.1
|
||||
* Improve UI of search barcode dialog
|
||||
* Stability improvements
|
||||
* Update ZXingCpp
|
||||
* Update Russian translation
|
||||
|
||||
## 1.68.0
|
||||
* Add automated actions
|
||||
* Improve managing profiles
|
||||
|
||||
## 1.67.1
|
||||
* Fix opening file:// Uris
|
||||
* Fix auto-return/immediate opening
|
||||
* Improve Bluetooth stability
|
||||
|
||||
## 1.67.0
|
||||
* Add preference profiles
|
||||
* Support regex when searching for a barcode
|
||||
* Improve feedback when searching for a barcode
|
||||
* Update translations
|
||||
|
||||
## 1.66.2
|
||||
* Remove forcing `?content=` on forwarding URL
|
||||
* Update Spanish translation
|
||||
|
||||
## 1.66.1
|
||||
* Show elements of invalid Wifi configurations too
|
||||
* Update translations
|
||||
|
||||
## 1.66.0
|
||||
* Support deep links to encode content via web links
|
||||
* Parse International Driver Licenses (IDL) in PDF417 barcodes
|
||||
* Add WiFis from system settings
|
||||
* Show parsed data items
|
||||
* Update translations
|
||||
|
||||
## 1.65.2
|
||||
* Sanity check ROI metrics
|
||||
|
||||
## 1.65.1
|
||||
* Improve simple mode
|
||||
* Remember quiet zone settings
|
||||
* Reduce height for 1D barcodes
|
||||
* Add execute argument for binaryeye://encode
|
||||
* Update Russian translation
|
||||
|
||||
## 1.65.0
|
||||
* Add a welcome screen to choose a mode
|
||||
* Add a deep link to encode a barcode
|
||||
* Remember zoom level of generated barcodes
|
||||
* Remove Human Readable Interpretation (HRI) decoration
|
||||
* Update Italian translation
|
||||
|
||||
## 1.64.2
|
||||
* Separate crop window settings for image scanning
|
||||
* Fix initializing dataMask
|
||||
* Update Russian translation
|
||||
|
||||
## 1.64.1
|
||||
* Handle empty input for recreation
|
||||
* Fix setting up database
|
||||
|
||||
## 1.64.0
|
||||
* Find barcode by content
|
||||
* Immediately replace toasts
|
||||
* Use detected barcode for recreation
|
||||
* Show data mask of QR Codes
|
||||
* Fix deep linking
|
||||
* Replace margin with quiet zone switch when generating barcodes
|
||||
* Update Russian translation
|
||||
* Update Portuguese (Brazil) translation
|
||||
|
||||
## 1.63.13
|
||||
* Fix UTF-8 conversion of binary escape sequences when creating barcodes
|
||||
* Fix scan order for intervals under one second
|
||||
* Add arabic translation
|
||||
* Update French translation
|
||||
|
||||
## 1.63.12
|
||||
* Fix magnifying barcode recreation
|
||||
|
||||
## 1.63.11
|
||||
* Update parcel tracking URL
|
||||
* Update multiple translations
|
||||
* Only ask for barcode size when exporting/sharing
|
||||
|
||||
## 1.63.10
|
||||
* Use maximum brightness when showing a generated barcode
|
||||
* Allow file input from share menu
|
||||
* Update multiple translations
|
||||
|
||||
## 1.63.9
|
||||
* Export/share as JPEG
|
||||
* Ignore "URL:" before URLs in barcode content
|
||||
* Fix exporting/sharing multiple history items
|
||||
* Update multiple translations
|
||||
|
||||
## 1.63.8
|
||||
* Add an option to show a checksum for the decoded content
|
||||
* Add support for encoding escape sequences for non-printable characters
|
||||
* Update Italian translation
|
||||
|
||||
## 1.63.7
|
||||
* Update ZXingCpp for Aztec Runes
|
||||
* Update Italian translation
|
||||
|
||||
## 1.63.6
|
||||
* Add history shortcut
|
||||
* Return Codabar start/stop codes
|
||||
* Update French, Russian, Spanish and Ukrainian translations
|
||||
|
||||
## 1.63.5
|
||||
* Add an option to ignore any duplicates
|
||||
* Fix invalid bluetooth connection
|
||||
* Update Russian translation
|
||||
* Update Ukrainian translation
|
||||
|
||||
## 1.63.4
|
||||
* Update Chinese translation
|
||||
* Improve support for old devices
|
||||
|
||||
## 1.63.3
|
||||
* Fix SCAN Intent
|
||||
|
||||
## 1.63.2
|
||||
* Add DX Film Edge barcode format
|
||||
* Update Turkish translation
|
||||
* Update Ukrainian translation
|
||||
|
||||
## 1.63.1
|
||||
* Include scan label in exports/shares
|
||||
* Update Russian translation
|
||||
|
||||
## 1.63.0
|
||||
* Edit scan label in decoding screen
|
||||
* Add a button to add generated codes to the history
|
||||
* Improve scanning from images
|
||||
* Improve asking for camera permission
|
||||
* Fix margin for recreated barcodes
|
||||
* Limit duration of error beep to 1s
|
||||
* Update Ukrainian translation
|
||||
* Update Spanish translation
|
||||
|
||||
## 1.62.3
|
||||
* Extend selection with single taps in history listing
|
||||
* Update Brazilian Portuguese translation
|
||||
|
||||
## 1.62.2
|
||||
* Include rMQR Code for updates
|
||||
|
||||
## 1.62.1
|
||||
* Make rMQR Code selectable
|
||||
|
||||
## 1.62.0
|
||||
* Add support for rMQR (rectangular Micro QR) Code
|
||||
* Share history as a file
|
||||
* Only share selected scans from history
|
||||
|
||||
## 1.61.2
|
||||
* Fix quick settings tile for Android 14
|
||||
* Fix Xiaomi MIUI lockscreen barcode shortcut
|
||||
|
||||
@@ -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
|
||||
|
||||
+31
-14
@@ -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 123
|
||||
versionName '1.61.2'
|
||||
defaultConfig {
|
||||
minSdk 9
|
||||
targetSdk sdk_version
|
||||
|
||||
versionCode 158
|
||||
versionName '1.68.4'
|
||||
}
|
||||
|
||||
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
|
||||
@@ -52,18 +56,31 @@ android {
|
||||
enableSplit = false
|
||||
}
|
||||
}
|
||||
|
||||
// Required since Gradle 8.
|
||||
compileOptions {
|
||||
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 "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.10.2"
|
||||
|
||||
// Support
|
||||
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'
|
||||
|
||||
// External
|
||||
implementation 'com.github.markusfisch:CameraView:1.10.0'
|
||||
implementation 'com.github.markusfisch:ScalingImageView:1.4.1'
|
||||
implementation 'com.github.markusfisch:zxing-cpp:v2.1.0.2'
|
||||
implementation 'com.github.markusfisch:zxing-cpp:v3.0.2.0'
|
||||
|
||||
// Testing
|
||||
testImplementation 'junit:junit:4.13.2'
|
||||
testImplementation 'org.robolectric:robolectric:4.16.1'
|
||||
}
|
||||
|
||||
Vendored
-2
@@ -3,5 +3,3 @@
|
||||
-dontwarn android.support.v8.**
|
||||
|
||||
-keep class android.support.v7.widget.SearchView { *; }
|
||||
|
||||
-keep class de.markusfisch.android.zxingcpp.** { *; }
|
||||
|
||||
@@ -19,7 +19,8 @@
|
||||
<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-feature android:name="android.hardware.camera.autofocus"
|
||||
android:required="false" />
|
||||
<uses-sdk tools:overrideLibrary="android.support.v14.preference"/>
|
||||
<queries>
|
||||
<intent>
|
||||
@@ -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,14 +109,34 @@
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".activity.MainActivity"
|
||||
android:name=".activity.WelcomeActivity"
|
||||
android:exported="true"/>
|
||||
<activity
|
||||
android:name=".activity.MainActivity"
|
||||
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>
|
||||
<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>
|
||||
|
||||
+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
|
||||
}
|
||||
|
||||
+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())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+3
-2
@@ -9,7 +9,8 @@ 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
|
||||
@@ -87,6 +88,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
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+44
-32
@@ -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.provider.Settings
|
||||
import android.support.annotation.RequiresApi
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import java.util.*
|
||||
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,17 +100,17 @@ 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) {
|
||||
@@ -201,7 +198,7 @@ object WifiConnector {
|
||||
})
|
||||
}
|
||||
}
|
||||
} catch (e: IllegalArgumentException) {
|
||||
} catch (_: IllegalArgumentException) {
|
||||
// Don't accept passphrases that aren't ASCII encodeable.
|
||||
return null
|
||||
}
|
||||
@@ -313,6 +310,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 +378,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.
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package de.markusfisch.android.binaryeye.activity
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.app.AlertDialog
|
||||
import android.content.Context
|
||||
@@ -9,42 +10,54 @@ import android.graphics.Matrix
|
||||
import android.graphics.Rect
|
||||
import android.hardware.Camera
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.support.design.widget.FloatingActionButton
|
||||
import android.support.v7.app.AppCompatActivity
|
||||
import android.support.v7.widget.Toolbar
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.widget.EditText
|
||||
import android.widget.SeekBar
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.adapter.prettifyFormatName
|
||||
import de.markusfisch.android.binaryeye.app.*
|
||||
import de.markusfisch.android.binaryeye.app.PERMISSION_CAMERA
|
||||
import de.markusfisch.android.binaryeye.app.applyLocale
|
||||
import de.markusfisch.android.binaryeye.app.db
|
||||
import de.markusfisch.android.binaryeye.app.hasBluetoothPermission
|
||||
import de.markusfisch.android.binaryeye.app.hasCameraPermission
|
||||
import de.markusfisch.android.binaryeye.app.prefs
|
||||
import de.markusfisch.android.binaryeye.automation.runAutomatedActions
|
||||
import de.markusfisch.android.binaryeye.bluetooth.sendBluetoothAsync
|
||||
import de.markusfisch.android.binaryeye.content.copyToClipboard
|
||||
import de.markusfisch.android.binaryeye.content.execShareIntent
|
||||
import de.markusfisch.android.binaryeye.content.openUrl
|
||||
import de.markusfisch.android.binaryeye.content.toHexString
|
||||
import de.markusfisch.android.binaryeye.database.toScan
|
||||
import de.markusfisch.android.binaryeye.graphics.FrameMetrics
|
||||
import de.markusfisch.android.binaryeye.graphics.mapPosition
|
||||
import de.markusfisch.android.binaryeye.graphics.setFrameRoi
|
||||
import de.markusfisch.android.binaryeye.graphics.setFrameToView
|
||||
import de.markusfisch.android.binaryeye.media.releaseToneGenerators
|
||||
import de.markusfisch.android.binaryeye.net.isScanDeeplink
|
||||
import de.markusfisch.android.binaryeye.net.sendAsync
|
||||
import de.markusfisch.android.binaryeye.net.urlEncode
|
||||
import de.markusfisch.android.binaryeye.preference.Preferences
|
||||
import de.markusfisch.android.binaryeye.view.errorFeedback
|
||||
import de.markusfisch.android.binaryeye.view.initSystemBars
|
||||
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.DetectorView
|
||||
import de.markusfisch.android.binaryeye.widget.toast
|
||||
import de.markusfisch.android.cameraview.widget.CameraView
|
||||
import de.markusfisch.android.zxingcpp.ZxingCpp
|
||||
import de.markusfisch.android.zxingcpp.ZxingCpp.BarcodeFormat
|
||||
import de.markusfisch.android.zxingcpp.ZxingCpp.Binarizer
|
||||
import de.markusfisch.android.zxingcpp.ZxingCpp.DecodeHints
|
||||
import de.markusfisch.android.zxingcpp.ZxingCpp.ReaderOptions
|
||||
import de.markusfisch.android.zxingcpp.ZxingCpp.Result
|
||||
import de.markusfisch.android.zxingcpp.ZxingCpp.TextMode
|
||||
import java.io.FileInputStream
|
||||
import java.util.Scanner
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
import kotlin.math.roundToInt
|
||||
@@ -58,7 +71,9 @@ class CameraActivity : AppCompatActivity() {
|
||||
private lateinit var zoomBar: SeekBar
|
||||
private lateinit var flashFab: FloatingActionButton
|
||||
|
||||
private var formatsToRead = setOf<String>()
|
||||
private var currentProfile = prefs.profile
|
||||
private var shouldStoreSettings = true
|
||||
private var formatsToRead = setOf<BarcodeFormat>()
|
||||
private var frameMetrics = FrameMetrics()
|
||||
private var decoding = true
|
||||
private var returnResult = false
|
||||
@@ -67,8 +82,10 @@ class CameraActivity : AppCompatActivity() {
|
||||
private var frontFacing = false
|
||||
private var bulkMode = prefs.bulkMode
|
||||
private var restrictFormat: String? = null
|
||||
private var searchTerm: Regex? = null
|
||||
private var ignoreNext: String? = null
|
||||
private var fallbackBuffer: IntArray? = null
|
||||
private var requestCameraPermission = true
|
||||
|
||||
override fun onRequestPermissionsResult(
|
||||
requestCode: Int,
|
||||
@@ -91,7 +108,7 @@ class CameraActivity : AppCompatActivity() {
|
||||
) {
|
||||
when (requestCode) {
|
||||
PICK_FILE_RESULT_CODE -> {
|
||||
if (resultCode == Activity.RESULT_OK && resultData != null) {
|
||||
if (resultCode == RESULT_OK && resultData != null) {
|
||||
val pick = Intent(this, PickActivity::class.java)
|
||||
pick.action = Intent.ACTION_VIEW
|
||||
pick.setDataAndType(resultData.data, "image/*")
|
||||
@@ -114,8 +131,7 @@ class CameraActivity : AppCompatActivity() {
|
||||
// custom locale.
|
||||
setTitle(R.string.scan_code)
|
||||
|
||||
initSystemBars(this)
|
||||
setSupportActionBar(findViewById(R.id.toolbar) as Toolbar)
|
||||
initBars()
|
||||
|
||||
cameraView = findViewById(R.id.camera_view) as CameraView
|
||||
detectorView = findViewById(R.id.detector_view) as DetectorView
|
||||
@@ -127,7 +143,7 @@ class CameraActivity : AppCompatActivity() {
|
||||
initDetectorView()
|
||||
|
||||
if (intent?.action == Intent.ACTION_SEND &&
|
||||
intent.type == "text/plain"
|
||||
intent.type?.startsWith("text/") == true
|
||||
) {
|
||||
handleSendText(intent)
|
||||
}
|
||||
@@ -136,37 +152,89 @@ class CameraActivity : AppCompatActivity() {
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
fallbackBuffer = null
|
||||
saveZoom()
|
||||
detectorView.saveCropHandlePos()
|
||||
releaseToneGenerators()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
if (firstStart()) {
|
||||
startActivity(Intent(this, WelcomeActivity::class.java))
|
||||
return
|
||||
}
|
||||
loadPreferences()
|
||||
if (refreshIfProfileChanged()) {
|
||||
return
|
||||
}
|
||||
System.gc()
|
||||
updateHints()
|
||||
setReturnTarget(intent)
|
||||
// Avoid asking multiple times when the user has denied access
|
||||
// for this session. Otherwise ActivityCompat.requestPermissions()
|
||||
// will trigger onResume() again and again.
|
||||
if (hasCameraPermission(requestCameraPermission)) {
|
||||
openCamera()
|
||||
}
|
||||
requestCameraPermission = false
|
||||
}
|
||||
|
||||
private fun Context.firstStart(): Boolean {
|
||||
val welcomeShownName = "welcome_shown"
|
||||
if (prefs.defaultPreferences.getBoolean(welcomeShownName, false)) {
|
||||
return false
|
||||
}
|
||||
prefs.defaultPreferences.edit().putBoolean(welcomeShownName, true).apply()
|
||||
val packageInfo = packageManager.getPackageInfo(packageName, 0)
|
||||
val installedSince = System.currentTimeMillis() -
|
||||
packageInfo.firstInstallTime
|
||||
return installedSince < 86400000
|
||||
}
|
||||
|
||||
private fun refreshIfProfileChanged(): Boolean {
|
||||
if (currentProfile == prefs.profile) {
|
||||
return false
|
||||
}
|
||||
shouldStoreSettings = false
|
||||
zoomBar.max = 0 // Reset zoom when there are no preferences yet.
|
||||
currentProfile = prefs.profile
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
|
||||
recreate()
|
||||
} else {
|
||||
finish()
|
||||
startActivity(Intent(this, CameraActivity::class.java))
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private fun loadPreferences() {
|
||||
detectorView.updateCropHandlePos()
|
||||
updateHintsAndTitle()
|
||||
if (prefs.bulkMode && bulkMode != prefs.bulkMode) {
|
||||
bulkMode = prefs.bulkMode
|
||||
invalidateOptionsMenu()
|
||||
ignoreNext = null
|
||||
}
|
||||
setReturnTarget(intent)
|
||||
if (hasCameraPermission()) {
|
||||
openCamera()
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateHints() {
|
||||
private fun updateHintsAndTitle() {
|
||||
val restriction = restrictFormat
|
||||
formatsToRead = if (restriction != null) {
|
||||
title = getString(
|
||||
R.string.scan_format,
|
||||
prettifyFormatName(restriction)
|
||||
)
|
||||
setOf(restriction)
|
||||
} else {
|
||||
setTitle(R.string.scan_code)
|
||||
prefs.barcodeFormats
|
||||
}.toFormatSet()
|
||||
updateTitle()
|
||||
}
|
||||
|
||||
private fun updateTitle() {
|
||||
if (searchTerm != null || restrictFormat != null) {
|
||||
title = getString(
|
||||
R.string.scan_for,
|
||||
listOfNotNull(
|
||||
searchTerm?.let { "\"$it\"" },
|
||||
restrictFormat?.prettifyFormatName(),
|
||||
).joinToString(" ")
|
||||
)
|
||||
} else {
|
||||
setTitle(R.string.scan_code)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -176,7 +244,7 @@ class CameraActivity : AppCompatActivity() {
|
||||
returnResult = true
|
||||
}
|
||||
|
||||
intent?.dataString?.isReturnUrl() == true -> {
|
||||
intent?.dataString?.let(::isScanDeeplink) == true -> {
|
||||
finishAfterShowingResult = true
|
||||
returnUrlTemplate = intent.data?.getQueryParameter("ret")
|
||||
}
|
||||
@@ -199,12 +267,21 @@ class CameraActivity : AppCompatActivity() {
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
closeCamera()
|
||||
if (shouldStoreSettings) {
|
||||
storeSettings()
|
||||
}
|
||||
shouldStoreSettings = true
|
||||
}
|
||||
|
||||
private fun closeCamera() {
|
||||
cameraView.close()
|
||||
}
|
||||
|
||||
private fun storeSettings() {
|
||||
storeZoomBarSettings()
|
||||
detectorView.storeCropHandlePos()
|
||||
}
|
||||
|
||||
override fun onRestoreInstanceState(savedState: Bundle) {
|
||||
super.onRestoreInstanceState(savedState)
|
||||
zoomBar.max = savedState.getInt(ZOOM_MAX)
|
||||
@@ -212,6 +289,7 @@ class CameraActivity : AppCompatActivity() {
|
||||
frontFacing = savedState.getBoolean(FRONT_FACING)
|
||||
bulkMode = savedState.getBoolean(BULK_MODE)
|
||||
restrictFormat = savedState.getString(RESTRICT_FORMAT)
|
||||
searchTerm = savedState.getString(SEARCH_TERM)?.toRegex()
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
@@ -220,6 +298,7 @@ class CameraActivity : AppCompatActivity() {
|
||||
outState.putBoolean(FRONT_FACING, frontFacing)
|
||||
outState.putBoolean(BULK_MODE, bulkMode)
|
||||
outState.putString(RESTRICT_FORMAT, restrictFormat)
|
||||
outState.putString(SEARCH_TERM, searchTerm?.toString())
|
||||
super.onSaveInstanceState(outState)
|
||||
}
|
||||
|
||||
@@ -235,6 +314,10 @@ class CameraActivity : AppCompatActivity() {
|
||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||
menuInflater.inflate(R.menu.activity_camera, menu)
|
||||
menu.findItem(R.id.bulk_mode).isChecked = bulkMode
|
||||
menu.findItem(R.id.profile).title = getString(
|
||||
R.string.current_profile,
|
||||
prefs.profile ?: getString(R.string.profile_default)
|
||||
)
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -280,6 +363,11 @@ class CameraActivity : AppCompatActivity() {
|
||||
true
|
||||
}
|
||||
|
||||
R.id.find_code -> {
|
||||
askForCode()
|
||||
true
|
||||
}
|
||||
|
||||
R.id.preferences -> {
|
||||
startActivity(MainActivity.getPreferencesIntent(this))
|
||||
true
|
||||
@@ -290,6 +378,11 @@ class CameraActivity : AppCompatActivity() {
|
||||
true
|
||||
}
|
||||
|
||||
R.id.profile -> {
|
||||
pickProfile()
|
||||
true
|
||||
}
|
||||
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
}
|
||||
@@ -319,12 +412,38 @@ class CameraActivity : AppCompatActivity() {
|
||||
setTitle(R.string.restrict_format)
|
||||
setItems(names.toTypedArray()) { _, which ->
|
||||
restrictFormat = formats[which]
|
||||
updateHints()
|
||||
updateHintsAndTitle()
|
||||
}
|
||||
show()
|
||||
}
|
||||
}
|
||||
|
||||
// Dialogs do not have a parent view.
|
||||
@SuppressLint("InflateParams")
|
||||
private fun askForCode() {
|
||||
val view = layoutInflater.inflate(R.layout.dialog_find_code, null)
|
||||
val editText = view.findViewById<EditText>(R.id.term)
|
||||
searchTerm?.let {
|
||||
editText.setText(it.toString())
|
||||
}
|
||||
AlertDialog.Builder(this)
|
||||
.setView(view)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
val term = editText.text.toString().trim()
|
||||
if (!term.isEmpty()) {
|
||||
searchTerm = term.toRegex()
|
||||
} else {
|
||||
searchTerm = null
|
||||
}
|
||||
ignoreNext = null
|
||||
updateTitle()
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||
// do not change anything on cancel
|
||||
}
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun openReadme() {
|
||||
val intent = Intent(
|
||||
Intent.ACTION_VIEW,
|
||||
@@ -333,11 +452,57 @@ class CameraActivity : AppCompatActivity() {
|
||||
execShareIntent(intent)
|
||||
}
|
||||
|
||||
private fun pickProfile() {
|
||||
val profiles = arrayOf(
|
||||
getString(R.string.profile_default),
|
||||
) + prefs.profiles
|
||||
AlertDialog.Builder(this)
|
||||
.setTitle(R.string.profile)
|
||||
.setItems(profiles) { _, which ->
|
||||
val newProfile = when (which) {
|
||||
0 -> null
|
||||
else -> profiles[which]
|
||||
}
|
||||
if (currentProfile != newProfile) {
|
||||
storeSettings()
|
||||
prefs.load(this, newProfile)
|
||||
loadPreferences()
|
||||
refreshIfProfileChanged()
|
||||
}
|
||||
}
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun handleSendText(intent: Intent) {
|
||||
val text = intent.getStringExtra(Intent.EXTRA_TEXT)
|
||||
|
||||
if (text?.isEmpty() == false) {
|
||||
startActivity(MainActivity.getEncodeIntent(this, text, true))
|
||||
finish()
|
||||
return
|
||||
}
|
||||
|
||||
// Read text from file.
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
|
||||
@Suppress("DEPRECATION")
|
||||
intent.getParcelableExtra(Intent.EXTRA_STREAM) as Uri?
|
||||
} else {
|
||||
intent.getParcelableExtra(Intent.EXTRA_STREAM, Uri::class.java)
|
||||
}?.let { textUri ->
|
||||
val file = contentResolver.openFileDescriptor(textUri, "r")
|
||||
if (file != null) {
|
||||
val fs = FileInputStream(file.fileDescriptor)
|
||||
val scn = Scanner(fs).useDelimiter("\\A")
|
||||
if (scn.hasNext()) {
|
||||
startActivity(
|
||||
MainActivity.getEncodeIntent(
|
||||
this, scn.next(), true
|
||||
)
|
||||
)
|
||||
finish()
|
||||
}
|
||||
file.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -364,7 +529,8 @@ class CameraActivity : AppCompatActivity() {
|
||||
v ?: return false
|
||||
val dist = offset - pos
|
||||
val maxValue = zoomBar.max
|
||||
val change = maxValue / v.height.toFloat() * 2f * dist
|
||||
val change = maxValue /
|
||||
v.height.toFloat() * 2f * dist
|
||||
zoomBar.progress = min(
|
||||
maxValue,
|
||||
max(progress + change.roundToInt(), 0)
|
||||
@@ -394,11 +560,12 @@ class CameraActivity : AppCompatActivity() {
|
||||
parameters: Camera.Parameters
|
||||
) {
|
||||
zoomBar.visibility = if (parameters.isZoomSupported) {
|
||||
restoreZoomBarSettings()
|
||||
val max = parameters.maxZoom
|
||||
if (zoomBar.max != max) {
|
||||
zoomBar.max = max
|
||||
zoomBar.progress = max / 10
|
||||
saveZoom()
|
||||
storeZoomBarSettings()
|
||||
}
|
||||
parameters.zoom = zoomBar.progress
|
||||
View.VISIBLE
|
||||
@@ -432,41 +599,58 @@ class CameraActivity : AppCompatActivity() {
|
||||
ignoreNext = null
|
||||
decoding = true
|
||||
// These settings can't change while the camera is open.
|
||||
val decodeHints = DecodeHints(
|
||||
val options = ReaderOptions(
|
||||
tryHarder = prefs.tryHarder,
|
||||
tryRotate = prefs.autoRotate,
|
||||
tryInvert = true,
|
||||
tryDownscale = true,
|
||||
maxNumberOfSymbols = 1
|
||||
maxNumberOfSymbols = 1,
|
||||
textMode = TextMode.PLAIN
|
||||
)
|
||||
var useLocalAverage = false
|
||||
camera.setPreviewCallback { frameData, _ ->
|
||||
if (decoding) {
|
||||
useLocalAverage = useLocalAverage xor true
|
||||
ZxingCpp.readByteArray(
|
||||
frameData,
|
||||
frameMetrics.width,
|
||||
frameRoi.left, frameRoi.top,
|
||||
frameRoi.width(), frameRoi.height(),
|
||||
frameMetrics.orientation,
|
||||
decodeHints.apply {
|
||||
// By default, ZXing uses LOCAL_AVERAGE, but
|
||||
// this does not work well with inverted
|
||||
// barcodes on low-contrast backgrounds.
|
||||
binarizer = if (useLocalAverage) {
|
||||
Binarizer.LOCAL_AVERAGE
|
||||
} else {
|
||||
Binarizer.GLOBAL_HISTOGRAM
|
||||
}
|
||||
formats = formatsToRead.joinToString()
|
||||
}
|
||||
)?.let { results ->
|
||||
val result = results.first()
|
||||
if (result.text != ignoreNext) {
|
||||
postResult(result)
|
||||
decoding = false
|
||||
if (!decoding ||
|
||||
frameData == null ||
|
||||
frameData.isEmpty()
|
||||
) {
|
||||
return@setPreviewCallback
|
||||
}
|
||||
useLocalAverage = useLocalAverage xor true
|
||||
ZxingCpp.readByteArray(
|
||||
frameData,
|
||||
frameMetrics.width,
|
||||
frameRoi.left, frameRoi.top,
|
||||
frameRoi.width(), frameRoi.height(),
|
||||
frameMetrics.orientation,
|
||||
options.apply {
|
||||
// By default, ZXing uses LOCAL_AVERAGE, but
|
||||
// this does not work well with inverted
|
||||
// barcodes on low-contrast backgrounds.
|
||||
binarizer = if (useLocalAverage) {
|
||||
Binarizer.LOCAL_AVERAGE
|
||||
} else {
|
||||
Binarizer.GLOBAL_HISTOGRAM
|
||||
}
|
||||
formats = formatsToRead
|
||||
}
|
||||
)?.let { results ->
|
||||
val result = results.first()
|
||||
val text = result.text
|
||||
if (text == ignoreNext) {
|
||||
return@let
|
||||
}
|
||||
val term = searchTerm
|
||||
if (term != null &&
|
||||
!text.matches(term) &&
|
||||
!text.contains(term)
|
||||
) {
|
||||
ignoreNext = text
|
||||
errorFeedback()
|
||||
toast(R.string.does_not_match_search_term)
|
||||
return@let
|
||||
}
|
||||
postResult(result)
|
||||
decoding = false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -495,7 +679,6 @@ class CameraActivity : AppCompatActivity() {
|
||||
|
||||
override fun onStopTrackingTouch(seekBar: SeekBar) {}
|
||||
})
|
||||
restoreZoom()
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
@@ -504,19 +687,19 @@ class CameraActivity : AppCompatActivity() {
|
||||
val params = parameters
|
||||
params.zoom = zoom
|
||||
parameters = params
|
||||
} catch (e: RuntimeException) {
|
||||
} catch (_: RuntimeException) {
|
||||
// Ignore. There's nothing we can do.
|
||||
}
|
||||
}
|
||||
|
||||
private fun saveZoom() {
|
||||
private fun storeZoomBarSettings() {
|
||||
val editor = prefs.preferences.edit()
|
||||
editor.putInt(ZOOM_MAX, zoomBar.max)
|
||||
editor.putInt(ZOOM_LEVEL, zoomBar.progress)
|
||||
editor.apply()
|
||||
}
|
||||
|
||||
private fun restoreZoom() {
|
||||
private fun restoreZoomBarSettings() {
|
||||
zoomBar.max = prefs.preferences.getInt(ZOOM_MAX, zoomBar.max)
|
||||
zoomBar.progress = prefs.preferences.getInt(
|
||||
ZOOM_LEVEL,
|
||||
@@ -533,12 +716,21 @@ class CameraActivity : AppCompatActivity() {
|
||||
updateFrameRoiAndMappingMatrix()
|
||||
}
|
||||
detectorView.setPaddingFromWindowInsets()
|
||||
detectorView.restoreCropHandlePos()
|
||||
detectorView.cropHandleName = "camera_crop_handle"
|
||||
}
|
||||
|
||||
private fun updateFrameRoiAndMappingMatrix() {
|
||||
val viewRect = cameraView.previewRect
|
||||
val viewRoi = if (detectorView.roi.width() < 1) {
|
||||
if (viewRect.width() < 1 ||
|
||||
viewRect.height() < 1 ||
|
||||
!frameMetrics.isValid()
|
||||
) {
|
||||
return
|
||||
}
|
||||
val viewRoi = if (
|
||||
detectorView.roi.width() < 1 ||
|
||||
detectorView.roi.height() < 1
|
||||
) {
|
||||
viewRect
|
||||
} else {
|
||||
detectorView.roi
|
||||
@@ -596,7 +788,7 @@ class CameraActivity : AppCompatActivity() {
|
||||
}
|
||||
when {
|
||||
returnResult -> {
|
||||
setResult(Activity.RESULT_OK, getReturnIntent(result))
|
||||
setResult(RESULT_OK, getReturnIntent(result))
|
||||
finish()
|
||||
}
|
||||
|
||||
@@ -615,8 +807,12 @@ class CameraActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
if (bulkMode) {
|
||||
if (prefs.ignoreConsecutiveDuplicates) {
|
||||
ignoreNext = result.text
|
||||
when (prefs.ignoreDuplicates()) {
|
||||
Preferences.Companion.IgnoreDuplicates.Consecutive,
|
||||
Preferences.Companion.IgnoreDuplicates.Any ->
|
||||
ignoreNext = result.text
|
||||
|
||||
else -> Unit
|
||||
}
|
||||
if (prefs.showToastInBulkMode) {
|
||||
toast(result.text)
|
||||
@@ -635,9 +831,14 @@ class CameraActivity : AppCompatActivity() {
|
||||
private const val FRONT_FACING = "front_facing"
|
||||
private const val BULK_MODE = "bulk_mode"
|
||||
private const val RESTRICT_FORMAT = "restrict_format"
|
||||
private const val SEARCH_TERM = "search_term"
|
||||
}
|
||||
}
|
||||
|
||||
fun Set<String>.toFormatSet(): Set<BarcodeFormat> = map {
|
||||
BarcodeFormat.valueOf(it)
|
||||
}.toSet()
|
||||
|
||||
fun Activity.showResult(
|
||||
result: Result,
|
||||
bulkMode: Boolean = false,
|
||||
@@ -652,7 +853,7 @@ fun Activity.showResult(
|
||||
if (prefs.sendScanActive && prefs.sendScanUrl.isNotEmpty()) {
|
||||
if (prefs.sendScanType == "4") {
|
||||
openUrl(
|
||||
prefs.sendScanUrl + scan.content.urlEncode()
|
||||
prefs.sendScanUrl + scan.text.urlEncode()
|
||||
)
|
||||
return
|
||||
}
|
||||
@@ -663,7 +864,7 @@ fun Activity.showResult(
|
||||
if (code == null || code < 200 || code > 299) {
|
||||
errorFeedback()
|
||||
}
|
||||
if (body != null && body.isNotEmpty()) {
|
||||
if (!body.isNullOrEmpty()) {
|
||||
toast(body)
|
||||
} else if (code == null || code > 299) {
|
||||
toast(R.string.background_request_failed)
|
||||
@@ -694,6 +895,9 @@ fun Activity.showResult(
|
||||
)
|
||||
}
|
||||
}
|
||||
if (runAutomatedActions(scan)) {
|
||||
return
|
||||
}
|
||||
if (!bulkMode) {
|
||||
startActivity(
|
||||
MainActivity.getDecodeIntent(this, scan)
|
||||
@@ -703,7 +907,7 @@ fun Activity.showResult(
|
||||
|
||||
private fun getReturnIntent(result: Result) = Intent().apply {
|
||||
putExtra("SCAN_RESULT", result.text)
|
||||
putExtra("SCAN_RESULT_FORMAT", result.format)
|
||||
putExtra("SCAN_RESULT_FORMAT", result.format.toString())
|
||||
putExtra("SCAN_RESULT_ORIENTATION", result.orientation)
|
||||
putExtra("SCAN_RESULT_ERROR_CORRECTION_LEVEL", result.ecLevel)
|
||||
if (result.rawBytes.isNotEmpty()) {
|
||||
@@ -711,19 +915,11 @@ private fun getReturnIntent(result: Result) = Intent().apply {
|
||||
}
|
||||
}
|
||||
|
||||
private fun String.isReturnUrl() = listOf(
|
||||
"binaryeye://scan",
|
||||
"http://markusfisch.de/BinaryEye",
|
||||
"https://markusfisch.de/BinaryEye"
|
||||
).firstOrNull { startsWith(it) } != null
|
||||
|
||||
private fun completeUrl(urlTemplate: String, result: Result) = Uri.parse(
|
||||
urlTemplate
|
||||
.replace("{RESULT}", result.text.urlEncode())
|
||||
.replace("{RESULT_BYTES}", result.rawBytes.toHexString())
|
||||
.replace(
|
||||
"{FORMAT}", result.format.urlEncode()
|
||||
)
|
||||
.replace("{FORMAT}", result.format.name.urlEncode())
|
||||
// And support {CODE} from the old ZXing app, too.
|
||||
.replace("{CODE}", result.text.urlEncode())
|
||||
)
|
||||
|
||||
@@ -3,11 +3,11 @@ package de.markusfisch.android.binaryeye.activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.Uri
|
||||
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
|
||||
@@ -20,9 +20,9 @@ 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.net.isEncodeDeeplink
|
||||
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
|
||||
|
||||
class MainActivity : AppCompatActivity() {
|
||||
override fun onRequestPermissionsResult(
|
||||
@@ -31,7 +31,8 @@ class MainActivity : AppCompatActivity() {
|
||||
grantResults: IntArray
|
||||
) {
|
||||
when (requestCode) {
|
||||
PERMISSION_LOCATION, PERMISSION_WRITE -> if (grantResults.isNotEmpty() &&
|
||||
PERMISSION_LOCATION, PERMISSION_WRITE -> if (
|
||||
grantResults.isNotEmpty() &&
|
||||
grantResults[0] == PackageManager.PERMISSION_GRANTED
|
||||
) {
|
||||
permissionGrantedCallback?.invoke()
|
||||
@@ -59,10 +60,7 @@ class MainActivity : AppCompatActivity() {
|
||||
super.onCreate(state)
|
||||
setContentView(R.layout.activity_main)
|
||||
|
||||
initSystemBars(this)
|
||||
val toolbar = findViewById(R.id.toolbar) as Toolbar
|
||||
recordToolbarHeight(toolbar)
|
||||
setSupportActionBar(toolbar)
|
||||
initBars()
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
|
||||
supportFragmentManager.addOnBackStackChangedListener {
|
||||
@@ -70,7 +68,12 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
if (state == null) {
|
||||
supportFragmentManager?.setFragment(getFragmentForIntent(intent))
|
||||
val fragment = intent?.getFragmentForIntent()
|
||||
if (fragment == null) {
|
||||
finish()
|
||||
return
|
||||
}
|
||||
supportFragmentManager?.setFragment(fragment)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,21 +83,29 @@ class MainActivity : AppCompatActivity() {
|
||||
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)
|
||||
)
|
||||
private fun Intent.getFragmentForIntent(): Fragment? = when {
|
||||
hasExtra(PREFERENCES) -> PreferencesFragment()
|
||||
hasExtra(HISTORY) -> HistoryFragment()
|
||||
|
||||
intent.hasExtra(DECODED) -> DecodeFragment.newInstance(
|
||||
intent.getParcelableExtra(DECODED)!!
|
||||
dataString?.let(::isEncodeDeeplink) == true -> {
|
||||
val uri = Uri.parse(dataString)
|
||||
EncodeFragment.newInstance(
|
||||
content = uri.getQueryParameter("content"),
|
||||
format = uri.getQueryParameter("format")?.uppercase(),
|
||||
execute = uri.getQueryParameter("execute")
|
||||
.let { it == "" || it.toBoolean() }
|
||||
)
|
||||
|
||||
else -> PreferencesFragment()
|
||||
}
|
||||
|
||||
hasExtra(ENCODE) -> EncodeFragment.newInstance(
|
||||
getStringExtra(ENCODE)
|
||||
)
|
||||
|
||||
hasExtra(DECODED) -> getScanExtra(DECODED)?.let {
|
||||
DecodeFragment.newInstance(it)
|
||||
}
|
||||
|
||||
else -> PreferencesFragment()
|
||||
}
|
||||
|
||||
fun getPreferencesIntent(context: Context): Intent {
|
||||
@@ -117,10 +128,11 @@ class MainActivity : AppCompatActivity() {
|
||||
val intent = Intent(context, MainActivity::class.java)
|
||||
intent.putExtra(ENCODE, text)
|
||||
if (isExternal) {
|
||||
val flagActivityClearTask =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
|
||||
Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||
} else 0
|
||||
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
|
||||
@@ -137,3 +149,23 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
private fun Intent.getScanExtra(name: String): Scan? = if (
|
||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU
|
||||
) {
|
||||
try {
|
||||
getParcelableExtra(name, Scan::class.java)
|
||||
} catch (_: Throwable) {
|
||||
// `getParcelableExtra(name, Class)` can throw on some versions
|
||||
// of Android when the ClassLoader associated with the Bundle's
|
||||
// lazy-decoded value is null in certain scenarios (e.g., after
|
||||
// an activity restart where the system re-delivers the intent).
|
||||
// Simply fallback to the "deprecated" `getParcelableExtra(name)`
|
||||
// which works in this case, because it doesn't verify the
|
||||
// parcelable's class against Scan::class.java.
|
||||
null
|
||||
}
|
||||
} else {
|
||||
null
|
||||
} ?: getParcelableExtra(name)
|
||||
|
||||
@@ -7,10 +7,9 @@ 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 de.markusfisch.android.binaryeye.R
|
||||
@@ -22,8 +21,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
|
||||
@@ -31,7 +29,7 @@ import de.markusfisch.android.binaryeye.widget.DetectorView
|
||||
import de.markusfisch.android.binaryeye.widget.toast
|
||||
import de.markusfisch.android.zxingcpp.ZxingCpp
|
||||
import de.markusfisch.android.zxingcpp.ZxingCpp.Binarizer
|
||||
import de.markusfisch.android.zxingcpp.ZxingCpp.DecodeHints
|
||||
import de.markusfisch.android.zxingcpp.ZxingCpp.ReaderOptions
|
||||
import de.markusfisch.android.zxingcpp.ZxingCpp.Result
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -46,19 +44,18 @@ class PickActivity : AppCompatActivity() {
|
||||
private val matrix = Matrix()
|
||||
private val parentJob = Job()
|
||||
private val scope = CoroutineScope(Dispatchers.IO + parentJob)
|
||||
private val decodeHints = DecodeHints(
|
||||
private val options = ReaderOptions(
|
||||
tryHarder = true,
|
||||
tryRotate = true,
|
||||
tryInvert = true,
|
||||
tryDownscale = true,
|
||||
maxNumberOfSymbols = 1,
|
||||
formats = prefs.barcodeFormats.joinToString()
|
||||
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 +71,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,23 +80,34 @@ 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 {
|
||||
showResult()
|
||||
@@ -125,35 +129,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 +172,10 @@ class PickActivity : AppCompatActivity() {
|
||||
}
|
||||
result = it
|
||||
scanFeedback()
|
||||
detectorView.update(
|
||||
dv.update(
|
||||
matrix.mapPosition(
|
||||
it.position,
|
||||
detectorView.coordinates
|
||||
dv.coordinates
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -179,7 +190,7 @@ class PickActivity : AppCompatActivity() {
|
||||
0, 0,
|
||||
width, height,
|
||||
0,
|
||||
decodeHints.apply {
|
||||
options.apply {
|
||||
binarizer = Binarizer.LOCAL_AVERAGE
|
||||
}
|
||||
) ?: ZxingCpp.readBitmap(
|
||||
@@ -187,14 +198,14 @@ class PickActivity : AppCompatActivity() {
|
||||
0, 0,
|
||||
width, height,
|
||||
0,
|
||||
decodeHints.apply {
|
||||
options.apply {
|
||||
binarizer = Binarizer.GLOBAL_HISTOGRAM
|
||||
}
|
||||
)
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
detectorView.saveCropHandlePos()
|
||||
detectorView?.storeCropHandlePos()
|
||||
parentJob.cancel()
|
||||
releaseToneGenerators()
|
||||
}
|
||||
@@ -216,8 +227,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 +247,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 +282,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,32 @@
|
||||
package de.markusfisch.android.binaryeye.activity
|
||||
|
||||
import android.os.Bundle
|
||||
import android.support.v7.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(R.id.expert).setOnClickListener {
|
||||
finish()
|
||||
}
|
||||
findViewById(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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,7 @@ class ScansAdapter(context: Context, cursor: Cursor) :
|
||||
)
|
||||
}
|
||||
|
||||
fun prettifyFormatName(format: String) = format.replace("_", " ")
|
||||
fun String.prettifyFormatName() = replace("_", " ")
|
||||
|
||||
private fun formatDateTime(rfc: String): String {
|
||||
try {
|
||||
@@ -149,7 +149,7 @@ private fun formatDateTime(rfc: String): String {
|
||||
DateFormat.SHORT
|
||||
).format(it)
|
||||
}
|
||||
} catch (e: ParseException) {
|
||||
} catch (_: ParseException) {
|
||||
// Ignore and fall through.
|
||||
}
|
||||
return rfc
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -10,9 +10,9 @@ import android.support.v4.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)
|
||||
}
|
||||
@@ -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,203 @@
|
||||
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()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
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 = 1,
|
||||
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.CODE_39,
|
||||
BarcodeFormat.CODE_93,
|
||||
BarcodeFormat.CODE_128,
|
||||
BarcodeFormat.DATA_BAR,
|
||||
BarcodeFormat.DATA_BAR_EXPANDED,
|
||||
BarcodeFormat.DX_FILM_EDGE,
|
||||
BarcodeFormat.EAN_8,
|
||||
BarcodeFormat.EAN_13,
|
||||
BarcodeFormat.ITF,
|
||||
BarcodeFormat.UPC_A,
|
||||
BarcodeFormat.UPC_E -> size / 3
|
||||
|
||||
else -> size
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -33,12 +33,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 {
|
||||
@@ -61,12 +69,15 @@ fun Context.shareText(text: String, mimeType: String = "text/plain") {
|
||||
})
|
||||
}
|
||||
|
||||
private const val SHARE_AS_TEXT_FILE_NAME = "barcode_content"
|
||||
fun Context.wipeShareFile() {
|
||||
File(externalCacheDir, SHARE_AS_TEXT_FILE_NAME).delete()
|
||||
private var lastShareFile: File? = null
|
||||
fun wipeLastShareFile() {
|
||||
lastShareFile?.let {
|
||||
it.delete()
|
||||
lastShareFile = null
|
||||
}
|
||||
}
|
||||
|
||||
fun <T> Context.shareAsFile(content: T) {
|
||||
fun <T> Context.shareAsFile(content: T, fileName: String) {
|
||||
val (bytes, mimeType) = when (content) {
|
||||
is ByteArray -> Pair(
|
||||
content,
|
||||
@@ -82,15 +93,17 @@ fun <T> Context.shareAsFile(content: T) {
|
||||
"Unsupported content text for shareAsFile()"
|
||||
)
|
||||
}
|
||||
val file = File(externalCacheDir, SHARE_AS_TEXT_FILE_NAME)
|
||||
wipeLastShareFile()
|
||||
val file = File(externalCacheDir, fileName)
|
||||
try {
|
||||
FileOutputStream(file).use {
|
||||
it.write(bytes)
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
} catch (_: IOException) {
|
||||
toast(R.string.error_saving_file)
|
||||
return
|
||||
}
|
||||
lastShareFile = file
|
||||
shareFile(file, mimeType)
|
||||
}
|
||||
|
||||
|
||||
@@ -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,6 +1,7 @@
|
||||
package de.markusfisch.android.binaryeye.database
|
||||
|
||||
import android.database.Cursor
|
||||
import de.markusfisch.android.zxingcpp.ZxingCpp
|
||||
|
||||
// Overwrite Kotlin's ".use" function for Cursor because Cursor cannot
|
||||
// be cast to Closeable below API level 16. This should be removed when
|
||||
@@ -30,3 +31,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.zxingcpp.ZxingCpp.BarcodeFormat
|
||||
|
||||
class Database {
|
||||
private lateinit var db: SQLiteDatabase
|
||||
@@ -14,12 +16,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)}
|
||||
@@ -27,12 +29,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,
|
||||
@@ -54,7 +56,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 {
|
||||
""
|
||||
@@ -69,12 +71,12 @@ class Database {
|
||||
null
|
||||
}
|
||||
|
||||
fun getScan(id: Long): Scan? = 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,
|
||||
@@ -87,16 +89,49 @@ class Database {
|
||||
$SCANS_GTIN_PRICE,
|
||||
$SCANS_GTIN_ISSUE_NUMBER
|
||||
FROM $SCANS
|
||||
WHERE $SCANS_ID IN (${ids.joinToString(",")})
|
||||
ORDER BY $SCANS_DATETIME DESC
|
||||
""".trimMargin(), null
|
||||
)
|
||||
|
||||
fun getScan(id: Long): Scan? = db.rawQuery(
|
||||
"""SELECT
|
||||
$SCANS_ID,
|
||||
$SCANS_DATETIME,
|
||||
$SCANS_NAME,
|
||||
$SCANS_TEXT,
|
||||
$SCANS_RAW,
|
||||
$SCANS_FORMAT,
|
||||
$SCANS_ERROR_CORRECTION_LEVEL,
|
||||
$SCANS_VERSION,
|
||||
$SCANS_DATA_MASK,
|
||||
$SCANS_SYMBOL_WIDTH,
|
||||
$SCANS_SYMBOL_HEIGHT,
|
||||
$SCANS_SYMBOL_DATA,
|
||||
$SCANS_SEQUENCE_SIZE,
|
||||
$SCANS_SEQUENCE_INDEX,
|
||||
$SCANS_SEQUENCE_ID,
|
||||
$SCANS_GTIN_COUNTRY,
|
||||
$SCANS_GTIN_ADD_ON,
|
||||
$SCANS_GTIN_PRICE,
|
||||
$SCANS_GTIN_ISSUE_NUMBER
|
||||
FROM $SCANS
|
||||
WHERE $SCANS_ID = ?
|
||||
""".trimMargin(), arrayOf("$id")
|
||||
)?.use {
|
||||
).use {
|
||||
if (it.moveToFirst()) {
|
||||
Scan(
|
||||
it.getString(SCANS_CONTENT),
|
||||
it.getString(SCANS_TEXT),
|
||||
it.getBlob(SCANS_RAW),
|
||||
it.getString(SCANS_FORMAT),
|
||||
BarcodeFormat.valueOf(it.getString(SCANS_FORMAT)),
|
||||
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),
|
||||
@@ -105,74 +140,113 @@ 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)
|
||||
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,
|
||||
raw: ByteArray?,
|
||||
format: String
|
||||
format: BarcodeFormat
|
||||
): 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
|
||||
it.getString(SCANS_FORMAT) == format.name
|
||||
) {
|
||||
it.getLong(SCANS_ID)
|
||||
} 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"))
|
||||
@@ -189,7 +263,7 @@ class Database {
|
||||
}
|
||||
|
||||
private class OpenHelper(context: Context) :
|
||||
SQLiteOpenHelper(context, FILE_NAME, null, 6) {
|
||||
SQLiteOpenHelper(context, FILE_NAME, null, 8) {
|
||||
override fun onCreate(db: SQLiteDatabase) {
|
||||
db.createScans()
|
||||
}
|
||||
@@ -214,6 +288,12 @@ class Database {
|
||||
if (oldVersion < 6) {
|
||||
db.migrateToVersionString()
|
||||
}
|
||||
if (oldVersion < 7) {
|
||||
db.addDataMaskColumn()
|
||||
}
|
||||
if (oldVersion < 8) {
|
||||
db.addSymbolColumns()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -223,12 +303,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"
|
||||
@@ -251,11 +335,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,
|
||||
@@ -283,7 +371,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() {
|
||||
@@ -307,5 +395,16 @@ class Database {
|
||||
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,23 @@
|
||||
package de.markusfisch.android.binaryeye.database
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.os.Build
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
import android.text.format.DateFormat
|
||||
import 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.ContentType
|
||||
import de.markusfisch.android.zxingcpp.ZxingCpp.Format
|
||||
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: String,
|
||||
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,7 +26,8 @@ data class Scan(
|
||||
val price: String? = null,
|
||||
val issueNumber: String? = null,
|
||||
val dateTime: String = getDateTime(),
|
||||
var id: Long = 0L
|
||||
var id: Long = 0L,
|
||||
val label: String? = null
|
||||
) : Parcelable {
|
||||
// Needs to be overwritten manually, as ByteArray is an array and
|
||||
// this isn't handled well by Kotlin.
|
||||
@@ -40,12 +43,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 +65,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()
|
||||
@@ -76,11 +82,13 @@ data class Scan(
|
||||
}
|
||||
|
||||
private constructor(parcel: Parcel) : this(
|
||||
content = parcel.readString() ?: "",
|
||||
text = parcel.readString() ?: "",
|
||||
raw = parcel.readSizedByteArray(),
|
||||
format = parcel.readString() ?: "",
|
||||
format = BarcodeFormat.valueOf(parcel.readString() ?: ""),
|
||||
errorCorrectionLevel = parcel.readString(),
|
||||
version = parcel.readString(),
|
||||
dataMask = parcel.readInt(),
|
||||
symbol = parcel.readBitMatrix(),
|
||||
sequenceSize = parcel.readInt(),
|
||||
sequenceIndex = parcel.readInt(),
|
||||
sequenceId = parcel.readString() ?: "",
|
||||
@@ -89,16 +97,19 @@ data class Scan(
|
||||
price = parcel.readString(),
|
||||
issueNumber = parcel.readString(),
|
||||
dateTime = parcel.readString() ?: "",
|
||||
id = parcel.readLong()
|
||||
id = parcel.readLong(),
|
||||
label = parcel.readString()
|
||||
)
|
||||
|
||||
override fun writeToParcel(parcel: Parcel, flags: Int) {
|
||||
parcel.apply {
|
||||
writeString(content)
|
||||
writeString(text)
|
||||
writeSizedByteArray(raw)
|
||||
writeString(format)
|
||||
writeString(format.name)
|
||||
writeString(errorCorrectionLevel)
|
||||
writeString(version)
|
||||
writeInt(dataMask)
|
||||
writeBitMatrix(symbol)
|
||||
writeInt(sequenceSize)
|
||||
writeInt(sequenceIndex)
|
||||
writeString(sequenceId)
|
||||
@@ -108,6 +119,7 @@ data class Scan(
|
||||
writeString(issueNumber)
|
||||
writeString(dateTime)
|
||||
writeLong(id)
|
||||
writeString(label)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -129,12 +141,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,37 +159,6 @@ fun Result.toScan(): Scan {
|
||||
)
|
||||
}
|
||||
|
||||
data class Recreation(
|
||||
val format: Format,
|
||||
val ecLevel: Int,
|
||||
val size: Int,
|
||||
val margin: Int
|
||||
) {
|
||||
fun <T> encode(content: T): Bitmap? = try {
|
||||
ZxingCpp.encodeAsBitmap(
|
||||
content, format, size, size, margin, ecLevel
|
||||
)
|
||||
} catch (_: RuntimeException) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun Scan.toRecreation(
|
||||
size: Int = 128,
|
||||
margin: Int = -1
|
||||
) = Recreation(
|
||||
Format.valueOf(format),
|
||||
when (errorCorrectionLevel) {
|
||||
"L" -> 0
|
||||
"M" -> 4
|
||||
"Q" -> 6
|
||||
"H" -> 8
|
||||
else -> -1
|
||||
},
|
||||
size,
|
||||
margin
|
||||
)
|
||||
|
||||
private fun getDateTime(
|
||||
time: Long = System.currentTimeMillis()
|
||||
) = DateFormat.format(
|
||||
@@ -187,7 +170,11 @@ private fun getDateTime(
|
||||
"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
|
||||
@@ -207,3 +194,21 @@ private fun Parcel.readSizedByteArray(): ByteArray? {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun Parcel.writeBitMatrix(bm: BitMatrix?) {
|
||||
writeInt(bm?.width ?: 0)
|
||||
bm?.let {
|
||||
writeInt(bm.height)
|
||||
writeSizedByteArray(bm.data)
|
||||
}
|
||||
}
|
||||
|
||||
private fun Parcel.readBitMatrix(): BitMatrix? {
|
||||
val width = readInt()
|
||||
if (width < 1) {
|
||||
return null
|
||||
}
|
||||
val height = readInt()
|
||||
val data = readSizedByteArray() ?: return null
|
||||
return BitMatrix(width, height, data)
|
||||
}
|
||||
|
||||
+280
@@ -0,0 +1,280 @@
|
||||
package de.markusfisch.android.binaryeye.fragment
|
||||
|
||||
import android.app.AlertDialog
|
||||
import android.app.Dialog
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.support.v4.app.Fragment
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.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 AutomatedActionsFragment : Fragment() {
|
||||
private val actions = ArrayList<AutomatedAction>()
|
||||
|
||||
private lateinit var adapter: ActionsAdapter
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
state: Bundle?
|
||||
): View? {
|
||||
val ac = activity ?: return null
|
||||
ac.setTitle(R.string.automated_actions)
|
||||
|
||||
val view = inflater.inflate(
|
||||
R.layout.fragment_automated_actions,
|
||||
container,
|
||||
false
|
||||
)
|
||||
|
||||
actions.clear()
|
||||
actions.addAll(prefs.automatedActions)
|
||||
|
||||
val listView = view.findViewById<ListView>(R.id.actions)
|
||||
listView.emptyView = view.findViewById(R.id.no_actions)
|
||||
adapter = ActionsAdapter(ac, 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()
|
||||
|
||||
return view
|
||||
}
|
||||
|
||||
private fun editAction(position: Int?) {
|
||||
val ac = activity ?: return
|
||||
val action = position?.let { actions[it] }
|
||||
val view = ac.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(ac)
|
||||
.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) {
|
||||
val ac = activity ?: return
|
||||
AlertDialog.Builder(ac)
|
||||
.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,
|
||||
)
|
||||
}
|
||||
}
|
||||
+363
-124
@@ -4,44 +4,71 @@ import android.annotation.SuppressLint
|
||||
import android.app.AlertDialog
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Matrix
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.support.v4.app.Fragment
|
||||
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.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.graphics.COLOR_BLACK
|
||||
import de.markusfisch.android.binaryeye.graphics.COLOR_WHITE
|
||||
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.Format
|
||||
import kotlinx.coroutines.*
|
||||
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.*
|
||||
import java.util.Locale
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
|
||||
class BarcodeFragment : Fragment() {
|
||||
private enum class FileType {
|
||||
PNG, SVG, TXT
|
||||
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 lateinit var addToHistoryItem: MenuItem
|
||||
private lateinit var brightenScreenItem: MenuItem
|
||||
|
||||
private var currentBrightness = -1f
|
||||
|
||||
override fun onCreate(state: Bundle?) {
|
||||
super.onCreate(state)
|
||||
@@ -62,6 +89,9 @@ class BarcodeFragment : Fragment() {
|
||||
false
|
||||
)
|
||||
|
||||
// Make `imageView` available for `onPause()`.
|
||||
imageView = view.findViewById(R.id.barcode)
|
||||
|
||||
val bitmap: Bitmap
|
||||
try {
|
||||
barcode = arguments?.toBarcode() ?: throw IllegalArgumentException(
|
||||
@@ -81,15 +111,20 @@ class BarcodeFragment : Fragment() {
|
||||
return null
|
||||
}
|
||||
|
||||
val imageView = view.findViewById<ConfinedScalingImageView>(
|
||||
R.id.barcode
|
||||
)
|
||||
imageView.setImageBitmap(bitmap)
|
||||
imageView.post {
|
||||
// Make sure to invoke this after ScalingImageView.onLayout().
|
||||
imageView.runAfterLayout = {
|
||||
val scale = prefs.previewScale
|
||||
if (scale > 0) {
|
||||
imageView.setScale(
|
||||
scale,
|
||||
bitmap.width / 2f,
|
||||
bitmap.height / 2f
|
||||
)
|
||||
}
|
||||
|
||||
imageView.minWidth = min(
|
||||
imageView.minWidth / 2f,
|
||||
barcode.size.toFloat()
|
||||
max(bitmap.width, bitmap.height).toFloat()
|
||||
)
|
||||
}
|
||||
|
||||
@@ -101,28 +136,67 @@ class BarcodeFragment : Fragment() {
|
||||
}
|
||||
}
|
||||
|
||||
(view.findViewById(R.id.inset_layout) as View).setPaddingFromWindowInsets()
|
||||
imageView.doOnApplyWindowInsets { v, insets ->
|
||||
(v as ConfinedScalingImageView).insets.set(insets)
|
||||
view.findViewById<View>(R.id.inset_layout).setPaddingFromWindowInsets()
|
||||
imageView.doOnApplyWindowInsets { _, insets ->
|
||||
imageView.insets.set(insets)
|
||||
}
|
||||
|
||||
return view
|
||||
}
|
||||
|
||||
private fun Bundle.toBarcode() = Barcode(
|
||||
getString(CONTENT_TEXT) ?: getByteArray(CONTENT_RAW) ?: throw IllegalArgumentException(
|
||||
"content cannot be null"
|
||||
),
|
||||
Format.valueOf(
|
||||
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"
|
||||
)
|
||||
),
|
||||
getInt(SIZE),
|
||||
getInt(MARGIN),
|
||||
getInt(EC_LEVEL),
|
||||
Colors.values()[getInt(COLORS)]
|
||||
)
|
||||
)
|
||||
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.
|
||||
view?.post {
|
||||
brightenScreen()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
if (currentBrightness > -1f) {
|
||||
restoreScreenBrightness()
|
||||
}
|
||||
if (imageView.isInBounds()) {
|
||||
prefs.previewScale = imageView.getScale()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
@@ -131,21 +205,53 @@ class BarcodeFragment : Fragment() {
|
||||
|
||||
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
|
||||
context.toast(R.string.added_to_history)
|
||||
true
|
||||
}
|
||||
|
||||
R.id.copy_to_clipboard -> {
|
||||
context.apply {
|
||||
copyToClipboard(barcode.text())
|
||||
toast(R.string.copied_to_clipboard)
|
||||
}
|
||||
copyToClipboard(barcode.text())
|
||||
true
|
||||
}
|
||||
|
||||
R.id.copy_as_deeplink -> {
|
||||
copyToClipboard(deeplinkToCopy())
|
||||
true
|
||||
}
|
||||
|
||||
R.id.export_to_file -> {
|
||||
context.pickFileType(R.string.export_as) {
|
||||
askForFileNameAndSave(it)
|
||||
context.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
|
||||
}
|
||||
@@ -154,11 +260,23 @@ class BarcodeFragment : Fragment() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun copyToClipboard(text: String, isSensitive: Boolean = false) {
|
||||
activity?.apply {
|
||||
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.values()
|
||||
val fileTypes = FileType.entries.toTypedArray()
|
||||
AlertDialog.Builder(this)
|
||||
.setTitle(title)
|
||||
.setItems(fileTypes.map { it.name }.toTypedArray()) { _, which ->
|
||||
@@ -169,11 +287,13 @@ class BarcodeFragment : Fragment() {
|
||||
|
||||
// Dialogs do not have a parent view.
|
||||
@SuppressLint("InflateParams")
|
||||
private fun askForFileNameAndSave(fileType: FileType) {
|
||||
private fun askForFileNameAndSave(fileType: FileType, bitmapSize: Int) {
|
||||
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) }
|
||||
!ac.hasWritePermission {
|
||||
askForFileNameAndSave(fileType, bitmapSize)
|
||||
}
|
||||
) {
|
||||
return
|
||||
}
|
||||
@@ -191,21 +311,28 @@ class BarcodeFragment : Fragment() {
|
||||
addSuffixIfNotGiven(fileName, ".png"),
|
||||
MIME_PNG
|
||||
) {
|
||||
barcode.bitmap().saveAsPng(it)
|
||||
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
|
||||
) { outputStream ->
|
||||
outputStream.write(barcode.svg().toByteArray())
|
||||
) {
|
||||
it.write(barcode.svg().toByteArray())
|
||||
}
|
||||
|
||||
FileType.TXT -> saveAs(
|
||||
addSuffixIfNotGiven(fileName, ".txt"),
|
||||
MIME_TXT
|
||||
) { outputStream ->
|
||||
outputStream.write(barcode.text().toByteArray())
|
||||
) {
|
||||
it.write(barcode.text().toByteArray())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -214,6 +341,52 @@ class BarcodeFragment : Fragment() {
|
||||
.show()
|
||||
}
|
||||
|
||||
// Dialogs do not have a parent view.
|
||||
@SuppressLint("InflateParams")
|
||||
private fun askForSize(write: (size: Int) -> Unit) {
|
||||
val ac = activity ?: return
|
||||
val view = ac.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(ac)
|
||||
.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,
|
||||
@@ -221,7 +394,9 @@ class BarcodeFragment : Fragment() {
|
||||
) {
|
||||
val ac = activity ?: return
|
||||
scope.launch(Dispatchers.IO) {
|
||||
val message = ac.writeExternalFile(fileName, mimeType, write).toSaveResult()
|
||||
val message = ac.writeExternalFile(
|
||||
fileName, mimeType, write
|
||||
).toSaveResult()
|
||||
withContext(Dispatchers.Main) {
|
||||
ac.toast(message)
|
||||
}
|
||||
@@ -230,29 +405,42 @@ class BarcodeFragment : Fragment() {
|
||||
|
||||
private fun Context.shareAs(fileType: FileType) {
|
||||
when (fileType) {
|
||||
FileType.PNG -> share(barcode.bitmap())
|
||||
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) {
|
||||
private fun Context.share(bitmap: Bitmap, mimeType: String, ext: String) {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
val file = File(
|
||||
externalCacheDir,
|
||||
"shared_barcode.png"
|
||||
"shared_barcode.$ext"
|
||||
)
|
||||
val success = try {
|
||||
FileOutputStream(file).use {
|
||||
bitmap.saveAsPng(it)
|
||||
when (mimeType) {
|
||||
MIME_PNG -> bitmap.saveAsPng(it)
|
||||
MIME_JPG -> bitmap.saveAsJpg(it)
|
||||
else -> throw IllegalArgumentException(
|
||||
"Invalid mime type"
|
||||
)
|
||||
}
|
||||
}
|
||||
true
|
||||
} catch (e: IOException) {
|
||||
} catch (_: IOException) {
|
||||
false
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
if (success) {
|
||||
shareFile(file, "image/png")
|
||||
shareFile(file, mimeType)
|
||||
} else {
|
||||
toast(R.string.error_saving_file)
|
||||
}
|
||||
@@ -260,34 +448,96 @@ class BarcodeFragment : Fragment() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun brightenScreen() {
|
||||
activity?.let {
|
||||
if (!isAdded) {
|
||||
return
|
||||
}
|
||||
currentBrightness = it.getScreenBrightness()
|
||||
it.setScreenBrightness(1f)
|
||||
brightenScreenItem.isChecked = true
|
||||
}
|
||||
}
|
||||
|
||||
private fun restoreScreenBrightness() {
|
||||
if (currentBrightness > -1f && isAdded) {
|
||||
activity?.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 SIZE = "size"
|
||||
private const val MARGIN = "margin"
|
||||
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 <T> newInstance(
|
||||
content: T,
|
||||
format: Format,
|
||||
size: Int,
|
||||
margin: Int,
|
||||
ecLevel: Int = -1,
|
||||
fun newInstance(
|
||||
barcode: Barcode<*>,
|
||||
colors: Int = 0
|
||||
): Fragment {
|
||||
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)
|
||||
}
|
||||
|
||||
else -> throw IllegalArgumentException("unknown barcode")
|
||||
}
|
||||
args.putContentFormatColors(
|
||||
barcode.content,
|
||||
barcode.format,
|
||||
colors
|
||||
)
|
||||
val fragment = BarcodeFragment()
|
||||
fragment.arguments = args
|
||||
return fragment
|
||||
}
|
||||
|
||||
fun <T> newInstance(
|
||||
content: T,
|
||||
format: BarcodeFormat,
|
||||
ecLevel: Int = -1,
|
||||
margin: Int = -1,
|
||||
colors: Int = 0
|
||||
): Fragment {
|
||||
val args = Bundle()
|
||||
args.putContentFormatColors(content, format, colors)
|
||||
args.putEcLevelMargin(ecLevel, margin)
|
||||
val fragment = BarcodeFragment()
|
||||
fragment.arguments = args
|
||||
return fragment
|
||||
}
|
||||
|
||||
private fun <T> Bundle.putContentFormatColors(
|
||||
content: T,
|
||||
format: BarcodeFormat,
|
||||
colors: Int
|
||||
) {
|
||||
when (content) {
|
||||
is String -> {
|
||||
args.putString(CONTENT_TEXT, content)
|
||||
putString(CONTENT_TEXT, content)
|
||||
}
|
||||
|
||||
is ByteArray -> {
|
||||
args.putByteArray(CONTENT_RAW, content)
|
||||
putByteArray(CONTENT_RAW, content)
|
||||
}
|
||||
|
||||
else -> {
|
||||
@@ -296,82 +546,71 @@ class BarcodeFragment : Fragment() {
|
||||
)
|
||||
}
|
||||
}
|
||||
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
|
||||
putString(FORMAT, format.name)
|
||||
putInt(COLORS, colors)
|
||||
}
|
||||
|
||||
private fun Bundle.putEcLevelMargin(
|
||||
ecLevel: Int,
|
||||
margin: Int
|
||||
) {
|
||||
putInt(EC_LEVEL, ecLevel)
|
||||
putInt(MARGIN, margin)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private data class Barcode<T>(
|
||||
val content: T,
|
||||
val format: Format,
|
||||
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 fun ImageView.setScale(scale: Float, x: Float, y: Float) {
|
||||
imageMatrix = Matrix().apply {
|
||||
setTranslate(-x, -y)
|
||||
postScale(scale, scale)
|
||||
postTranslate(x, y)
|
||||
}
|
||||
}
|
||||
|
||||
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 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 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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,37 +1,74 @@
|
||||
package de.markusfisch.android.binaryeye.fragment
|
||||
|
||||
import android.app.Activity
|
||||
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.support.design.widget.FloatingActionButton
|
||||
import android.support.v4.app.Fragment
|
||||
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.view.*
|
||||
import android.widget.*
|
||||
import android.text.style.TypefaceSpan
|
||||
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.ImageView
|
||||
import android.widget.TableLayout
|
||||
import android.widget.TableRow
|
||||
import android.widget.TextView
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.actions.ActionRegistry
|
||||
import de.markusfisch.android.binaryeye.actions.IAction
|
||||
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.activity.MainActivity
|
||||
import de.markusfisch.android.binaryeye.adapter.prettifyFormatName
|
||||
import de.markusfisch.android.binaryeye.app.*
|
||||
import de.markusfisch.android.binaryeye.app.addFragment
|
||||
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.toHexString
|
||||
import de.markusfisch.android.binaryeye.content.wipeShareFile
|
||||
import de.markusfisch.android.binaryeye.database.Recreation
|
||||
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.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.net.createEncodeDeeplink
|
||||
import de.markusfisch.android.binaryeye.view.hideSoftKeyboard
|
||||
import de.markusfisch.android.binaryeye.view.setPaddingFromWindowInsets
|
||||
import de.markusfisch.android.binaryeye.widget.toast
|
||||
import kotlinx.coroutines.*
|
||||
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 DecodeFragment : Fragment() {
|
||||
@@ -42,11 +79,15 @@ class DecodeFragment : Fragment() {
|
||||
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 scope: CoroutineScope = CoroutineScope(
|
||||
Dispatchers.Main + parentJob
|
||||
)
|
||||
private val dp: Float
|
||||
get() = resources.displayMetrics.density
|
||||
private val content: String
|
||||
@@ -56,8 +97,23 @@ class DecodeFragment : Fragment() {
|
||||
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
|
||||
private var label: String? = null
|
||||
private var recreationSize = 0
|
||||
|
||||
override fun onActivityResult(
|
||||
requestCode: Int,
|
||||
resultCode: Int,
|
||||
resultData: Intent?
|
||||
) {
|
||||
when (requestCode) {
|
||||
OPEN_DOCUMENT -> {
|
||||
if (resultCode == Activity.RESULT_OK) {
|
||||
val uri = resultData?.data ?: return
|
||||
activity?.openPickedFile(uri)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate(state: Bundle?) {
|
||||
super.onCreate(state)
|
||||
@@ -77,21 +133,22 @@ class DecodeFragment : Fragment() {
|
||||
false
|
||||
)
|
||||
|
||||
closeAutomatically = prefs.closeAutomatically &&
|
||||
activity?.intent?.hasExtra(MainActivity.DECODED) == true
|
||||
val justScanned = activity?.intent?.hasExtra(
|
||||
MainActivity.DECODED
|
||||
) == true
|
||||
closeAutomatically = prefs.closeAutomatically && justScanned
|
||||
|
||||
val scan = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
scan = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
arguments?.getParcelable(SCAN, Scan::class.java)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
arguments?.getParcelable(SCAN) as Scan?
|
||||
arguments?.getParcelable(SCAN)
|
||||
} ?: throw IllegalArgumentException("DecodeFragment needs a Scan")
|
||||
|
||||
val originalContent = scan.content
|
||||
isBinary = scan.content.isEmpty()
|
||||
originalBytes = scan.raw ?: originalContent.toByteArray()
|
||||
format = scan.format
|
||||
id = scan.id
|
||||
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)
|
||||
@@ -100,36 +157,55 @@ class DecodeFragment : Fragment() {
|
||||
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)
|
||||
|
||||
initContentAndFab(originalContent)
|
||||
|
||||
if (prefs.showMetaData) {
|
||||
metaView.fillMetaView(scan)
|
||||
} else {
|
||||
metaView.visibility = View.GONE
|
||||
}
|
||||
if (prefs.showRecreation) {
|
||||
recreation = scan.toRecreation(
|
||||
(200f * dp).roundToInt()
|
||||
)
|
||||
|
||||
if (scan.id > 0) {
|
||||
scan.label?.let {
|
||||
labelView.setText(it)
|
||||
label = it
|
||||
}
|
||||
} else {
|
||||
labelView.visibility = View.GONE
|
||||
}
|
||||
|
||||
updateViewsAndFab(originalContent, originalBytes)
|
||||
view.findViewById<View>(R.id.inset_layout).setPaddingFromWindowInsets()
|
||||
view.findViewById<View>(R.id.scroll_view).setPaddingFromWindowInsets()
|
||||
|
||||
(view.findViewById(R.id.inset_layout) as View).setPaddingFromWindowInsets()
|
||||
(view.findViewById(R.id.scroll_view) as View).setPaddingFromWindowInsets()
|
||||
initContentAndFab(justScanned)
|
||||
updateViewsAndFab(scan.text, originalBytes)
|
||||
|
||||
return view
|
||||
}
|
||||
|
||||
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()
|
||||
activity?.wipeShareFile()
|
||||
wipeLastShareFile()
|
||||
}
|
||||
|
||||
private fun initContentAndFab(originalContent: String) {
|
||||
private fun initContentAndFab(justScanned: Boolean) {
|
||||
if (isBinary) {
|
||||
contentView.setText(String(originalBytes).foldNonAlNum())
|
||||
contentView.isEnabled = false
|
||||
@@ -138,7 +214,7 @@ class DecodeFragment : Fragment() {
|
||||
askForFileNameAndSave(originalBytes)
|
||||
}
|
||||
} else {
|
||||
contentView.setText(originalContent)
|
||||
contentView.setText(scan.text)
|
||||
contentView.addTextChangedListener(object : TextWatcher {
|
||||
override fun afterTextChanged(s: Editable?) {
|
||||
updateViewsAndFab(s?.toString() ?: "")
|
||||
@@ -161,7 +237,8 @@ class DecodeFragment : Fragment() {
|
||||
fab.setOnClickListener {
|
||||
executeAction(content)
|
||||
}
|
||||
if (prefs.openImmediately) {
|
||||
if (justScanned && prefs.openImmediately) {
|
||||
closeAutomatically = true
|
||||
executeAction(content)
|
||||
}
|
||||
}
|
||||
@@ -174,17 +251,16 @@ class DecodeFragment : Fragment() {
|
||||
}
|
||||
|
||||
private fun resolveActionAndUpdateFab(bytes: ByteArray) {
|
||||
val prevAction = action
|
||||
if (!prevAction.canExecuteOn(bytes)) {
|
||||
action = ActionRegistry.getAction(bytes)
|
||||
if (!isBinary) {
|
||||
val prevAction = action
|
||||
if (!prevAction.canExecuteOn(bytes)) {
|
||||
action = ActionRegistry.getAction(bytes)
|
||||
}
|
||||
if (prevAction !== action) {
|
||||
fab.setImageResource(action.iconResId)
|
||||
}
|
||||
}
|
||||
if (prevAction !== action) {
|
||||
updateFab(action)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateFab(action: IAction) {
|
||||
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)
|
||||
@@ -199,35 +275,119 @@ class DecodeFragment : Fragment() {
|
||||
formatView.text = resources.getQuantityString(
|
||||
R.plurals.barcode_info,
|
||||
bytes.size,
|
||||
prettifyFormatName(format),
|
||||
format.prettifyFormatName(),
|
||||
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
|
||||
)
|
||||
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()
|
||||
)
|
||||
}
|
||||
try {
|
||||
v.setImageBitmap(barcode.bitmap(recreationSize))
|
||||
v.setOnClickListener {
|
||||
v.context.hideSoftKeyboard(contentView)
|
||||
v.post {
|
||||
fragmentManager?.addFragment(
|
||||
BarcodeFragment.newInstance(barcode)
|
||||
)
|
||||
}
|
||||
}
|
||||
} catch (_: RuntimeException) {
|
||||
clearRecreation()
|
||||
}
|
||||
}
|
||||
dataView.fillDataView(text)
|
||||
dataView.fillDataView(text, bytes)
|
||||
stampView.setTrackingLink(bytes, format)
|
||||
}
|
||||
|
||||
private fun TableLayout.fillDataView(text: String) {
|
||||
val items = LinkedHashMap<Int, String?>()
|
||||
if (action is WifiAction) {
|
||||
WifiConnector.parseMap(text)?.let { wifiData ->
|
||||
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(
|
||||
Uri.parse(text).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),
|
||||
@@ -247,7 +407,7 @@ class DecodeFragment : Fragment() {
|
||||
}
|
||||
|
||||
private fun TableLayout.fillMetaView(scan: Scan) {
|
||||
val items = linkedMapOf(
|
||||
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(),
|
||||
@@ -258,7 +418,7 @@ class DecodeFragment : Fragment() {
|
||||
R.string.gtin_issue_number to scan.issueNumber,
|
||||
)
|
||||
if (!scan.version.isNullOrBlank()) {
|
||||
val versionString = if (scan.format == "QR_CODE") {
|
||||
val versionString = if (scan.format.name == "QR_CODE") {
|
||||
getString(
|
||||
R.string.qr_version_and_modules,
|
||||
scan.version,
|
||||
@@ -273,11 +433,20 @@ class DecodeFragment : Fragment() {
|
||||
)
|
||||
)
|
||||
}
|
||||
if (scan.format == ZxingCpp.BarcodeFormat.QR_CODE &&
|
||||
scan.dataMask > -1
|
||||
) {
|
||||
items.putAll(
|
||||
linkedMapOf(
|
||||
R.string.qr_data_mask to scan.dataMask.toString()
|
||||
)
|
||||
)
|
||||
}
|
||||
fill(items)
|
||||
}
|
||||
|
||||
private fun TableLayout.fill(
|
||||
items: LinkedHashMap<Int, String?>
|
||||
items: LinkedHashMap<Any, CharSequence?>
|
||||
) {
|
||||
removeAllViews()
|
||||
visibility = if (items.isEmpty()) View.GONE else {
|
||||
@@ -288,10 +457,18 @@ class DecodeFragment : Fragment() {
|
||||
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
|
||||
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)
|
||||
@@ -325,6 +502,12 @@ class DecodeFragment : Fragment() {
|
||||
true
|
||||
}
|
||||
|
||||
R.id.copy_as_deeplink -> {
|
||||
copyToClipboard(deeplinkToCopy())
|
||||
maybeBackOrFinish()
|
||||
true
|
||||
}
|
||||
|
||||
R.id.share -> {
|
||||
context?.apply {
|
||||
shareText(textOrHex())
|
||||
@@ -335,7 +518,12 @@ class DecodeFragment : Fragment() {
|
||||
|
||||
R.id.share_file -> {
|
||||
context?.apply {
|
||||
shareAsFile(if (isBinary) originalBytes else content)
|
||||
val (data, name) = if (isBinary) {
|
||||
Pair(originalBytes, "barcode_content.bin")
|
||||
} else {
|
||||
Pair(content, "barcode_content.txt")
|
||||
}
|
||||
shareAsFile(data, name)
|
||||
maybeBackOrFinish()
|
||||
}
|
||||
true
|
||||
@@ -352,7 +540,7 @@ class DecodeFragment : Fragment() {
|
||||
}
|
||||
|
||||
R.id.remove -> {
|
||||
db.removeScan(id)
|
||||
db.removeScan(scan.id)
|
||||
backOrFinish()
|
||||
true
|
||||
}
|
||||
@@ -367,14 +555,16 @@ class DecodeFragment : Fragment() {
|
||||
content
|
||||
}
|
||||
|
||||
private fun deeplinkToCopy() = createEncodeDeeplink(
|
||||
format = format,
|
||||
content = textOrHex(),
|
||||
)
|
||||
|
||||
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)
|
||||
}
|
||||
copyToClipboard(password, isSensitive = true)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -382,7 +572,10 @@ class DecodeFragment : Fragment() {
|
||||
private fun copyToClipboard(text: String, isSensitive: Boolean = false) {
|
||||
activity?.apply {
|
||||
copyToClipboard(text, isSensitive)
|
||||
toast(R.string.copied_to_clipboard)
|
||||
// There's a clipboard popup from Android 13 on.
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
|
||||
toast(R.string.copied_to_clipboard)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -401,20 +594,51 @@ class DecodeFragment : Fragment() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun executeAction(content: String) {
|
||||
private fun executeAction(str: 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()
|
||||
}
|
||||
if (str.isEmpty() || openLocalDocument(str)) {
|
||||
return
|
||||
}
|
||||
if (action is WifiAction &&
|
||||
Build.VERSION.SDK_INT < Build.VERSION_CODES.Q &&
|
||||
!ac.hasLocationPermission { executeAction(str) }
|
||||
) {
|
||||
return
|
||||
}
|
||||
scope.launch {
|
||||
action.execute(ac, 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) {
|
||||
@@ -443,6 +667,8 @@ class DecodeFragment : Fragment() {
|
||||
|
||||
companion object {
|
||||
private const val SCAN = "scan"
|
||||
private const val OPEN_DOCUMENT = 1
|
||||
private const val SCHEME_FILE = "file://"
|
||||
|
||||
fun newInstance(scan: Scan): Fragment {
|
||||
val args = Bundle()
|
||||
@@ -467,7 +693,7 @@ private inline fun <T : View> T.showIf(
|
||||
}
|
||||
|
||||
private val nonAlNum = "[^a-zA-Z0-9]".toRegex()
|
||||
private val multipleDots = "[…]+".toRegex()
|
||||
private val multipleDots = "…+".toRegex()
|
||||
private fun String.foldNonAlNum() = replace(nonAlNum, "…")
|
||||
.replace(multipleDots, "…")
|
||||
|
||||
@@ -563,7 +789,7 @@ private fun generateDpTrackingLink(bytes: ByteArray, format: String): String? {
|
||||
"%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>"
|
||||
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.
|
||||
@@ -598,3 +824,60 @@ private fun String.fromHtml() = if (
|
||||
@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)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -3,9 +3,9 @@ package de.markusfisch.android.binaryeye.fragment
|
||||
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.support.v7.widget.SwitchCompat
|
||||
import android.view.LayoutInflater
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
@@ -16,20 +16,19 @@ 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 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.app.setFragment
|
||||
import de.markusfisch.android.binaryeye.text.unescape
|
||||
import de.markusfisch.android.binaryeye.view.hideSoftKeyboard
|
||||
import de.markusfisch.android.binaryeye.view.setPaddingFromWindowInsets
|
||||
import de.markusfisch.android.binaryeye.widget.toast
|
||||
import de.markusfisch.android.zxingcpp.ZxingCpp.Format
|
||||
import de.markusfisch.android.zxingcpp.ZxingCpp.BarcodeFormat
|
||||
import java.io.InputStream
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
|
||||
class EncodeFragment : Fragment() {
|
||||
@@ -38,29 +37,24 @@ class EncodeFragment : Fragment() {
|
||||
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(
|
||||
Format.AZTEC,
|
||||
Format.CODABAR,
|
||||
Format.CODE_39,
|
||||
Format.CODE_128,
|
||||
Format.DATA_MATRIX,
|
||||
Format.EAN_8,
|
||||
Format.EAN_13,
|
||||
Format.ITF,
|
||||
Format.PDF_417,
|
||||
Format.QR_CODE,
|
||||
Format.UPC_A
|
||||
BarcodeFormat.AZTEC,
|
||||
BarcodeFormat.CODABAR,
|
||||
BarcodeFormat.CODE_39,
|
||||
BarcodeFormat.CODE_128,
|
||||
BarcodeFormat.DATA_MATRIX,
|
||||
BarcodeFormat.EAN_8,
|
||||
BarcodeFormat.EAN_13,
|
||||
BarcodeFormat.ITF,
|
||||
BarcodeFormat.PDF_417,
|
||||
BarcodeFormat.QR_CODE,
|
||||
BarcodeFormat.UPC_A
|
||||
)
|
||||
|
||||
private var minMargin = 0
|
||||
private var bytes: ByteArray? = null
|
||||
|
||||
override fun onActivityResult(
|
||||
@@ -109,7 +103,7 @@ class EncodeFragment : Fragment() {
|
||||
val formatAdapter = ArrayAdapter(
|
||||
ac,
|
||||
android.R.layout.simple_spinner_item,
|
||||
formats.map { prettifyFormatName(it.name) }
|
||||
formats.map { it.name.prettifyFormatName() }
|
||||
)
|
||||
formatAdapter.setDropDownViewResource(
|
||||
android.R.layout.simple_spinner_dropdown_item
|
||||
@@ -124,9 +118,9 @@ class EncodeFragment : Fragment() {
|
||||
) {
|
||||
val format = formats[position]
|
||||
val arrayId = when (format) {
|
||||
Format.AZTEC -> R.array.aztec_error_correction_levels
|
||||
Format.QR_CODE -> R.array.qr_error_correction_levels
|
||||
Format.PDF_417 -> R.array.pdf417_error_correction_levels
|
||||
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
|
||||
else -> 0
|
||||
}
|
||||
if (arrayId > 0) {
|
||||
@@ -145,12 +139,13 @@ class EncodeFragment : Fragment() {
|
||||
colorsLabel, colorsSpinner
|
||||
)
|
||||
when (format) {
|
||||
Format.AZTEC -> setMarginBar(4, 4)
|
||||
Format.DATA_MATRIX -> setMarginBar(1, 1)
|
||||
Format.QR_CODE -> setMarginBar(4, 4)
|
||||
Format.PDF_417 -> setMarginBar(0, 30)
|
||||
BarcodeFormat.AZTEC,
|
||||
BarcodeFormat.DATA_MATRIX,
|
||||
BarcodeFormat.QR_CODE,
|
||||
BarcodeFormat.PDF_417 -> true
|
||||
|
||||
else -> false
|
||||
}.setVisibilityFor(marginLayout)
|
||||
}.setVisibilityFor(addQuietZoneSwitch)
|
||||
}
|
||||
|
||||
override fun onNothingSelected(parentView: AdapterView<*>?) {}
|
||||
@@ -177,26 +172,22 @@ 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
|
||||
|
||||
var complete = false
|
||||
val args = arguments
|
||||
args?.getString(CONTENT_TEXT)?.let {
|
||||
contentView.setText(it)
|
||||
complete = it.isNotEmpty()
|
||||
}
|
||||
args?.getByteArray(CONTENT_RAW)?.let {
|
||||
bytes = it
|
||||
complete = it.isNotEmpty()
|
||||
setEncodeByteArray()
|
||||
}
|
||||
|
||||
@@ -215,8 +206,17 @@ 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()
|
||||
|
||||
if (complete &&
|
||||
args?.getBoolean(EXECUTE, false) == true
|
||||
) {
|
||||
view.post {
|
||||
val content = view.context.getContent() ?: return@post
|
||||
fragmentManager?.setFragment(newEncodeFragment(content))
|
||||
}
|
||||
}
|
||||
|
||||
return view
|
||||
}
|
||||
@@ -248,113 +248,65 @@ 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 {
|
||||
fragmentManager?.addFragment(newEncodeFragment(content))
|
||||
}
|
||||
val format = formats[formatView.selectedItemPosition]
|
||||
fragmentManager?.addFragment(
|
||||
BarcodeFragment.newInstance(
|
||||
content,
|
||||
format,
|
||||
getSize(sizeBarView.progress),
|
||||
when (format) {
|
||||
Format.AZTEC,
|
||||
Format.DATA_MATRIX,
|
||||
Format.QR_CODE,
|
||||
Format.PDF_417 -> marginBarView.progress
|
||||
}
|
||||
|
||||
else -> -1
|
||||
},
|
||||
format.getErrorCorrectionLevel(ecSpinner.selectedItemPosition),
|
||||
if (format.canBeInverted()) {
|
||||
colorsSpinner.selectedItemPosition
|
||||
} else 0
|
||||
)
|
||||
private fun newEncodeFragment(content: Any): Fragment {
|
||||
val format = formats[formatView.selectedItemPosition]
|
||||
return BarcodeFragment.newInstance(
|
||||
content,
|
||||
format,
|
||||
format.getErrorCorrectionLevel(ecSpinner.selectedItemPosition),
|
||||
when (format) {
|
||||
BarcodeFormat.AZTEC,
|
||||
BarcodeFormat.DATA_MATRIX,
|
||||
BarcodeFormat.QR_CODE,
|
||||
BarcodeFormat.PDF_417 -> if (
|
||||
addQuietZoneSwitch.isChecked
|
||||
) 1 else 0
|
||||
|
||||
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() {
|
||||
@@ -368,11 +320,13 @@ class EncodeFragment : Fragment() {
|
||||
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(
|
||||
content: T? = null,
|
||||
format: String? = null
|
||||
format: String? = null,
|
||||
execute: Boolean = false
|
||||
): Fragment {
|
||||
val args = Bundle()
|
||||
content?.let {
|
||||
@@ -383,6 +337,7 @@ class EncodeFragment : Fragment() {
|
||||
"content must be a String or a ByteArray"
|
||||
)
|
||||
}
|
||||
args.putBoolean(EXECUTE, execute)
|
||||
}
|
||||
format?.let { args.putString(FORMAT, it) }
|
||||
val fragment = EncodeFragment()
|
||||
@@ -399,39 +354,41 @@ private fun Boolean.setVisibilityFor(vararg views: View) {
|
||||
}
|
||||
}
|
||||
|
||||
private fun Format.packEcLevel(packed: Int, level: Int): Int {
|
||||
private fun BarcodeFormat.packEcLevel(packed: Int, level: Int): Int {
|
||||
val s = ecLevelShift()
|
||||
return (level shl s) or (packed and (15 shl s).inv())
|
||||
}
|
||||
|
||||
private fun Format.unpackEcLevel(packed: Int) =
|
||||
private fun BarcodeFormat.unpackEcLevel(packed: Int) =
|
||||
(packed shr ecLevelShift()) and 15
|
||||
|
||||
private fun Format.ecLevelShift() = when (this) {
|
||||
Format.AZTEC -> 0
|
||||
Format.QR_CODE -> 4
|
||||
Format.PDF_417 -> 8
|
||||
private fun BarcodeFormat.ecLevelShift() = when (this) {
|
||||
BarcodeFormat.AZTEC -> 0
|
||||
BarcodeFormat.QR_CODE -> 4
|
||||
BarcodeFormat.PDF_417 -> 8
|
||||
else -> throw IllegalArgumentException("$this does not have error levels")
|
||||
}
|
||||
|
||||
private fun Format.canBeInverted() = when (this) {
|
||||
Format.AZTEC,
|
||||
Format.DATA_MATRIX,
|
||||
Format.QR_CODE -> true
|
||||
private fun BarcodeFormat.canBeInverted() = when (this) {
|
||||
BarcodeFormat.AZTEC,
|
||||
BarcodeFormat.DATA_MATRIX,
|
||||
BarcodeFormat.QR_CODE -> true
|
||||
|
||||
else -> false
|
||||
}
|
||||
|
||||
private fun String.toFormat(default: Format = Format.QR_CODE): Format = try {
|
||||
Format.valueOf(this)
|
||||
private fun String.toFormat(
|
||||
default: BarcodeFormat = BarcodeFormat.QR_CODE
|
||||
): BarcodeFormat = try {
|
||||
BarcodeFormat.valueOf(this)
|
||||
} catch (_: IllegalArgumentException) {
|
||||
default
|
||||
}
|
||||
|
||||
private fun Format.getErrorCorrectionLevel(position: Int) = when (this) {
|
||||
Format.AZTEC -> position
|
||||
Format.QR_CODE -> (position + 1) * 2
|
||||
Format.PDF_417 -> position
|
||||
private fun BarcodeFormat.getErrorCorrectionLevel(position: Int) = when (this) {
|
||||
BarcodeFormat.AZTEC -> position
|
||||
BarcodeFormat.QR_CODE -> (position + 1) * 2
|
||||
BarcodeFormat.PDF_417 -> position
|
||||
else -> 0
|
||||
}.coerceIn(0, 8)
|
||||
|
||||
@@ -446,8 +403,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)
|
||||
@@ -461,4 +416,4 @@ private fun InputStream.readBytesMax(max: Int): ByteArray {
|
||||
offset += read
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,20 +15,43 @@ 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 de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.adapter.ScansAdapter
|
||||
import de.markusfisch.android.binaryeye.app.*
|
||||
import de.markusfisch.android.binaryeye.app.addFragment
|
||||
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.database.*
|
||||
import de.markusfisch.android.binaryeye.content.wipeLastShareFile
|
||||
import de.markusfisch.android.binaryeye.database.Database
|
||||
import de.markusfisch.android.binaryeye.database.exportCsv
|
||||
import de.markusfisch.android.binaryeye.database.exportDatabase
|
||||
import de.markusfisch.android.binaryeye.database.exportJson
|
||||
import de.markusfisch.android.binaryeye.database.use
|
||||
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() {
|
||||
private lateinit var useHistorySwitch: SwitchCompat
|
||||
@@ -47,9 +70,13 @@ class HistoryFragment : Fragment() {
|
||||
R.menu.fragment_history_edit,
|
||||
menu
|
||||
)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP &&
|
||||
// SDK35+ doesn't draw a status bar background anymore.
|
||||
Build.VERSION.SDK_INT < Build.VERSION_CODES.VANILLA_ICE_CREAM
|
||||
) {
|
||||
lockStatusBarColor()
|
||||
val ac = activity ?: return false
|
||||
@Suppress("DEPRECATION")
|
||||
ac.window.statusBarColor = ContextCompat.getColor(
|
||||
ac,
|
||||
R.color.accent_dark
|
||||
@@ -80,7 +107,7 @@ class HistoryFragment : Fragment() {
|
||||
true
|
||||
}
|
||||
|
||||
R.id.edit_scan -> {
|
||||
R.id.edit_label -> {
|
||||
scansAdapter?.forSelection { id, position ->
|
||||
ac.askForName(
|
||||
id,
|
||||
@@ -117,6 +144,7 @@ class HistoryFragment : Fragment() {
|
||||
private var filter: String? = null
|
||||
private var clearListMenuItem: MenuItem? = null
|
||||
private var exportHistoryMenuItem: MenuItem? = null
|
||||
private var shareAsFileMenuItem: MenuItem? = null
|
||||
|
||||
override fun onCreate(state: Bundle?) {
|
||||
super.onCreate(state)
|
||||
@@ -139,12 +167,16 @@ class HistoryFragment : Fragment() {
|
||||
|
||||
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)
|
||||
@@ -159,12 +191,12 @@ class HistoryFragment : Fragment() {
|
||||
|
||||
fab = view.findViewById(R.id.share)
|
||||
fab.setOnClickListener { v ->
|
||||
v.context.pickListSeparatorAndShare()
|
||||
v.context.pickListSeparatorAndShare(false)
|
||||
}
|
||||
|
||||
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()
|
||||
@@ -176,6 +208,7 @@ class HistoryFragment : Fragment() {
|
||||
super.onDestroy()
|
||||
scansAdapter?.changeCursor(null)
|
||||
parentJob.cancel()
|
||||
wipeLastShareFile()
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
@@ -185,10 +218,19 @@ class HistoryFragment : Fragment() {
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
inflater.inflate(R.menu.fragment_history, menu)
|
||||
initSearchView(menu.findItem(R.id.search))
|
||||
menu.setGroupVisible(R.id.scans_available, scansAdapter?.count != 0)
|
||||
clearListMenuItem = menu.findItem(R.id.clear)
|
||||
exportHistoryMenuItem = menu.findItem(R.id.export_history)
|
||||
val searchMenuItem = menu.findItem(R.id.search)
|
||||
initSearchView(searchMenuItem)
|
||||
val visible = scansAdapter?.count != 0
|
||||
searchMenuItem?.isVisible = visible
|
||||
exportHistoryMenuItem = menu.findItem(R.id.export_history)?.apply {
|
||||
isVisible = visible
|
||||
}
|
||||
shareAsFileMenuItem = menu.findItem(R.id.share_as_file)?.apply {
|
||||
isVisible = visible
|
||||
}
|
||||
clearListMenuItem = menu.findItem(R.id.clear)?.apply {
|
||||
isVisible = visible
|
||||
}
|
||||
}
|
||||
|
||||
private fun initSearchView(item: MenuItem?) {
|
||||
@@ -230,6 +272,11 @@ class HistoryFragment : Fragment() {
|
||||
true
|
||||
}
|
||||
|
||||
R.id.share_as_file -> {
|
||||
context.pickListSeparatorAndShare(true)
|
||||
true
|
||||
}
|
||||
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
}
|
||||
@@ -254,7 +301,7 @@ class HistoryFragment : Fragment() {
|
||||
val cursor = db.getScans(filter)
|
||||
withContext(Dispatchers.Main) {
|
||||
val ac = activity ?: return@withContext
|
||||
val hasScans = cursor != null && cursor.count > 0
|
||||
val hasScans = cursor.count > 0
|
||||
if (filter == null) {
|
||||
if (!hasScans) {
|
||||
listView.emptyView = useHistorySwitch
|
||||
@@ -267,7 +314,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)
|
||||
@@ -283,6 +330,7 @@ class HistoryFragment : Fragment() {
|
||||
private fun enableMenuItems(enabled: Boolean) {
|
||||
clearListMenuItem?.isEnabled = enabled
|
||||
exportHistoryMenuItem?.isEnabled = enabled
|
||||
shareAsFileMenuItem?.isEnabled = enabled
|
||||
}
|
||||
|
||||
private fun closeActionMode() {
|
||||
@@ -297,7 +345,7 @@ class HistoryFragment : Fragment() {
|
||||
closeActionMode()
|
||||
try {
|
||||
fragmentManager?.addFragment(DecodeFragment.newInstance(scan))
|
||||
} catch (e: IllegalArgumentException) {
|
||||
} catch (_: IllegalArgumentException) {
|
||||
// Ignore, can never happen.
|
||||
}
|
||||
}
|
||||
@@ -398,12 +446,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)
|
||||
@@ -412,32 +460,44 @@ class HistoryFragment : Fragment() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun Context.pickListSeparatorAndShare() {
|
||||
private fun Context.pickListSeparatorAndShare(asFile: Boolean) {
|
||||
val separators = resources.getStringArray(
|
||||
R.array.list_separators_values
|
||||
)
|
||||
AlertDialog.Builder(this)
|
||||
.setTitle(R.string.pick_list_separator)
|
||||
.setItems(R.array.list_separators_names) { _, which ->
|
||||
shareScans(separators[which])
|
||||
shareScans(separators[which], asFile)
|
||||
}
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun shareScans(format: String) = scope.launch {
|
||||
private fun shareScans(format: String, asFile: Boolean) = scope.launch {
|
||||
progressView.useVisibility {
|
||||
var text: String? = null
|
||||
db.getScansDetailed(filter)?.use { cursor ->
|
||||
val selectedIds = scansAdapter?.getSelectedIds()
|
||||
if (selectedIds?.isNotEmpty() == true) {
|
||||
db.getScansDetailed(selectedIds.toLongArray())
|
||||
} else {
|
||||
db.getScansDetailed(filter)
|
||||
}.use { cursor ->
|
||||
val details = format.split(":")
|
||||
text = when (details[0]) {
|
||||
"text" -> cursor.exportText(details[1])
|
||||
"csv" -> cursor.exportCsv(details[1])
|
||||
else -> cursor.exportJson()
|
||||
when (details[0]) {
|
||||
"text" -> Pair(cursor.exportText(details[1]), "txt")
|
||||
"csv" -> Pair(cursor.exportCsv(details[1]), "csv")
|
||||
else -> Pair(cursor.exportJson(), "json")
|
||||
}
|
||||
}
|
||||
text?.let {
|
||||
withContext(Dispatchers.Main) {
|
||||
context.shareText(it)
|
||||
}.let { (text, ext) ->
|
||||
text?.let {
|
||||
withContext(Dispatchers.Main) {
|
||||
if (asFile) {
|
||||
context.shareAsFile(
|
||||
it,
|
||||
String.format("scans.%s", ext)
|
||||
)
|
||||
} else {
|
||||
context.shareText(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -446,7 +506,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)
|
||||
|
||||
+4
-2
@@ -7,7 +7,9 @@ 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.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.ListView
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
@@ -49,7 +51,7 @@ 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 {
|
||||
|
||||
+108
-29
@@ -28,6 +28,8 @@ 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?,
|
||||
@@ -35,34 +37,78 @@ class PreferencesFragment : PreferenceFragmentCompat() {
|
||||
) {
|
||||
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)
|
||||
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)
|
||||
|
||||
wireProfiles()
|
||||
wireAutomatedActions()
|
||||
setBluetoothResources()
|
||||
wireClearNetworkPreferences()
|
||||
}
|
||||
|
||||
private fun wireProfiles() {
|
||||
findPreference(PROFILE).apply {
|
||||
updateProfileSummary(this)
|
||||
onPreferenceClickListener = Preference.OnPreferenceClickListener {
|
||||
activity?.supportFragmentManager?.addFragment(
|
||||
ProfilesFragment()
|
||||
)
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun wireAutomatedActions() {
|
||||
findPreference(AUTOMATED_ACTIONS).apply {
|
||||
updateAutomatedActionsSummary(this)
|
||||
onPreferenceClickListener = Preference.OnPreferenceClickListener {
|
||||
activity?.supportFragmentManager?.addFragment(
|
||||
AutomatedActionsFragment()
|
||||
)
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setBluetoothResources() {
|
||||
if (prefs.sendScanBluetooth &&
|
||||
activity?.hasBluetoothPermission() == true
|
||||
@@ -79,7 +125,9 @@ class PreferencesFragment : PreferenceFragmentCompat() {
|
||||
// From R+ we can query past network suggestions and
|
||||
// make them editable.
|
||||
setOnPreferenceClickListener {
|
||||
fragmentManager.addFragment(NetworkSuggestionsFragment())
|
||||
activity?.supportFragmentManager?.addFragment(
|
||||
NetworkSuggestionsFragment()
|
||||
)
|
||||
true
|
||||
}
|
||||
} else if (Build.VERSION.SDK_INT == Build.VERSION_CODES.Q) {
|
||||
@@ -124,13 +172,19 @@ class PreferencesFragment : PreferenceFragmentCompat() {
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
if (lastProfile != prefs.profile) {
|
||||
loadPreferences()
|
||||
}
|
||||
activity?.setTitle(R.string.preferences)
|
||||
findPreference(AUTOMATED_ACTIONS)?.let {
|
||||
updateAutomatedActionsSummary(it)
|
||||
}
|
||||
findPreference(PROFILE)?.let {
|
||||
updateProfileSummary(it)
|
||||
}
|
||||
listView.setPaddingFromWindowInsets()
|
||||
listView.removeOnScrollListener(systemBarRecyclerViewScrollListener)
|
||||
listView.addOnScrollListener(systemBarRecyclerViewScrollListener)
|
||||
preferenceScreen.sharedPreferences
|
||||
.registerOnSharedPreferenceChangeListener(changeListener)
|
||||
setSummaries(preferenceScreen)
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
@@ -165,9 +219,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 +233,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,6 +243,29 @@ class PreferencesFragment : PreferenceFragmentCompat() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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() {
|
||||
|
||||
@@ -0,0 +1,179 @@
|
||||
package de.markusfisch.android.binaryeye.fragment
|
||||
|
||||
import android.app.AlertDialog
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.support.v4.app.Fragment
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.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 ProfilesFragment : Fragment() {
|
||||
private val profiles = ArrayList<ProfileItem>()
|
||||
|
||||
private lateinit var adapter: ProfilesAdapter
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
state: Bundle?
|
||||
): View? {
|
||||
val ac = activity ?: return null
|
||||
ac.setTitle(R.string.profile)
|
||||
|
||||
val view = inflater.inflate(
|
||||
R.layout.fragment_profiles,
|
||||
container,
|
||||
false
|
||||
)
|
||||
|
||||
refreshProfiles()
|
||||
|
||||
val listView = view.findViewById<ListView>(R.id.profiles)
|
||||
listView.emptyView = view.findViewById(R.id.no_profiles)
|
||||
adapter = ProfilesAdapter(ac, 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()
|
||||
|
||||
return view
|
||||
}
|
||||
|
||||
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 ac = activity ?: return
|
||||
val profile = profiles[position].name
|
||||
if (prefs.profile == profile) {
|
||||
return
|
||||
}
|
||||
prefs.load(ac, profile)
|
||||
adapter.notifyDataSetChanged()
|
||||
}
|
||||
|
||||
// Dialogs do not have a parent view.
|
||||
@Suppress("InflateParams")
|
||||
private fun addProfile() {
|
||||
val ac = activity ?: return
|
||||
val view = ac.layoutInflater.inflate(
|
||||
R.layout.dialog_profile_name, null
|
||||
)
|
||||
val editText = view.findViewById<EditText>(R.id.name)
|
||||
AlertDialog.Builder(ac)
|
||||
.setView(view)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
val name = editText.text.toString().trim()
|
||||
if (name.isNotEmpty() && prefs.addProfile(name)) {
|
||||
prefs.load(ac, name)
|
||||
refreshProfiles()
|
||||
} else {
|
||||
ac.toast(R.string.profile_invalid_name)
|
||||
}
|
||||
}
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun confirmRemoveProfile(position: Int) {
|
||||
val ac = activity ?: return
|
||||
val profile = profiles[position]
|
||||
val profileName = profile.name ?: return
|
||||
AlertDialog.Builder(ac)
|
||||
.setMessage(R.string.profile_remove_confirm)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
val isCurrent = prefs.profile == profileName
|
||||
prefs.removeProfile(profileName)
|
||||
if (isCurrent) {
|
||||
prefs.load(ac, 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
|
||||
)
|
||||
@@ -9,6 +9,7 @@ import de.markusfisch.android.binaryeye.app.prefs
|
||||
import de.markusfisch.android.binaryeye.database.Scan
|
||||
import de.markusfisch.android.binaryeye.net.sendAsync
|
||||
import de.markusfisch.android.binaryeye.preference.UrlPreference
|
||||
import de.markusfisch.android.zxingcpp.ZxingCpp.BarcodeFormat
|
||||
|
||||
class UrlDialogFragment : PreferenceDialogFragmentCompat() {
|
||||
private var urlView: TextView? = null
|
||||
@@ -50,7 +51,7 @@ class UrlDialogFragment : PreferenceDialogFragmentCompat() {
|
||||
Scan(
|
||||
"test",
|
||||
null,
|
||||
"none"
|
||||
BarcodeFormat.NONE
|
||||
).sendAsync(url, prefs.sendScanType) { code, body ->
|
||||
textView.text = when {
|
||||
code != null -> "$code"
|
||||
@@ -79,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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -5,11 +5,12 @@ import android.graphics.Canvas
|
||||
|
||||
fun Bitmap.fixTransparency(): Bitmap {
|
||||
val result = analyzeTransparency()
|
||||
return if (result.hasTransparentPixels) {
|
||||
val cfg = config
|
||||
return if (cfg != null && result.hasTransparentPixels) {
|
||||
val copy = Bitmap.createBitmap(
|
||||
width,
|
||||
height,
|
||||
config
|
||||
cfg
|
||||
)
|
||||
Canvas(copy).apply {
|
||||
drawColor(result.backgroundColor)
|
||||
|
||||
@@ -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,9 +69,9 @@ 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,
|
||||
"format" to format.name,
|
||||
"errorCorrectionLevel" to errorCorrectionLevel,
|
||||
"version" to version,
|
||||
"sequenceSize" to sequenceSize.toString(),
|
||||
|
||||
@@ -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,46 @@
|
||||
package de.markusfisch.android.binaryeye.preference
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import android.graphics.Point
|
||||
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()
|
||||
).apply()
|
||||
}
|
||||
|
||||
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}"
|
||||
@@ -1,55 +1,56 @@
|
||||
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 de.markusfisch.android.zxingcpp.ZxingCpp.Format
|
||||
import de.markusfisch.android.binaryeye.automation.AutomatedAction
|
||||
import de.markusfisch.android.binaryeye.automation.AutomatedAction.Companion.fromJsonArray
|
||||
import de.markusfisch.android.binaryeye.automation.AutomatedAction.Companion.toJsonArray
|
||||
import de.markusfisch.android.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).apply()
|
||||
field = value
|
||||
}
|
||||
var barcodeFormats = setOf(
|
||||
Format.AZTEC.name,
|
||||
Format.CODABAR.name,
|
||||
Format.CODE_39.name,
|
||||
Format.CODE_93.name,
|
||||
Format.CODE_128.name,
|
||||
Format.DATA_BAR.name,
|
||||
Format.DATA_BAR_EXPANDED.name,
|
||||
Format.DATA_MATRIX.name,
|
||||
Format.EAN_8.name,
|
||||
Format.EAN_13.name,
|
||||
Format.ITF.name,
|
||||
Format.MAXICODE.name,
|
||||
Format.PDF_417.name,
|
||||
Format.QR_CODE.name,
|
||||
Format.MICRO_QR_CODE.name,
|
||||
Format.UPC_A.name,
|
||||
Format.UPC_E.name,
|
||||
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.DX_FILM_EDGE.name,
|
||||
BarcodeFormat.EAN_8.name,
|
||||
BarcodeFormat.EAN_13.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,
|
||||
)
|
||||
@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)
|
||||
@@ -105,9 +106,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
|
||||
@@ -130,6 +131,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)
|
||||
@@ -150,7 +156,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
|
||||
@@ -192,12 +198,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
|
||||
@@ -207,24 +208,94 @@ 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 addProfile(name: String): Boolean {
|
||||
if (!name.matches("^[a-zA-Z0-9_-]+$".toRegex()) ||
|
||||
profiles.contains(name)
|
||||
) {
|
||||
return false
|
||||
}
|
||||
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()
|
||||
).apply()
|
||||
}
|
||||
|
||||
fun loadProfiles() {
|
||||
profiles.clear()
|
||||
JSONArray(
|
||||
defaultPreferences.getString(PROFILES, "[]")
|
||||
).let {
|
||||
profiles.addAll(Array(it.length()) { i -> it.getString(i) })
|
||||
}
|
||||
}
|
||||
|
||||
fun update() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
|
||||
preferences.getStringSet(BARCODE_FORMATS, barcodeFormats)?.let {
|
||||
barcodeFormats = it
|
||||
barcodeFormats = addFormatsOnUpdate(
|
||||
it,
|
||||
BarcodeFormat.RMQR_CODE,
|
||||
BarcodeFormat.DX_FILM_EDGE
|
||||
)
|
||||
}
|
||||
}
|
||||
cropHandleX = preferences.getInt(CROP_HANDLE_X, cropHandleX)
|
||||
cropHandleY = preferences.getInt(CROP_HANDLE_Y, cropHandleY)
|
||||
cropHandleOrientation = preferences.getInt(
|
||||
CROP_HANDLE_ORIENTATION,
|
||||
cropHandleOrientation
|
||||
)
|
||||
|
||||
showCropHandle = preferences.getBoolean(
|
||||
SHOW_CROP_HANDLE,
|
||||
showCropHandle
|
||||
@@ -247,10 +318,20 @@ 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).apply()
|
||||
ignoreDuplicatesName = "accept_duplicates"
|
||||
}
|
||||
|
||||
preferences.getString(
|
||||
IGNORE_DUPLICATES_NAME,
|
||||
ignoreDuplicatesName
|
||||
)?.also {
|
||||
ignoreDuplicatesName = it
|
||||
}
|
||||
copyImmediately = preferences.getBoolean(
|
||||
COPY_IMMEDIATELY,
|
||||
copyImmediately
|
||||
@@ -261,6 +342,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,
|
||||
@@ -303,17 +387,56 @@ 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).apply()
|
||||
add(format.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
@@ -323,12 +446,15 @@ class Preferences {
|
||||
else -> ToneGenerator.TONE_PROP_BEEP
|
||||
}
|
||||
|
||||
fun ignoreDuplicates() = when (ignoreDuplicatesName) {
|
||||
"ignore_consecutive_duplicates" -> IgnoreDuplicates.Consecutive
|
||||
"ignore_any_duplicates" -> IgnoreDuplicates.Any
|
||||
else -> IgnoreDuplicates.Never
|
||||
}
|
||||
|
||||
private fun put(label: String, value: Boolean) =
|
||||
preferences.edit().putBoolean(label, value)
|
||||
|
||||
private fun put(label: String, value: String) =
|
||||
preferences.edit().putString(label, value)
|
||||
|
||||
private fun apply(label: String, value: Boolean) {
|
||||
put(label, value).apply()
|
||||
}
|
||||
@@ -341,6 +467,11 @@ class Preferences {
|
||||
preferences.edit().putInt(label, value).apply()
|
||||
}
|
||||
|
||||
private fun apply(label: String, value: Float) {
|
||||
preferences.edit().putFloat(label, value).apply()
|
||||
}
|
||||
|
||||
@SuppressLint("ApplySharedPref")
|
||||
private fun commit(label: String, value: String) {
|
||||
preferences.edit().putString(label, value).commit()
|
||||
}
|
||||
@@ -351,10 +482,13 @@ class Preferences {
|
||||
}
|
||||
|
||||
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"
|
||||
@@ -366,11 +500,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"
|
||||
@@ -383,8 +518,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,5 +1,6 @@
|
||||
package de.markusfisch.android.binaryeye.service
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.PendingIntent
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
@@ -13,17 +14,21 @@ class ScanTileService : TileService() {
|
||||
super.onClick()
|
||||
val intent = Intent(applicationContext, CameraActivity::class.java)
|
||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
// TileService#startActivityAndCollapse(Intent) is only ever run
|
||||
// below UpsideDownCake, but Lint doesn't understand this yet.
|
||||
@SuppressLint("StartActivityAndCollapseDeprecated")
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||
@Suppress("DEPRECATION")
|
||||
startActivityAndCollapse(intent)
|
||||
} else {
|
||||
val pendingIntent = PendingIntent.getActivity(
|
||||
this,
|
||||
0,
|
||||
intent,
|
||||
PendingIntent.FLAG_IMMUTABLE
|
||||
startActivityAndCollapse(
|
||||
PendingIntent.getActivity(
|
||||
this,
|
||||
0,
|
||||
intent,
|
||||
PendingIntent.FLAG_IMMUTABLE
|
||||
)
|
||||
)
|
||||
startActivityAndCollapse(pendingIntent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ 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.support.v7.widget.Toolbar
|
||||
import android.view.View
|
||||
import android.view.WindowManager
|
||||
import android.widget.AbsListView
|
||||
@@ -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,8 +39,10 @@ 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() <
|
||||
val scrolled =
|
||||
layoutManager.findFirstCompletelyVisibleItemPosition() != 0
|
||||
val scrollable = scrolled ||
|
||||
layoutManager.findLastVisibleItemPosition() <
|
||||
recyclerView.adapter.itemCount - 1
|
||||
colorSystemAndToolBars(recyclerView.context, scrolled, scrollable)
|
||||
}
|
||||
@@ -57,10 +61,9 @@ private fun lastChildOutOfView(listView: AbsListView): Boolean {
|
||||
return child != null && child.bottom >= listView.height
|
||||
}
|
||||
|
||||
fun initSystemBars(activity: AppCompatActivity) {
|
||||
fun AppCompatActivity.initBars() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
// Keeps the soft keyboard from repositioning the layout.
|
||||
val window = activity.window
|
||||
window.setSoftInputMode(
|
||||
WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE
|
||||
)
|
||||
@@ -69,7 +72,16 @@ fun initSystemBars(activity: AppCompatActivity) {
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
|
||||
}
|
||||
colorSystemAndToolBars(activity)
|
||||
colorSystemAndToolBars(this)
|
||||
setPaddingFromWindowInsets(
|
||||
findViewById(R.id.main),
|
||||
(findViewById(R.id.toolbar) as Toolbar).apply {
|
||||
setSupportActionBar(this)
|
||||
},
|
||||
findViewById(R.id.navbar)?.apply {
|
||||
setBackgroundColor(translucentPrimaryColor)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private var statusBarColorLocked = false
|
||||
@@ -97,14 +109,27 @@ fun colorSystemAndToolBars(
|
||||
val topColor = if (scrolled) translucentPrimaryColor else 0
|
||||
val activity = getAppCompatActivity(context) ?: return
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
val window = activity.window
|
||||
if (!statusBarColorLocked) {
|
||||
window.statusBarColor = topColor
|
||||
}
|
||||
window.navigationBarColor = if (scrolled || scrollable) {
|
||||
translucentPrimaryColor
|
||||
// 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
|
||||
}
|
||||
} else {
|
||||
0
|
||||
activity.findViewById(R.id.navbar)?.apply {
|
||||
visibility = if (scrolled || scrollable) {
|
||||
View.VISIBLE
|
||||
} else {
|
||||
View.GONE
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
activity.supportActionBar?.setBackgroundDrawable(
|
||||
|
||||
@@ -7,10 +7,41 @@ 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
|
||||
|
||||
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() {
|
||||
|
||||
+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,10 +2,13 @@ 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
|
||||
@@ -23,10 +26,10 @@ 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()
|
||||
@@ -42,7 +45,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 +56,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 +84,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 +125,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 +221,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) {
|
||||
@@ -344,37 +325,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,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>
|
||||
@@ -8,9 +8,10 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"/>
|
||||
<android.support.design.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"
|
||||
|
||||
@@ -4,9 +4,11 @@
|
||||
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>
|
||||
|
||||
@@ -9,9 +9,10 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"/>
|
||||
<android.support.design.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
|
||||
style="@style/FAB"
|
||||
@@ -19,4 +20,5 @@
|
||||
android:contentDescription="@string/scan_code"
|
||||
android:src="@drawable/ic_action_next"/>
|
||||
</android.support.design.widget.CoordinatorLayout>
|
||||
<include layout="@layout/navbar"/>
|
||||
</merge>
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
<ScrollView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
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:drawableTop="@drawable/logo"
|
||||
android:drawablePadding="16dp"
|
||||
android:gravity="center"
|
||||
android:textAppearance="?android:attr/textAppearanceLarge"
|
||||
android:text="@string/welcome"/>
|
||||
|
||||
<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"/>
|
||||
<android.support.design.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"/>
|
||||
<android.support.design.widget.FloatingActionButton
|
||||
style="@style/FAB"
|
||||
android:id="@+id/add"
|
||||
android:contentDescription="@string/automated_action_add"
|
||||
android:src="@drawable/ic_action_create"/>
|
||||
</android.support.design.widget.CoordinatorLayout>
|
||||
</FrameLayout>
|
||||
@@ -13,6 +13,17 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
<EditText
|
||||
android:id="@+id/label"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:gravity="start|top"
|
||||
android:hint="@string/label"
|
||||
android:imeOptions="flagNoExtractUi"
|
||||
android:importantForAutofill="no"
|
||||
android:inputType="text"
|
||||
tools:ignore="UnusedAttribute"/>
|
||||
<EditText
|
||||
android:id="@+id/content"
|
||||
android:layout_width="match_parent"
|
||||
|
||||
@@ -65,42 +65,12 @@
|
||||
android:id="@+id/colors"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:entries="@array/colors"/>
|
||||
<TextView
|
||||
android:id="@+id/size_display"
|
||||
<android.support.v7.widget.SwitchCompat
|
||||
android:visibility="gone"
|
||||
android:id="@+id/add_quiet_zone"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
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="4"
|
||||
android:max="7"/>
|
||||
<LinearLayout
|
||||
android:id="@+id/margin"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:visibility="gone">
|
||||
<TextView
|
||||
android:id="@+id/margin_display"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="8dp"/>
|
||||
<SeekBar
|
||||
style="@style/Widget.AppCompat.SeekBar.Discrete"
|
||||
android:id="@+id/margin_bar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:paddingTop="16dp"
|
||||
android:paddingBottom="16dp"
|
||||
android:max="32"/>
|
||||
</LinearLayout>
|
||||
android:text="@string/add_quiet_zone"/>
|
||||
</LinearLayout>
|
||||
</de.markusfisch.android.binaryeye.widget.ConfinedScrollView>
|
||||
<android.support.design.widget.CoordinatorLayout
|
||||
|
||||
@@ -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/profiles"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:clipToPadding="false"
|
||||
android:divider="@color/separator"
|
||||
android:dividerHeight="1px"/>
|
||||
<android.support.design.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_profiles"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:gravity="center"
|
||||
android:padding="32dp"
|
||||
android:text="@string/profile_empty"/>
|
||||
<android.support.design.widget.FloatingActionButton
|
||||
style="@style/FAB"
|
||||
android:id="@+id/add"
|
||||
android:contentDescription="@string/profile_add"
|
||||
android:src="@drawable/ic_action_create"/>
|
||||
</android.support.design.widget.CoordinatorLayout>
|
||||
</FrameLayout>
|
||||
@@ -0,0 +1,8 @@
|
||||
<!-- Navigation bar background from SDK35+ -->
|
||||
<View
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/navbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_gravity="bottom"
|
||||
android:visibility="gone"/>
|
||||
@@ -32,6 +32,11 @@
|
||||
android:title="@string/restrict_format"
|
||||
android:icon="@drawable/ic_action_restrict_format"
|
||||
material:showAsAction="ifRoom"/>
|
||||
<item
|
||||
android:id="@+id/find_code"
|
||||
android:title="@string/find_code"
|
||||
android:icon="@drawable/ic_action_search"
|
||||
material:showAsAction="ifRoom"/>
|
||||
<item
|
||||
android:id="@+id/preferences"
|
||||
android:title="@string/preferences"
|
||||
@@ -42,4 +47,9 @@
|
||||
android:title="@string/info"
|
||||
android:icon="@drawable/ic_action_info"
|
||||
material:showAsAction="ifRoom"/>
|
||||
<item
|
||||
android:id="@+id/profile"
|
||||
android:title="@string/profile_default"
|
||||
android:icon="@drawable/ic_action_profile"
|
||||
material:showAsAction="ifRoom"/>
|
||||
</menu>
|
||||
|
||||
@@ -1,14 +1,30 @@
|
||||
<menu
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:material="http://schemas.android.com/apk/res-auto">
|
||||
<item
|
||||
android:id="@+id/add_to_history"
|
||||
android:title="@string/add_to_history"
|
||||
android:icon="@drawable/ic_action_history_add"
|
||||
material:showAsAction="ifRoom"/>
|
||||
<item
|
||||
android:id="@+id/copy_to_clipboard"
|
||||
android:title="@string/copy_to_clipboard"
|
||||
android:icon="@drawable/ic_action_copy"
|
||||
material:showAsAction="ifRoom"/>
|
||||
<item
|
||||
android:id="@+id/copy_as_deeplink"
|
||||
android:title="@string/copy_as_deeplink"
|
||||
android:icon="@drawable/ic_action_link"
|
||||
material:showAsAction="ifRoom"/>
|
||||
<item
|
||||
android:id="@+id/export_to_file"
|
||||
android:title="@string/export_to_file"
|
||||
android:icon="@drawable/ic_action_save"
|
||||
material:showAsAction="ifRoom"/>
|
||||
<item
|
||||
android:id="@+id/brighten_screen"
|
||||
android:title="@string/brighten_screen"
|
||||
android:icon="@drawable/ic_action_bright"
|
||||
android:checkable="true"
|
||||
material:showAsAction="ifRoom"/>
|
||||
</menu>
|
||||
|
||||
@@ -18,6 +18,11 @@
|
||||
android:title="@string/copy_to_clipboard"
|
||||
android:icon="@drawable/ic_action_copy"
|
||||
material:showAsAction="ifRoom"/>
|
||||
<item
|
||||
android:id="@+id/copy_as_deeplink"
|
||||
android:title="@string/copy_as_deeplink"
|
||||
android:icon="@drawable/ic_action_link"
|
||||
material:showAsAction="ifRoom"/>
|
||||
<item
|
||||
android:id="@+id/share"
|
||||
android:title="@string/share"
|
||||
@@ -26,7 +31,7 @@
|
||||
<item
|
||||
android:id="@+id/share_file"
|
||||
android:title="@string/share_file"
|
||||
android:icon="@drawable/ic_action_share"
|
||||
android:icon="@drawable/ic_action_pick_file"
|
||||
material:showAsAction="ifRoom"/>
|
||||
<item
|
||||
android:id="@+id/create"
|
||||
|
||||
@@ -1,23 +1,25 @@
|
||||
<menu
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:material="http://schemas.android.com/apk/res-auto">
|
||||
<group android:id="@+id/scans_available"
|
||||
android:visible="false">
|
||||
<item
|
||||
android:id="@+id/search"
|
||||
android:icon="@drawable/ic_action_search"
|
||||
android:title="@string/search_scan"
|
||||
material:actionViewClass="android.support.v7.widget.SearchView"
|
||||
material:showAsAction="collapseActionView|always"/>
|
||||
<item
|
||||
android:id="@+id/export_history"
|
||||
android:icon="@drawable/ic_action_export"
|
||||
android:title="@string/export_to_file"
|
||||
material:showAsAction="ifRoom"/>
|
||||
<item
|
||||
android:id="@+id/clear"
|
||||
android:icon="@drawable/ic_action_remove"
|
||||
android:title="@string/clear_history"
|
||||
material:showAsAction="ifRoom"/>
|
||||
</group>
|
||||
<item
|
||||
android:id="@+id/search"
|
||||
android:icon="@drawable/ic_action_search"
|
||||
android:title="@string/search_scan"
|
||||
material:actionViewClass="android.support.v7.widget.SearchView"
|
||||
material:showAsAction="collapseActionView|always"/>
|
||||
<item
|
||||
android:id="@+id/export_history"
|
||||
android:icon="@drawable/ic_action_export"
|
||||
android:title="@string/export_to_file"
|
||||
material:showAsAction="ifRoom"/>
|
||||
<item
|
||||
android:id="@+id/share_as_file"
|
||||
android:icon="@drawable/ic_action_share"
|
||||
android:title="@string/share_file"
|
||||
material:showAsAction="ifRoom"/>
|
||||
<item
|
||||
android:id="@+id/clear"
|
||||
android:icon="@drawable/ic_action_remove"
|
||||
android:title="@string/clear_history"
|
||||
material:showAsAction="ifRoom"/>
|
||||
</menu>
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
android:icon="@drawable/ic_action_copy"
|
||||
material:showAsAction="ifRoom"/>
|
||||
<item
|
||||
android:id="@+id/edit_scan"
|
||||
android:title="@string/edit_scan"
|
||||
android:id="@+id/edit_label"
|
||||
android:title="@string/edit_label"
|
||||
android:icon="@drawable/ic_action_edit"
|
||||
material:showAsAction="ifRoom"/>
|
||||
<item
|
||||
|
||||
@@ -0,0 +1,277 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<string name="welcome">مرحبًا!</string>
|
||||
<string name="mode_expert">متقدم</string>
|
||||
<string name="mode_expert_description">أود التحقق من المحتوى المقروء وفتحه يدويًا.</string>
|
||||
<string name="mode_simple">بسيط</string>
|
||||
<string name="mode_simple_description">أريد فقط قراءة الباركود وفتحه فورًا.</string>
|
||||
<string name="camera_error">يتعذر الوصول إلى الكاميرا، يرجى المحاولة مرة أخرى</string>
|
||||
<string name="scan_code">مسح الرمز</string>
|
||||
<string name="compose_barcode">إنشاء رمز Bardcode</string>
|
||||
<string name="decode_barcode">فك رمز Barcode</string>
|
||||
<string name="content">المحتوى</string>
|
||||
<string name="binary_data">(بيانات ثنائية)</string>
|
||||
<plurals name="barcode_info">
|
||||
<item quantity="zero">%2$d حروف، %1$s</item>
|
||||
<item quantity="one">حرف واحد، %1$s</item>
|
||||
<item quantity="two">حرفين، %1$s</item>
|
||||
<item quantity="few">%2$d حروف، %1$s</item>
|
||||
<item quantity="many">%2$d حرف، %1$s</item>
|
||||
<item quantity="other">%2$d حرف، %1$s</item>
|
||||
</plurals>
|
||||
<string name="error_correction_level">مستوى تصحيح الأخطاء</string>
|
||||
<string name="error_correction_level_l" formatted="false">منخفض (~7% تصحيح)</string>
|
||||
<string name="error_correction_level_m" formatted="false">متوسط (~15% تصحيح)</string>
|
||||
<string name="error_correction_level_q" formatted="false">ربع (~25% تصحيح)</string>
|
||||
<string name="error_correction_level_h" formatted="false">مرتفع (~30% تصحيح)</string>
|
||||
<string name="label">العنوان</string>
|
||||
<string name="colors">المقدمة والخلفية</string>
|
||||
<string name="colors_black_on_white">أسود على خلفية بيضاء</string>
|
||||
<string name="colors_white_on_black">أبيض على خلفية سوداء</string>
|
||||
<string name="colors_black_on_transparent">أسود على خلفية شفافة</string>
|
||||
<string name="colors_white_on_transparent">أبيض على خلفية شفافة</string>
|
||||
<string name="sequence_size">حجم التسلسل</string>
|
||||
<string name="sequence_index">رقم التسلسل</string>
|
||||
<string name="sequence_id">معرف التسلسل</string>
|
||||
<string name="gtin_issue_number">رقم الإصدار</string>
|
||||
<string name="gtin_country">بلد التصنيع المحتمل</string>
|
||||
<string name="gtin_price">السعر المقترح</string>
|
||||
<string name="gtin_add_on">إضافة UPC EAN</string>
|
||||
<string name="barcode_version_number">الإصدار</string>
|
||||
<string name="qr_version_and_modules" tools:ignore="PluralsCandidate">%1$s (%2$d وحدات)</string>
|
||||
<string name="qr_data_mask">قناع البيانات</string>
|
||||
<string name="toggle_flash">تشغيل/إيقاف الوميض</string>
|
||||
<string name="error_flash">خطأ في تشغيل الوميض</string>
|
||||
<string name="share">مشاركة</string>
|
||||
<string name="share_file">مشاركة كملف</string>
|
||||
<string name="share_as">مشاركة كـ؟</string>
|
||||
<string name="copy_to_clipboard">نسخ إلى الحافظة</string>
|
||||
<string name="copy_as_deeplink">نسخ كرابط عميق</string>
|
||||
<string name="copied_to_clipboard">تم النسخ إلى الحافظة</string>
|
||||
<string name="copy_password">نسخ كلمة المرور إلى الحافظة</string>
|
||||
<string name="open_url">فتح الرابط</string>
|
||||
<string name="cannot_resolve_action">لم يتم العثور على تطبيق يمكنه فتح ذلك</string>
|
||||
<string name="pick_search_engine">اختر محرك البحث</string>
|
||||
<string name="format">التنسيق</string>
|
||||
<string name="size_width_by_height">الحجم بالبكسل: %1$d×%2$d</string>
|
||||
<string name="size_no_magnification">بدون تكبير</string>
|
||||
<string name="add_quiet_zone">إضافة منطقة هادئة</string>
|
||||
<string name="input_content_here">أدخل المحتوى هنا</string>
|
||||
<string name="unescape">توسيع رموز الهروب (escape sequences)</string>
|
||||
<string name="encode">ترميز</string>
|
||||
<string name="error_no_content">المحتوى مفقود</string>
|
||||
<string name="error_encoding_barcode">لا يمكن إنشاء رمز Barcode</string>
|
||||
<string name="view_barcode">عرض رمز Barcode</string>
|
||||
<string name="info">معلومات</string>
|
||||
<string name="switch_camera">تبديل الكاميرا</string>
|
||||
<string name="bulk_mode">المسح بشكل مستمر</string>
|
||||
<string name="bulk_mode_summary">ابدأ المسح دائمًا بشكل مستمر.</string>
|
||||
<string name="bulk_mode_delay">الفارق الزمني بين عمليات المسح المستمرة</string>
|
||||
<string name="restrict_format">تحديد التنسيق</string>
|
||||
<string name="remove_restriction">إزالة التحديد</string>
|
||||
<string name="find_code">العثور على الرمز</string>
|
||||
<string name="find_code_with_content">ما المحتوى؟</string>
|
||||
<string name="find_code_hint">مصطلح أو تعبير عادي</string>
|
||||
<string name="does_not_match_search_term">لا يطابق مصطلح البحث</string>
|
||||
<string name="scan_for">مسح %s</string>
|
||||
<string name="quarter_second">ربع ثانية</string>
|
||||
<string name="half_second">نصف ثانية</string>
|
||||
<string name="a_second">ثانية واحدة</string>
|
||||
<string name="two_seconds">ثانيتان</string>
|
||||
<string name="five_seconds">خمس ثوانٍ</string>
|
||||
<string name="history">التاريخ</string>
|
||||
<string name="preferences">الإعدادات</string>
|
||||
<string name="scan_category">المسح</string>
|
||||
<string name="content_category">المحتوى</string>
|
||||
<string name="locale_category">اللغة</string>
|
||||
<string name="profile_category">الملف الشخصي</string>
|
||||
<string name="profile">الملف الشخصي</string>
|
||||
<string name="profile_default">افتراضي</string>
|
||||
<string name="profile_add">إضافة ملف شخصي جديد</string>
|
||||
<string name="profile_enter_name">أدخل اسم الملف الشخصي</string>
|
||||
<string name="profile_enter_name_hint">الاسم</string>
|
||||
<string name="profile_invalid_name">اسم الملف الشخصي غير صالح</string>
|
||||
<string name="profile_empty">لا توجد ملفات شخصية بعد</string>
|
||||
<string name="profile_current">الملف الشخصي الحالي</string>
|
||||
<string name="profile_remove_confirm">إزالة هذا الملف الشخصي؟</string>
|
||||
<string name="current_profile">الملف الشخصي: %s</string>
|
||||
<string name="custom_locale">تغيير اللغة</string>
|
||||
<string name="follow_system_settings">اتبع إعدادات النظام</string>
|
||||
<string name="show_crop_handle">عرض محدد الاقتصاص</string>
|
||||
<string name="show_crop_handle_summary">حصر المسح في منطقة قابلة للتخصيص.</string>
|
||||
<string name="barcode_formats">تنسيقات Barcode</string>
|
||||
<string name="zoom_by_swiping">مرر لأعلى أو لأسفل للتكبير/التصغير</string>
|
||||
<string name="zoom_by_swiping_summary">مرر لأعلى أو لأسفل على واجهة الكاميرا للتحكم في التكبير/التصغير لها.</string>
|
||||
<string name="auto_rotate">التعرف على رموز Barcode العمودية أحادية الأبعاد</string>
|
||||
<string name="auto_rotate_summary">تدوير إطار الكاميرا تلقائيًا للتعرف على رموز Barcode العمودية أحادية الأبعاد. بناءً على الجهاز المستخدم، قد يؤثر ذلك على الأداء.</string>
|
||||
<string name="try_harder">تحسين دقة القارئ</string>
|
||||
<string name="try_harder_summary">قم بتمكين هذا الخيار للرموز التي يصعب قراءتها. سيؤثر على الأداء.</string>
|
||||
<string name="show_toast_in_bulk_mode">عرض البيانات في الوضع المستمر</string>
|
||||
<string name="show_toast_in_bulk_mode_summary">عرض سريع للبيانات الممسوحة عند المسح المستمر.</string>
|
||||
<string name="vibrate">الاهتزاز عند الكشف</string>
|
||||
<string name="vibrate_summary">قم بتمكين هذا إذا كنت تريد أن يهتز جهازك عندما يتم التعرف على رمز.</string>
|
||||
<string name="beep">صوت صفير عند الكشف</string>
|
||||
<string name="beep_summary">قم بتمكين هذا إذا كنت تريد أن يصدر جهازك صوتًا عند التعرف على رمز.</string>
|
||||
<string name="beep_tone">نغمة الصفير</string>
|
||||
<string name="tone_cdma_confirm">تأكيد CDMA</string>
|
||||
<string name="tone_sup_confirm">تأكيد الإشراف على الاتصال</string>
|
||||
<string name="tone_sup_radio_ack">إقرار مسار الراديو الإشرافي على الاتصال</string>
|
||||
<string name="tone_prop_ack">إقرار إيجابي</string>
|
||||
<string name="tone_prop_beep">صوت صفير عام</string>
|
||||
<string name="tone_prop_beep2">صوت صفير مزدوج عام</string>
|
||||
<string name="use_history">حفظ تاريخ المسح</string>
|
||||
<string name="use_history_summary">حفظ الرموز المتعرف عليها على جهازك.</string>
|
||||
<string name="ignore_duplicates">تجاهل النسخ المكررة</string>
|
||||
<string name="ignore_consecutive_duplicates">النسخ المكررة المتتالية</string>
|
||||
<string name="ignore_any_duplicates">أي نسخ مكررة</string>
|
||||
<string name="accept_duplicates">قبول النسخ المكررة</string>
|
||||
<string name="open_immediately">فتح مباشرةً</string>
|
||||
<string name="open_immediately_summary">تخطي الفحص وفتح المحتويات على الفور. قد يتم فتح محتويات ضارة إذا تم تمكينه.</string>
|
||||
<string name="copy_immediately">نسخ تلقائي إلى الحافظة</string>
|
||||
<string name="copy_immediately_summary">نسخ المحتويات الممسوحة إلى الحافظة تلقائيًا. يرجى ملاحظة أن التطبيقات الأخرى قد تتطفل على الحافظة الخاصة بك في الخلفية.</string>
|
||||
<string name="show_meta_data">عرض البيانات الوصفية</string>
|
||||
<string name="show_meta_data_summary">عرض بيانات إضافية حول رمز Barcode الممسوح.</string>
|
||||
<string name="show_hex_dump">عرض التفريغ سداسي عشري</string>
|
||||
<string name="show_hex_dump_summary">عرض تفريغ سداسي عشري للمحتويات الممسوحة.</string>
|
||||
<string name="show_checksum">عرض الـ checksum</string>
|
||||
<string name="no_checksum">الـ checksum غير متوفر</string>
|
||||
<string name="show_recreation">إظهار رمز Barcode المعاد إنشاؤه</string>
|
||||
<string name="show_recreation_summary">إعادة إنشاء وإظهار رمز Barcode من المحتوى الذي تمت قراءته إن أمكن.</string>
|
||||
<string name="close_automatically">العودة للخلف بعد النسخ أو المشاركة</string>
|
||||
<string name="close_automatically_summary">العودة تلقائيًا إلى شاشة المسح بعد نسخ أو مشاركة المحتويات المقروءة.</string>
|
||||
<string name="default_search_engine">فتح البيانات المجهولة في</string>
|
||||
<string name="always_ask">اسأل دائمًا</string>
|
||||
<string name="open_with_url">فتح البيانات المجهولة باستخدام رابط</string>
|
||||
|
||||
<string name="automated_actions">الإجراءات الآلية</string>
|
||||
<string name="automated_actions_none">لا توجد إجراءات آلية</string>
|
||||
<string name="automated_actions_empty">لا توجد إجراءات آلية بعد</string>
|
||||
<plurals name="automated_actions_count">
|
||||
<item quantity="zero">%d إجراء آلي</item>
|
||||
<item quantity="one">%d إجراء آلي</item>
|
||||
<item quantity="two">%d إجراءان آليان</item>
|
||||
<item quantity="few">%d إجراءات آلية</item>
|
||||
<item quantity="many">%d إجراءً آليًا</item>
|
||||
<item quantity="other">%d إجراءات آلية</item>
|
||||
</plurals>
|
||||
<string name="automated_action_add">إضافة إجراء</string>
|
||||
<string name="automated_action_edit">تعديل الإجراء</string>
|
||||
<string name="automated_action_regex">تعبير نمطي</string>
|
||||
<string name="automated_action_regex_hint">مثل ^https?://.*</string>
|
||||
<string name="automated_action_regex_required">التعبير النمطي مطلوب</string>
|
||||
<string name="automated_action_regex_invalid">تعبير نمطي غير صالح</string>
|
||||
<string name="automated_action_type">نوع الإجراء</string>
|
||||
<string name="automated_action_type_intent">فتح URL</string>
|
||||
<string name="automated_action_url_template">قالب URL</string>
|
||||
<string name="automated_action_url_hint">https://example.com/?q={RESULT}</string>
|
||||
<string name="automated_action_url_required">قالب URL مطلوب</string>
|
||||
<string name="automated_action_invalid_url">قالب URL غير صالح</string>
|
||||
<string name="automated_action_remove_confirm">إزالة هذا الإجراء؟</string>
|
||||
<string name="automated_action_list_intent">فتح URL: %s</string>
|
||||
<string name="automated_action_list_intent_empty">فتح URL</string>
|
||||
<string name="automated_action_webview_title">إجراء آلي</string>
|
||||
<string name="clear_network_suggestions">إزالة الشبكات المقترحة</string>
|
||||
<string name="clear_network_suggestions_summary">إزالة الشبكات المقترحة التي تمت إضافتها مسبقًا من خلال هذا التطبيق.</string>
|
||||
<string name="really_remove_all_networks">هل تريد حقًا إزالة جميع الشبكات؟</string>
|
||||
<string name="clear_network_suggestions_success">تم إزالة الشبكات المقترحة</string>
|
||||
<string name="clear_network_suggestions_nothing_to_remove">لا يوجد أي شبكات المقترحة</string>
|
||||
<string name="send_category">إعادة التوجيه</string>
|
||||
<string name="send_scan_active">إعادة توجيه عمليات المسح</string>
|
||||
<string name="send_scan_active_summary">قم بالتمكين لإعادة توجيه عمليات المسح إلى رابط معين.</string>
|
||||
<string name="send_scan_url">الرابط المراد إعادة التوجيه إليه</string>
|
||||
<string name="send_scan_type">نوع الطلب</string>
|
||||
<string name="send_type_get_add_content">نوع GET وأضفه ببساطة</string>
|
||||
<string name="send_type_get_query_string">نوع GET مع سلسلة الاستعلام الكاملة</string>
|
||||
<string name="send_type_post_form">POST application/x-www-form-urlencoded</string>
|
||||
<string name="send_type_post_json">POST application/json</string>
|
||||
<string name="send_type_external_browser">فتح في متصفح خارجي</string>
|
||||
<string name="test_url">رابط للاختبار</string>
|
||||
<string name="send_scan_bluetooth">إرسال عمليات المسح عبر بلوتوث</string>
|
||||
<string name="send_scan_bluetooth_summary">تمكين إرسال عمليات المسح إلى مضيف بلوتوث معين.</string>
|
||||
<string name="send_scan_bluetooth_host">المضيف</string>
|
||||
<string name="no_bluetooth_hosts_paired">لا يوجد اقتران مع مضيفين</string>
|
||||
<string name="bluetooth_connect_fail">تعذر الاتصال بجهاز بلوتوث.</string>
|
||||
<string name="bluetooth_send_fail">تعذر الإرسال إلى جهاز بلوتوث.</string>
|
||||
<string name="bluetooth_send_success">نجح الإرسال إلى جهاز بلوتوث.</string>
|
||||
<string name="generation_category">التوليد</string>
|
||||
<string name="brighten_screen">شاشة ساطعة</string>
|
||||
<string name="brighten_screen_summary">تعيين مستوى السطوع إلى الحد الأقصى لتحسين مسح الرمز من الشاشة.</string>
|
||||
<string name="really_remove_scan">هل تريد حقًا إزالة المسح؟</string>
|
||||
<string name="really_remove_all_scans">هل تريد حقًا إزالة كافة عمليات المسح؟</string>
|
||||
<string name="really_remove_selected_scans">هل تريد حقًا إزالة عمليات المسح المحددة؟</string>
|
||||
<string name="clear_history">مسح التاريخ</string>
|
||||
<string name="copy_scan">نسخ عملية المسح</string>
|
||||
<string name="edit_label">تعديل العنوان</string>
|
||||
<string name="remove_scan">حذف عملية المسح</string>
|
||||
<string name="enter_name">أدخل اسمًا لوصف عملية المسح</string>
|
||||
<string name="enter_name_hint">اسم عملية المسح</string>
|
||||
<string name="no_barcode_found">لا يوجد أي رمز Barcode</string>
|
||||
<string name="pick_list_separator">الفاصل المستخدم لفصل عناصر القائمة؟</string>
|
||||
<string name="separator_line_break">خط فاصل</string>
|
||||
<string name="separator_ruler">المسطرة</string>
|
||||
<string name="save_as_file_name">حفظ في مجلد التنزيلات؟</string>
|
||||
<string name="file_name">اسم الملف</string>
|
||||
<string name="error_saving_file">يتعذر حفظ الملف</string>
|
||||
<string name="error_file_exists">الملف موجود بالفعل</string>
|
||||
<string name="connect_to_wifi">الاتصال بشبكة Wi-Fi</string>
|
||||
<string name="wifi_config_failed">تعذر تهيئة شبكة Wi-Fi</string>
|
||||
<string name="wifi_added">تمت إضافة شبكة Wi-Fi</string>
|
||||
<string name="sms_send">إرسال رسالة نصية قصيرة</string>
|
||||
<string name="sms_error">تعذر إرسال رسالة نصية قصيرة</string>
|
||||
<string name="tel_dial">الاتصال بالرقم</string>
|
||||
<string name="tel_error">تعذر الاتصال بالرقم</string>
|
||||
<string name="mail_send">إرسال بريد إلكتروني</string>
|
||||
<string name="mail_error">تعذر إرسال البريد الإلكتروني</string>
|
||||
<string name="vcard_add">إضافة إلى جهات الاتصال</string>
|
||||
<string name="vcard_failed">تعذرت الإضافة إلى جهات الاتصال</string>
|
||||
<string name="vevent_add">إضافة إلى التقويم</string>
|
||||
<string name="vevent_failed">تعذرت الإضافة إلى التقويم</string>
|
||||
<string name="otpauth_add">أضف المصادقة الثنائية (2FA)</string>
|
||||
<string name="search_web">البحث عبر الإنترنت</string>
|
||||
<string name="search_scan">البحث عن عملية مسح</string>
|
||||
<string name="add_to_history">إضافة إلى التاريخ</string>
|
||||
<string name="added_to_history">تمت الإضافة إلى التاريخ</string>
|
||||
<string name="export_to_file">تصدير إلى ملف</string>
|
||||
<string name="export_as">تصدير كـ؟</string>
|
||||
<string name="export_csv_comma">ملف CSV مع فواصل</string>
|
||||
<string name="export_csv_semicolon">ملف CSV مع فواصل منقوطة</string>
|
||||
<string name="export_json">ملف JSON</string>
|
||||
<string name="export_database">قاعدة بيانات SQLite</string>
|
||||
<string name="saved_in_downloads">تم الحفظ في مجلد التنزيلات</string>
|
||||
<string name="rotate_image_cw">تدوير الصورة باتجاه عقارب الساعة</string>
|
||||
<string name="toggle_free_rotation">تبديل التدوير الحر</string>
|
||||
<string name="pick_file">اختر ملف</string>
|
||||
<string name="pick_image_file">اختر صورة</string>
|
||||
<string name="pick_code_to_scan">اختر رمزًا لمسحه</string>
|
||||
<string name="shortcut_decode">مسح رمز Barcode</string>
|
||||
<string name="shortcut_encode">إنشاء رمز Barcode</string>
|
||||
<string name="shortcut_history">التاريخ</string>
|
||||
<string name="shortcut_preferences">الإعدادات</string>
|
||||
<string name="background_request_failed">فشل الطلب للعمل في الخلفية</string>
|
||||
<string name="entry_type">النوع</string>
|
||||
<string name="wifi_network">شبكة Wi-Fi</string>
|
||||
<string name="wifi_ssid">الاسم</string>
|
||||
<string name="wifi_type">نوع المصادقة</string>
|
||||
<string name="wifi_password">كلمة المرور</string>
|
||||
<string name="wifi_hidden">شبكة مخفية</string>
|
||||
<string name="wifi_eap">طريقة EAP</string>
|
||||
<string name="wifi_anonymous_identity">هوية مجهولة</string>
|
||||
<string name="wifi_identity">الهوية</string>
|
||||
<string name="wifi_phase2">طريقة Phase 2</string>
|
||||
<string name="network_suggestions">الشبكات المقترحة</string>
|
||||
<string name="remove_suggestion">إزالة الاقتراحات</string>
|
||||
<string name="email_to">المستلم</string>
|
||||
<string name="email_subject">الموضوع</string>
|
||||
<string name="email_body">النص</string>
|
||||
<string name="scheme">المخطط</string>
|
||||
<string name="host">المضيف</string>
|
||||
<string name="query">استعلام</string>
|
||||
<string name="automated_action_type_custom_intent">Intent مخصص</string>
|
||||
<string name="automated_action_intent_uri_template">قالب عنوان Intent</string>
|
||||
<string name="automated_action_intent_uri_hint">intent:#Intent;action=android.intent.action.SEND;type=text/plain;S.android.intent.extra.TEXT={RESULT};end</string>
|
||||
<string name="automated_action_intent_uri_required">عنوان Intent مطلوب</string>
|
||||
<string name="automated_action_intent_uri_invalid">عنوان Intent غير صالح</string>
|
||||
<string name="automated_action_list_custom_intent">Intent مخصص: %s</string>
|
||||
<string name="automated_action_list_custom_intent_empty">Intent مخصص</string>
|
||||
|
||||
</resources>
|
||||
@@ -1,4 +1,9 @@
|
||||
<resources>
|
||||
<string name="welcome">Добре дошли!</string>
|
||||
<string name="mode_expert">Разширен</string>
|
||||
<string name="mode_expert_description">Искам да проверя прочетеното съдържание и да го отворя ръчно.</string>
|
||||
<string name="mode_simple">Опростен</string>
|
||||
<string name="mode_simple_description">Искам просто да сканирам баркодове и да ги отварям веднага.</string>
|
||||
<string name="camera_error">Няма достъп до камерата, моля, опитайте отново</string>
|
||||
<string name="scan_code">Сканирайте код</string>
|
||||
<string name="compose_barcode">Създаване на баркод</string>
|
||||
@@ -14,6 +19,7 @@
|
||||
<string name="error_correction_level_m" formatted="false">Средно (~15% корекция)</string>
|
||||
<string name="error_correction_level_q" formatted="false">Четвъртично (~25% корекция)</string>
|
||||
<string name="error_correction_level_h" formatted="false">Високо (~30% корекция)</string>
|
||||
<string name="label">Етикет</string>
|
||||
<string name="colors">Преден и заден план</string>
|
||||
<string name="colors_black_on_white">Черно на бяло</string>
|
||||
<string name="colors_white_on_black">Бяло на черно</string>
|
||||
@@ -28,21 +34,23 @@
|
||||
<string name="gtin_add_on">UPC EAN разширение</string>
|
||||
<string name="barcode_version_number">Версия</string>
|
||||
<string name="qr_version_and_modules">%1$s (%2$d модули)</string>
|
||||
<string name="qr_data_mask">Маска за данни</string>
|
||||
<string name="toggle_flash">Превключване на светкавицата</string>
|
||||
<string name="error_flash">Грешка при превключването на светкавицата</string>
|
||||
<string name="share">Сподели</string>
|
||||
<string name="share_file">Share as file</string>
|
||||
<string name="share_file">Споделяне като файл</string>
|
||||
<string name="share_as">Сподели като?</string>
|
||||
<string name="copy_to_clipboard">Копиране в клипборда</string>
|
||||
<string name="copy_as_deeplink">Копиране като дийп линк</string>
|
||||
<string name="copied_to_clipboard">Копирано в клипборда</string>
|
||||
<string name="copy_password">Копиране на паролата в клипборда</string>
|
||||
<string name="copied_password_to_clipboard">Паролата е копирана в клипборда</string>
|
||||
<string name="open_url">Отворете URL(Линк)</string>
|
||||
<string name="cannot_resolve_action">Няма приложение което може да отвори това</string>
|
||||
<string name="pick_search_engine">Изберете търсачка</string>
|
||||
<string name="format">Формат</string>
|
||||
<string name="size_width_by_height">Размер в пиксели: %1$d×%2$d</string>
|
||||
<string name="margin_size">Марж в пиксели: %1$d</string>
|
||||
<string name="size_no_magnification">Без увеличение</string>
|
||||
<string name="add_quiet_zone">Добавяне на тиха зона</string>
|
||||
<string name="input_content_here">Въведете съдържание тук</string>
|
||||
<string name="unescape">Разширете последователности за извеждане</string>
|
||||
<string name="encode">КОДИРАНЕ</string>
|
||||
@@ -56,7 +64,11 @@
|
||||
<string name="bulk_mode_delay">Забавяне между непрекъснатите сканирания</string>
|
||||
<string name="restrict_format">Ограничаване на формат</string>
|
||||
<string name="remove_restriction">Премахване на ограничението</string>
|
||||
<string name="scan_format">Сканиране %s</string>
|
||||
<string name="find_code">Намерете кода</string>
|
||||
<string name="find_code_with_content">Какво съдържание?</string>
|
||||
<string name="find_code_hint">Термин или регулярен израз</string>
|
||||
<string name="does_not_match_search_term">Не съответства на търсения термин</string>
|
||||
<string name="scan_for">Сканиране %s</string>
|
||||
<string name="quarter_second">Червърт секунда</string>
|
||||
<string name="half_second">Половин секунда</string>
|
||||
<string name="a_second">Една секунда</string>
|
||||
@@ -67,6 +79,17 @@
|
||||
<string name="scan_category">Сканиране</string>
|
||||
<string name="content_category">Съдържание</string>
|
||||
<string name="locale_category">Език</string>
|
||||
<string name="profile_category">Профил</string>
|
||||
<string name="profile">Профил</string>
|
||||
<string name="profile_default">По подразбиране</string>
|
||||
<string name="profile_add">Добавяне на нов профил</string>
|
||||
<string name="profile_enter_name">Въведете име на профила</string>
|
||||
<string name="profile_enter_name_hint">Име</string>
|
||||
<string name="profile_invalid_name">Невалидно име на профила</string>
|
||||
<string name="profile_empty">Все още няма профили</string>
|
||||
<string name="profile_current">Текущ профил</string>
|
||||
<string name="profile_remove_confirm">Премахване на този профил?</string>
|
||||
<string name="current_profile">Профил: %s</string>
|
||||
<string name="custom_locale">Персонализиран език</string>
|
||||
<string name="follow_system_settings">Следване на системните настройки</string>
|
||||
<string name="show_crop_handle">Показване на ограничителя за изрязване</string>
|
||||
@@ -93,8 +116,10 @@
|
||||
<string name="tone_prop_beep2">Общ двоен звуков сигнал</string>
|
||||
<string name="use_history">Хронология на сканирането</string>
|
||||
<string name="use_history_summary">Запазва разпознатите кодове на вашето устройство.</string>
|
||||
<string name="ignore_duplicates">Игнорирайте дубликати</string>
|
||||
<string name="ignore_consecutive_duplicates">Игнориране на дубликатите</string>
|
||||
<string name="ignore_consecutive_duplicates_summary">Не запазва последователни дубликати в хронологията на сканирането.</string>
|
||||
<string name="ignore_any_duplicates">Ignore any duplicates</string>
|
||||
<string name="accept_duplicates">Приемане на дубликати</string>
|
||||
<string name="open_immediately">Отваряй внезапно</string>
|
||||
<string name="open_immediately_summary">Пропуска проверката и внезапно отваря съдържанието. Може да отвори вредно съдържание, ако е активирано.</string>
|
||||
<string name="copy_immediately">Поставяй в клипборда</string>
|
||||
@@ -103,6 +128,8 @@
|
||||
<string name="show_meta_data_summary">Показване на допълнителни данни за сканирания баркод.</string>
|
||||
<string name="show_hex_dump">Показване на шестнадесетичен дъмп</string>
|
||||
<string name="show_hex_dump_summary">Показване на шестнадесетичен дъмп на сканираното съдържание.</string>
|
||||
<string name="show_checksum">Покажи контролна сума</string>
|
||||
<string name="no_checksum">Без контролна сума</string>
|
||||
<string name="show_recreation">Показване на пресъздаден баркод</string>
|
||||
<string name="show_recreation_summary">Създай и покажи баркод отново от прочетеното съдържание, ако е възможно.</string>
|
||||
<string name="close_automatically">Върщане се след Копиране/Споделяне</string>
|
||||
@@ -110,6 +137,30 @@
|
||||
<string name="default_search_engine">Отворяй неизвестни данни в</string>
|
||||
<string name="always_ask">Винаги питай</string>
|
||||
<string name="open_with_url">Отворяне на неизвестни данни с URL</string>
|
||||
|
||||
<string name="automated_actions">Автоматизирани действия</string>
|
||||
<string name="automated_actions_none">Няма автоматизирани действия</string>
|
||||
<string name="automated_actions_empty">Все още няма автоматизирани действия</string>
|
||||
<plurals name="automated_actions_count">
|
||||
<item quantity="one">%d автоматизирано действие</item>
|
||||
<item quantity="other">%d автоматизирани действия</item>
|
||||
</plurals>
|
||||
<string name="automated_action_add">Добавяне на действие</string>
|
||||
<string name="automated_action_edit">Редактиране на действие</string>
|
||||
<string name="automated_action_regex">Регулярен израз</string>
|
||||
<string name="automated_action_regex_hint">напр. ^https?://.*</string>
|
||||
<string name="automated_action_regex_required">Регулярен израз е задължителен</string>
|
||||
<string name="automated_action_regex_invalid">Невалиден регулярен израз</string>
|
||||
<string name="automated_action_type">Тип действие</string>
|
||||
<string name="automated_action_type_intent">Отваряне на URL</string>
|
||||
<string name="automated_action_url_template">Шаблон на URL</string>
|
||||
<string name="automated_action_url_hint">https://example.com/?q={RESULT}</string>
|
||||
<string name="automated_action_url_required">Шаблон на URL е задължителен</string>
|
||||
<string name="automated_action_invalid_url">Невалиден шаблон на URL</string>
|
||||
<string name="automated_action_remove_confirm">Да се премахне ли това действие?</string>
|
||||
<string name="automated_action_list_intent">Отваряне на URL: %s</string>
|
||||
<string name="automated_action_list_intent_empty">Отваряне на URL</string>
|
||||
<string name="automated_action_webview_title">Автоматизирано действие</string>
|
||||
<string name="clear_network_suggestions">Премахнете мрежовите предложения</string>
|
||||
<string name="clear_network_suggestions_summary">Премахване на мрежовите предложения, които преди са били добавени с това приложение.</string>
|
||||
<string name="really_remove_all_networks">Наистина ли искате да премахнете всички мрежи?</string>
|
||||
@@ -133,12 +184,15 @@
|
||||
<string name="bluetooth_connect_fail">Неуспешно свързване с Bluetooth устройство.</string>
|
||||
<string name="bluetooth_send_fail">Неуспешно изпращане до Bluetooth устройство.</string>
|
||||
<string name="bluetooth_send_success">Успешно Bluetooth изпращане.</string>
|
||||
<string name="generation_category">Поколение</string>
|
||||
<string name="brighten_screen">Подсвети екрана</string>
|
||||
<string name="brighten_screen_summary">Задайте яркостта на максимум, за да подобрите сканирането на баркод от екрана.</string>
|
||||
<string name="really_remove_scan">Наистина ли искате да премахнете това сканиране?</string>
|
||||
<string name="really_remove_all_scans">Наистина ли искате да премахнете всички сканирания?</string>
|
||||
<string name="really_remove_selected_scans">Наистина ли искате да премахнете избраните сканирания?</string>
|
||||
<string name="clear_history">Изчистване на историята</string>
|
||||
<string name="copy_scan">Сопиране на сканирането</string>
|
||||
<string name="edit_scan">Редактиране на етикет</string>
|
||||
<string name="edit_label">Редактиране на етикет</string>
|
||||
<string name="remove_scan">Премахване на сканиране</string>
|
||||
<string name="enter_name">Въведете име, за да опишете сканирането</string>
|
||||
<string name="enter_name_hint">Име на сканирането</string>
|
||||
@@ -166,6 +220,8 @@
|
||||
<string name="otpauth_add">Добави 2FA</string>
|
||||
<string name="search_web">Търси в мрежата</string>
|
||||
<string name="search_scan">Търсещо сканиране</string>
|
||||
<string name="add_to_history">Добави към историята</string>
|
||||
<string name="added_to_history">Добавено към историята</string>
|
||||
<string name="export_to_file">Извеждане в файл</string>
|
||||
<string name="export_as">Извеждане като?</string>
|
||||
<string name="export_csv_comma">CSV файл със запетаи</string>
|
||||
@@ -180,6 +236,7 @@
|
||||
<string name="pick_code_to_scan">Изберете код за сканиране</string>
|
||||
<string name="shortcut_decode">Сканирай баркод</string>
|
||||
<string name="shortcut_encode">Създай баркод</string>
|
||||
<string name="shortcut_history">История</string>
|
||||
<string name="shortcut_preferences">Настройки</string>
|
||||
<string name="background_request_failed">Неуспешна заявка в фонов режим</string>
|
||||
<string name="entry_type">Тип</string>
|
||||
@@ -194,4 +251,18 @@
|
||||
<string name="wifi_phase2">Фаза 2 метод</string>
|
||||
<string name="network_suggestions">Предложения за интернет мрежа</string>
|
||||
<string name="remove_suggestion">Премахване на предложението</string>
|
||||
<string name="email_to">Получател</string>
|
||||
<string name="email_subject">Тема</string>
|
||||
<string name="email_body">Съдържание</string>
|
||||
<string name="scheme">Схема</string>
|
||||
<string name="host">Домакин</string>
|
||||
<string name="query">Заявка</string>
|
||||
<string name="automated_action_type_custom_intent">Персонализиран Intent</string>
|
||||
<string name="automated_action_intent_uri_template">Шаблон за Intent URI</string>
|
||||
<string name="automated_action_intent_uri_hint">intent:#Intent;action=android.intent.action.SEND;type=text/plain;S.android.intent.extra.TEXT={RESULT};end</string>
|
||||
<string name="automated_action_intent_uri_required">Intent URI е задължителен</string>
|
||||
<string name="automated_action_intent_uri_invalid">Невалиден Intent URI</string>
|
||||
<string name="automated_action_list_custom_intent">Персонализиран Intent: %s</string>
|
||||
<string name="automated_action_list_custom_intent_empty">Персонализиран Intent</string>
|
||||
|
||||
</resources>
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
<resources>
|
||||
<string name="welcome">স্বাগতম!</string>
|
||||
<string name="mode_expert">উন্নত</string>
|
||||
<string name="mode_expert_description">আমি পড়া বিষয়বস্তু যাচাই করতে চাই এবং এটি নিজে খুলতে চাই।</string>
|
||||
<string name="mode_simple">সহজ</string>
|
||||
<string name="mode_simple_description">আমি কেবল বারকোড স্ক্যান করতে চাই এবং তা সঙ্গে সঙ্গে খুলতে চাই।</string>
|
||||
<string name="camera_error">ক্যামেরা পাওয়া যাচ্ছে না, পরে চেষ্টা করো</string>
|
||||
<string name="scan_code">সংকেতের পাঠোদ্ধার</string>
|
||||
<string name="compose_barcode">বারকোড বানাও</string>
|
||||
@@ -14,37 +19,40 @@
|
||||
<string name="error_correction_level_m" formatted="false">মধ্যম (~১৫% ঠিককরণ)</string>
|
||||
<string name="error_correction_level_q" formatted="false">বেশি (~২৫% ঠিককরণ)</string>
|
||||
<string name="error_correction_level_h" formatted="false">অনেক বেশি (~৩০% ঠিককরণ)</string>
|
||||
<string name="colors">Fore and background</string>
|
||||
<string name="colors_black_on_white">Black on white</string>
|
||||
<string name="colors_white_on_black">White on black</string>
|
||||
<string name="colors_black_on_transparent">Black on transparent</string>
|
||||
<string name="colors_white_on_transparent">White on transparent</string>
|
||||
<string name="sequence_size">Sequence size</string>
|
||||
<string name="sequence_index">Sequence index</string>
|
||||
<string name="sequence_id">Sequence ID</string>
|
||||
<string name="label">লেবেল</string>
|
||||
<string name="colors">পূর্ববর্তী এবং সৃজনাধীন</string>
|
||||
<string name="colors_black_on_white">সাদা উপরে কালো</string>
|
||||
<string name="colors_white_on_black">কালো উপর সাদা</string>
|
||||
<string name="colors_black_on_transparent">সাদা সহজ পারদার উপর</string>
|
||||
<string name="colors_white_on_transparent">পারদার উপর সাদা</string>
|
||||
<string name="sequence_size">ধারার আকার</string>
|
||||
<string name="sequence_index">ক্রমিক সূচী</string>
|
||||
<string name="sequence_id">ক্রম আইডি</string>
|
||||
<string name="gtin_issue_number">সমস্যা ক্রম</string>
|
||||
<string name="gtin_country">তৈরির সম্ভাব্য দেশ</string>
|
||||
<string name="gtin_price">প্রস্তসবিত দাম</string>
|
||||
<string name="gtin_add_on">ইউপিসি ইএএন বর্ধিত</string>
|
||||
<string name="barcode_version_number">Version</string>
|
||||
<string name="barcode_version_number">সংস্করণ</string>
|
||||
<string name="qr_version_and_modules">%1$s (%2$d modules)</string>
|
||||
<string name="qr_data_mask">ডেটা মাস্ক</string>
|
||||
<string name="toggle_flash">আলো</string>
|
||||
<string name="error_flash">Error toggling flash</string>
|
||||
<string name="error_flash">ফ্ল্যাশ টগল করার সময় ত্রুটি</string>
|
||||
<string name="share">ভাগ করো</string>
|
||||
<string name="share_file">Share as file</string>
|
||||
<string name="share_file">ফাইল হিসেবে ভাগাভাগি</string>
|
||||
<string name="share_as">ভাগ করার মাধ্যম</string>
|
||||
<string name="copy_to_clipboard">ক্লিপবোর্ডে অনুলিপি</string>
|
||||
<string name="copy_as_deeplink">ডিপলিংক হিসেবে কপি করুন</string>
|
||||
<string name="copied_to_clipboard">ক্লিপবোর্ডে অনুলিপিত</string>
|
||||
<string name="copy_password">ক্লিপবোর্ডে পাসওয়ার্ড অনুলিপি করো</string>
|
||||
<string name="copied_password_to_clipboard">ক্লিপবোর্ডে পাসওয়ার্ড অনুলিপিত</string>
|
||||
<string name="open_url">ইউআরএল খুলো</string>
|
||||
<string name="cannot_resolve_action">খোলার মতো অ্যাপ নেই</string>
|
||||
<string name="pick_search_engine">একটা সার্চ ইঞ্জিন নাও</string>
|
||||
<string name="format">ধরণ</string>
|
||||
<string name="size_width_by_height">পিক্সেলে আকা:%1$d×%2$d</string>
|
||||
<string name="margin_size">Margin in pixels: %1$d</string>
|
||||
<string name="size_no_magnification">কোনো বৃদ্ধি নেই</string>
|
||||
<string name="add_quiet_zone">শান্তি জোন যোগ করুন</string>
|
||||
<string name="input_content_here">এখানে তথ্য দাও</string>
|
||||
<string name="unescape">Expand escape sequences</string>
|
||||
<string name="unescape">এস্কেপ সিকোয়েন্স প্রসারিত করুন</string>
|
||||
<string name="encode">সংকেতকরণ</string>
|
||||
<string name="error_no_content">তথ্য নেই</string>
|
||||
<string name="error_encoding_barcode">বারকোড বানানো যায়নি</string>
|
||||
@@ -53,25 +61,40 @@
|
||||
<string name="switch_camera">ক্যামেরা পাল্টাও</string>
|
||||
<string name="bulk_mode">ধারাবাহিকভাবে খুঁজো</string>
|
||||
<string name="bulk_mode_summary">শুরুতে সবসময় ধারাবাহিকভাবে খুঁজো</string>
|
||||
<string name="bulk_mode_delay">Delay between continuous scans</string>
|
||||
<string name="restrict_format">Restrict format</string>
|
||||
<string name="remove_restriction">Remove restriction</string>
|
||||
<string name="scan_format">Scan %s</string>
|
||||
<string name="quarter_second">A quarter second</string>
|
||||
<string name="half_second">Half a second</string>
|
||||
<string name="a_second">One second</string>
|
||||
<string name="two_seconds">Two seconds</string>
|
||||
<string name="five_seconds">Five seconds</string>
|
||||
<string name="bulk_mode_delay">একটি অবিরত স্ক্যান মধ্যে বিলম্ব</string>
|
||||
<string name="restrict_format">বিনিয়োগ বাধাগ্রস্ত ফরম্যাট</string>
|
||||
<string name="remove_restriction">সীমাবদ্ধতা অপসারণ করুন</string>
|
||||
<string name="find_code">কোড খুঁজুন</string>
|
||||
<string name="find_code_with_content"> কী বিষয়টি? </string>
|
||||
<string name="find_code_hint">শব্দ বা রেগুলার এক্সপ্রেশন</string>
|
||||
<string name="does_not_match_search_term">অনুসন্ধান শর্তের সাথে মিলে না</string>
|
||||
<string name="scan_for">স্ক্যান %s</string>
|
||||
<string name="quarter_second">এক চতুর্থ সেকেন্ড</string>
|
||||
<string name="half_second">আধা সেকেন্ড</string>
|
||||
<string name="a_second">এক সেকেন্ড</string>
|
||||
<string name="two_seconds">দুই সেকেন্ড</string>
|
||||
<string name="five_seconds">পাঁচ সেকেন্ড</string>
|
||||
<string name="history">ইতিহাস</string>
|
||||
<string name="preferences">পছন্দসমূহ</string>
|
||||
<string name="scan_category">খুঁজো</string>
|
||||
<string name="content_category">তথ্য</string>
|
||||
<string name="locale_category">ভাষা</string>
|
||||
<string name="profile_category">প্রোফাইল</string>
|
||||
<string name="profile">প্রোফাইল</string>
|
||||
<string name="profile_default">ডিফল্ট</string>
|
||||
<string name="profile_add">নতুন প্রোফাইল যোগ করো</string>
|
||||
<string name="profile_enter_name">প্রোফাইলের নাম লেখো</string>
|
||||
<string name="profile_enter_name_hint">নাম</string>
|
||||
<string name="profile_invalid_name">প্রোফাইলের নাম অবৈধ</string>
|
||||
<string name="profile_empty">এখনও কোনো প্রোফাইল নেই</string>
|
||||
<string name="profile_current">বর্তমান প্রোফাইল</string>
|
||||
<string name="profile_remove_confirm">এই প্রোফাইলটি সরাবেন?</string>
|
||||
<string name="current_profile">প্রোফাইল: %s</string>
|
||||
<string name="custom_locale">আলাদা ভাষা</string>
|
||||
<string name="follow_system_settings">ব্যবস্থার পছন্দসমূহ অনুসরণ করো</string>
|
||||
<string name="show_crop_handle">ছাটার সীমা দেখাও</string>
|
||||
<string name="show_crop_handle_summary">পরিবর্তনযোগ্য জায়গায়ই শুধু খুঁজো।</string>
|
||||
<string name="barcode_formats">Barcode formats</string>
|
||||
<string name="barcode_formats">বারকোড বিন্যাস</string>
|
||||
<string name="zoom_by_swiping">ওপরে/নিচে টানো বড়ো করার জন্য</string>
|
||||
<string name="zoom_by_swiping_summary">ক্যামেরার ছোটো-বড় নিয়ন্ত্রণের জন্য উপরে-নিচে টানো।</string>
|
||||
<string name="auto_rotate">খাড়া ১ডি বারকোড সনাক্ত করো</string>
|
||||
@@ -82,19 +105,21 @@
|
||||
<string name="show_toast_in_bulk_mode_summary">ধারাবাহিকভাবে তথ্য দেখানোর সময় মুহূর্তের জন্য তথ্য দেখাও।</string>
|
||||
<string name="vibrate">চিনলে কাঁপো</string>
|
||||
<string name="vibrate_summary">এটা সক্ষম রাখলে সংকেত চিনার সাথে সাথে কাঁপবে।</string>
|
||||
<string name="beep">Beep on detection</string>
|
||||
<string name="beep_summary">Enable this if you want your device to beep when a code is recognized.</string>
|
||||
<string name="beep_tone">Beep tone</string>
|
||||
<string name="tone_cdma_confirm">CDMA confirm</string>
|
||||
<string name="tone_sup_confirm">Call supervisory confirm</string>
|
||||
<string name="tone_sup_radio_ack">Call supervisory radio path acknowledgment</string>
|
||||
<string name="tone_prop_ack">Positive acknowledgment</string>
|
||||
<string name="tone_prop_beep">General beep</string>
|
||||
<string name="tone_prop_beep2">General double beep</string>
|
||||
<string name="beep">প্রকোপে বিপ দিন</string>
|
||||
<string name="beep_summary">যদি আপনি চান যে আপনার ডিভাইসটি কোড প্রাপ্ত হলে বিপ শব্দ করে।</string>
|
||||
<string name="beep_tone">বিপ টোন</string>
|
||||
<string name="tone_cdma_confirm">সিডিএমএ নিশ্চিত করুন</string>
|
||||
<string name="tone_sup_confirm">কল সুপারভাইজরী কনফার্ম</string>
|
||||
<string name="tone_sup_radio_ack">কল সুপারভাইজরি রেডিও পাথ স্বীকৃতি</string>
|
||||
<string name="tone_prop_ack">ধন্যবাদ</string>
|
||||
<string name="tone_prop_beep">সাধারণ বিপ</string>
|
||||
<string name="tone_prop_beep2">সাধারণ ডাবল বিপ</string>
|
||||
<string name="use_history">ইতিহাস সংরক্ষণ করো।</string>
|
||||
<string name="use_history_summary">সনাক্তকৃত সংকেত যন্ত্রে সংরক্ষণ করো।</string>
|
||||
<string name="ignore_duplicates">নকল উপেক্ষা করুন</string>
|
||||
<string name="ignore_consecutive_duplicates">দ্বিত্ব উপেক্ষা করো</string>
|
||||
<string name="ignore_consecutive_duplicates_summary">দুইটা ইতিহাস সংরক্ষণ করবে না।</string>
|
||||
<string name="ignore_any_duplicates">Ignore any duplicates</string>
|
||||
<string name="accept_duplicates">ডুপ্লিকেট গ্রহণ করুন</string>
|
||||
<string name="open_immediately">সাথে সাথে খুলো</string>
|
||||
<string name="open_immediately_summary">না দেখে সাথে সাথে খুলো। সক্রিয় থাকলে ক্ষতিকারক তথ্য খুলতে পারে।</string>
|
||||
<string name="copy_immediately">ক্লিপবোর্ডে রাখো</string>
|
||||
@@ -103,20 +128,46 @@
|
||||
<string name="show_meta_data_summary">সনাক্তকৃত বারকোড নিয়ে অতিরিক্ত তথ্য দেখাও।</string>
|
||||
<string name="show_hex_dump">ষড়(hex) সংকেত দেখাও</string>
|
||||
<string name="show_hex_dump_summary">পড়া তথ্যের ষড় সংকেত দেখাও</string>
|
||||
<string name="show_recreation">Show recreated barcode</string>
|
||||
<string name="show_recreation_summary">Recreate and show barcode from read content if possible.</string>
|
||||
<string name="show_checksum">সামগ্রিক চেকসাম দেখান</string>
|
||||
<string name="no_checksum">কোনো চেকসাম নেই</string>
|
||||
<string name="show_recreation">পুনরায় তৈরি বারকোড দেখান</string>
|
||||
<string name="show_recreation_summary">যদি সম্ভব হয়, পঠিত বিষয় থেকে পুনরায় সৃষ্টি করুন এবং বারকোড দেখান।</string>
|
||||
<string name="close_automatically">অনুলিপি/ভাগ করার পরে পিছনে যাও</string>
|
||||
<string name="close_automatically_summary">তথ্য অনুলিপি/ভাগ করার পরে স্বয়ংক্রিয়ভাবে পিছনে যাও।</string>
|
||||
<string name="default_search_engine">Open unknown data in</string>
|
||||
<string name="always_ask">Always ask</string>
|
||||
<string name="default_search_engine">অজানা ডেটা খোলুন</string>
|
||||
<string name="always_ask">সবসময় জিজ্ঞাসা করুন</string>
|
||||
<string name="open_with_url">অজানা ইউআরএল খুলো</string>
|
||||
|
||||
<string name="automated_actions">স্বয়ংক্রিয় ক্রিয়াসমূহ</string>
|
||||
<string name="automated_actions_none">কোনো স্বয়ংক্রিয় ক্রিয়া নেই</string>
|
||||
<string name="automated_actions_empty">এখনো কোনো স্বয়ংক্রিয় ক্রিয়া নেই</string>
|
||||
<plurals name="automated_actions_count">
|
||||
<item quantity="one">%dটি স্বয়ংক্রিয় ক্রিয়া</item>
|
||||
<item quantity="other">%dটি স্বয়ংক্রিয় ক্রিয়া</item>
|
||||
</plurals>
|
||||
<string name="automated_action_add">ক্রিয়া যোগ করুন</string>
|
||||
<string name="automated_action_edit">ক্রিয়া সম্পাদনা</string>
|
||||
<string name="automated_action_regex">রেগেক্স</string>
|
||||
<string name="automated_action_regex_hint">উদাহরণ ^https?://.*</string>
|
||||
<string name="automated_action_regex_required">রেগেক্স প্রয়োজন</string>
|
||||
<string name="automated_action_regex_invalid">অবৈধ রেগেক্স</string>
|
||||
<string name="automated_action_type">ক্রিয়ার ধরন</string>
|
||||
<string name="automated_action_type_intent">URL খুলুন</string>
|
||||
<string name="automated_action_url_template">URL টেমপ্লেট</string>
|
||||
<string name="automated_action_url_hint">https://example.com/?q={RESULT}</string>
|
||||
<string name="automated_action_url_required">URL টেমপ্লেট প্রয়োজন</string>
|
||||
<string name="automated_action_invalid_url">অবৈধ URL টেমপ্লেট</string>
|
||||
<string name="automated_action_remove_confirm">এই ক্রিয়াটি সরাবেন?</string>
|
||||
<string name="automated_action_list_intent">URL খুলুন: %s</string>
|
||||
<string name="automated_action_list_intent_empty">URL খুলুন</string>
|
||||
<string name="automated_action_webview_title">স্বয়ংক্রিয় ক্রিয়া</string>
|
||||
<string name="clear_network_suggestions">Clear network suggestions</string>
|
||||
<string name="clear_network_suggestions_summary">Remove all of the network suggestions that were previously provided by this app.</string>
|
||||
<string name="really_remove_all_networks">Really remove all networks?</string>
|
||||
<string name="clear_network_suggestions_success">Network suggestions cleared</string>
|
||||
<string name="clear_network_suggestions_nothing_to_remove">Nothing to remove</string>
|
||||
<string name="really_remove_all_networks">সব নেটওয়ার্ক সত্যিই সরান?</string>
|
||||
<string name="clear_network_suggestions_success">নেটওয়ার্ক পরামর্শ মোছা হয়েছে</string>
|
||||
<string name="clear_network_suggestions_nothing_to_remove">মুছতে কিছু নেই</string>
|
||||
<string name="send_category">অন্যকে পাঠাও</string>
|
||||
<string name="send_scan_active">Forward scans</string>
|
||||
<string name="send_scan_active">পুনরায় স্ক্যান পাঠান</string>
|
||||
<string name="send_scan_active_summary">Enable to forward scans to a the given URL</string>
|
||||
<string name="send_scan_url">প্রতিটি পাঠোদ্ধার ইউআরএল-এ পাঠাও</string>
|
||||
<string name="send_scan_type">প্রতি পাঠোদ্ধারের জন্য অনুরোধ</string>
|
||||
@@ -124,21 +175,24 @@
|
||||
<string name="send_type_get_query_string">সম্পূর্ণ কুয়েরি স্ট্রিং দিয়ে গেট(GET) করো</string>
|
||||
<string name="send_type_post_form">POST application/x-www-form-urlencoded</string>
|
||||
<string name="send_type_post_json">POST application/json</string>
|
||||
<string name="send_type_external_browser">Open in external browser</string>
|
||||
<string name="send_type_external_browser">বাহ্যিক ব্রাউজারে খোলুন</string>
|
||||
<string name="test_url">পরীক্ষার জন্য ইউআরএল</string>
|
||||
<string name="send_scan_bluetooth">Forward scans with Bluetooth</string>
|
||||
<string name="send_scan_bluetooth">ব্লুটুথ দিয়ে স্ক্যান ফরোয়ার্ড করুন</string>
|
||||
<string name="send_scan_bluetooth_summary">Enable to forwards scans to a given Bluetooth host.</string>
|
||||
<string name="send_scan_bluetooth_host">Host</string>
|
||||
<string name="no_bluetooth_hosts_paired">No Hosts Paired</string>
|
||||
<string name="send_scan_bluetooth_host">হোস্ট</string>
|
||||
<string name="no_bluetooth_hosts_paired">কোন হোস্ট পেয়ার করা হয়নি</string>
|
||||
<string name="bluetooth_connect_fail">Could not connect to bluetooth device.</string>
|
||||
<string name="bluetooth_send_fail">Could not send to bluetooth device.</string>
|
||||
<string name="bluetooth_send_success">Successful bluetooth send.</string>
|
||||
<string name="generation_category">প্রজন্ম</string>
|
||||
<string name="brighten_screen">স্ক্রিন উজ্জ্বল করুন</string>
|
||||
<string name="brighten_screen_summary">স্ক্রিন থেকে বারকোড স্ক্যান করার জন্য উজ্জ্বলতা সর্বোচ্চ করুন।</string>
|
||||
<string name="really_remove_scan">আসলেই এটা মুছবে?</string>
|
||||
<string name="really_remove_all_scans">আসলেই সব মুছবে?</string>
|
||||
<string name="really_remove_selected_scans">আসলেই নির্বাচিত সব মুছবে?</string>
|
||||
<string name="clear_history">ইতিহাস মুছো</string>
|
||||
<string name="copy_scan">পাঠোদ্ধার অনুলিপি করো</string>
|
||||
<string name="edit_scan">চিহ্ন সম্পাদনা করো</string>
|
||||
<string name="edit_label">চিহ্ন সম্পাদনা করো</string>
|
||||
<string name="remove_scan">পাঠোদ্ধার মুছো</string>
|
||||
<string name="enter_name">পাঠোদ্ধার ব্যাখ্যা করতে একটা নাম দাও</string>
|
||||
<string name="enter_name_hint">পাঠোদ্ধারের নাম</string>
|
||||
@@ -166,6 +220,8 @@
|
||||
<string name="otpauth_add">২এফএ যোগ করো</string>
|
||||
<string name="search_web">ওয়েবে খুঁজো</string>
|
||||
<string name="search_scan">আগেরগুলোতে খুঁজো</string>
|
||||
<string name="add_to_history">ইতিহাসে যোগ করুন</string>
|
||||
<string name="added_to_history">ইতিহাসে যোগ করা হয়েছে</string>
|
||||
<string name="export_to_file">নথিতে রপ্তানি করো</string>
|
||||
<string name="export_as">রপ্তানির ধরণ?</string>
|
||||
<string name="export_csv_comma">কমাসহ সিএসভি(.csv) নথি</string>
|
||||
@@ -174,24 +230,39 @@
|
||||
<string name="export_database">এসকিউলাইট তথ্যভিত্তি</string>
|
||||
<string name="saved_in_downloads">ডাউনলোডে সংরক্ষিত</string>
|
||||
<string name="rotate_image_cw">ছবি ঘড়ির দিকে ঘুরাও</string>
|
||||
<string name="toggle_free_rotation">Toggle free rotation</string>
|
||||
<string name="pick_file">Pick a file</string>
|
||||
<string name="toggle_free_rotation">মুক্ত ঘূর্ণন টগল করুন</string>
|
||||
<string name="pick_file">একটি ফাইল চয়ন করুন</string>
|
||||
<string name="pick_image_file">ছবি নাও</string>
|
||||
<string name="pick_code_to_scan">খুঁজার জন্য কোড নাও</string>
|
||||
<string name="shortcut_decode">বারকোড খুজো</string>
|
||||
<string name="shortcut_encode">বারকোড বানাও</string>
|
||||
<string name="shortcut_history">ইতিহাস</string>
|
||||
<string name="shortcut_preferences">পছন্দসমূহ</string>
|
||||
<string name="background_request_failed">পটভূমির অনুরোধ ব্যর্থ</string>
|
||||
<string name="entry_type">Type</string>
|
||||
<string name="wifi_network">Wi-Fi network</string>
|
||||
<string name="wifi_ssid">Name</string>
|
||||
<string name="wifi_type">Authentication type</string>
|
||||
<string name="wifi_password">Password</string>
|
||||
<string name="wifi_hidden">Hidden network</string>
|
||||
<string name="wifi_eap">EAP method</string>
|
||||
<string name="wifi_anonymous_identity">Anonymous identity</string>
|
||||
<string name="wifi_identity">Identity</string>
|
||||
<string name="wifi_phase2">Phase 2 method</string>
|
||||
<string name="network_suggestions">Network suggestions</string>
|
||||
<string name="remove_suggestion">Remove suggestion</string>
|
||||
<string name="entry_type">ধরণ</string>
|
||||
<string name="wifi_network">ওয়াই-ফাই নেটওয়ার্ক</string>
|
||||
<string name="wifi_ssid">নাম</string>
|
||||
<string name="wifi_type">প্রমাণীকরণ ধরণ</string>
|
||||
<string name="wifi_password">গুরুত্বপূর্ণ</string>
|
||||
<string name="wifi_hidden">লুকানো নেটওয়ার্ক</string>
|
||||
<string name="wifi_eap">EAP পদ্ধতি</string>
|
||||
<string name="wifi_anonymous_identity">অজ্ঞাত পরিচিতি</string>
|
||||
<string name="wifi_identity">পরিচয়</string>
|
||||
<string name="wifi_phase2">ধাপ ২ পদ্ধতি</string>
|
||||
<string name="network_suggestions">নেটওয়ার্ক সুপারিশ</string>
|
||||
<string name="remove_suggestion">প্রস্তাব সরান</string>
|
||||
<string name="email_to">প্রাপক</string>
|
||||
<string name="email_subject">বিষয়</string>
|
||||
<string name="email_body">বার্তা</string>
|
||||
<string name="scheme">ব্যবস্থা</string>
|
||||
<string name="host">হোস্ট</string>
|
||||
<string name="query">অনুসন্ধান</string>
|
||||
<string name="automated_action_type_custom_intent">কাস্টম Intent</string>
|
||||
<string name="automated_action_intent_uri_template">Intent URI টেমপ্লেট</string>
|
||||
<string name="automated_action_intent_uri_hint">intent:#Intent;action=android.intent.action.SEND;type=text/plain;S.android.intent.extra.TEXT={RESULT};end</string>
|
||||
<string name="automated_action_intent_uri_required">Intent URI প্রয়োজন</string>
|
||||
<string name="automated_action_intent_uri_invalid">অবৈধ Intent URI</string>
|
||||
<string name="automated_action_list_custom_intent">কাস্টম Intent: %s</string>
|
||||
<string name="automated_action_list_custom_intent_empty">কাস্টম Intent</string>
|
||||
|
||||
</resources>
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="welcome">Vítejte!</string>
|
||||
<string name="mode_expert">Pokročilý</string>
|
||||
<string name="mode_expert_description">Chci zkontrolovat načtený obsah a otevřít jej ručně.</string>
|
||||
<string name="mode_simple">Jednoduchý</string>
|
||||
<string name="mode_simple_description">Chci jen skenovat čárové kódy a okamžitě je otevírat.</string>
|
||||
<string name="camera_error">Nepodařilo se otevřít fotoaparát, prosím zkuste to znovu</string>
|
||||
<string name="scan_code">Naskenovat kód</string>
|
||||
<string name="compose_barcode">Vytvořit čárový kód</string>
|
||||
@@ -12,10 +18,11 @@
|
||||
<item quantity="other">%2$d znaků, %1$s</item>
|
||||
</plurals>
|
||||
<string name="error_correction_level">Stupeň korekce chyb</string>
|
||||
<string name="error_correction_level_l" formatted="false">Nízký (~7% korekce)</string>
|
||||
<string name="error_correction_level_m" formatted="false">Střední (~15% korekce)</string>
|
||||
<string name="error_correction_level_q" formatted="false">Čtvrtinový (~25% korekce)</string>
|
||||
<string name="error_correction_level_h" formatted="false">Vysoký (~30% korekce)</string>
|
||||
<string name="error_correction_level_l" formatted="false">Nízký (~7% korekce)</string>
|
||||
<string name="error_correction_level_m" formatted="false">Střední (~15% korekce)</string>
|
||||
<string name="error_correction_level_q" formatted="false">Čtvrtinový (~25% korekce)</string>
|
||||
<string name="error_correction_level_h" formatted="false">Vysoký (~30% korekce)</string>
|
||||
<string name="label">Štítek</string>
|
||||
<string name="colors">Popředí a pozadí</string>
|
||||
<string name="colors_black_on_white">Černá na bílé</string>
|
||||
<string name="colors_white_on_black">Bílá na černé</string>
|
||||
@@ -30,21 +37,23 @@
|
||||
<string name="gtin_add_on">rozšíření UPC EAN</string>
|
||||
<string name="barcode_version_number">Verze</string>
|
||||
<string name="qr_version_and_modules">%1$s (%2$d modulů)</string>
|
||||
<string name="qr_data_mask">Maska dat</string>
|
||||
<string name="toggle_flash">Přepnout blesk</string>
|
||||
<string name="error_flash">Zapnutí/vypnutí blesku selhalo</string>
|
||||
<string name="share">Sdílet</string>
|
||||
<string name="share_file">Sdílet jako soubor</string>
|
||||
<string name="share_as">Sdílet jako</string>
|
||||
<string name="share_as">Sdílet jako?</string>
|
||||
<string name="copy_to_clipboard">Zkopírovat do schránky</string>
|
||||
<string name="copy_as_deeplink">Kopírovat jako deeplink</string>
|
||||
<string name="copied_to_clipboard">Zkopírováno do schránky</string>
|
||||
<string name="copy_password">Zkopírovat heslo do schránky</string>
|
||||
<string name="copied_password_to_clipboard">Heslo zkopírováno do schránky</string>
|
||||
<string name="open_url">Otevřít URL</string>
|
||||
<string name="cannot_resolve_action">Nemáte aplikaci, která by toto uměla otevřít</string>
|
||||
<string name="pick_search_engine">Zvolte vyhledávač</string>
|
||||
<string name="format">Formát</string>
|
||||
<string name="size_width_by_height">Velikost v pixelech: %1$d×%2$d</string>
|
||||
<string name="margin_size">Okraje v pixelech: %1$d</string>
|
||||
<string name="size_no_magnification">Žádné zvětšení</string>
|
||||
<string name="add_quiet_zone">Přidat tichou zónu</string>
|
||||
<string name="input_content_here">Sem vložte obsah</string>
|
||||
<string name="unescape">Rozšířit escape sekvence</string>
|
||||
<string name="encode">ZAKÓDOVAT</string>
|
||||
@@ -58,7 +67,11 @@
|
||||
<string name="bulk_mode_delay">Prodleva mezi skeny</string>
|
||||
<string name="restrict_format">Omezit na formát</string>
|
||||
<string name="remove_restriction">Zrušit omezení formátu</string>
|
||||
<string name="scan_format">Naskenovat %s</string>
|
||||
<string name="find_code">Najít kód</string>
|
||||
<string name="find_code_with_content">Co obsah?</string>
|
||||
<string name="find_code_hint">Termín nebo regulární výraz</string>
|
||||
<string name="does_not_match_search_term">Neodpovídá vyhledávacímu termínu</string>
|
||||
<string name="scan_for">Naskenovat %s</string>
|
||||
<string name="quarter_second">Čtvrt sekundy</string>
|
||||
<string name="half_second">Půl sekundy</string>
|
||||
<string name="a_second">Jedna sekunda</string>
|
||||
@@ -69,6 +82,17 @@
|
||||
<string name="scan_category">Skenování</string>
|
||||
<string name="content_category">Obsah</string>
|
||||
<string name="locale_category">Jazyk</string>
|
||||
<string name="profile_category">Profil</string>
|
||||
<string name="profile">Profil</string>
|
||||
<string name="profile_default">Výchozí</string>
|
||||
<string name="profile_add">Přidat nový profil</string>
|
||||
<string name="profile_enter_name">Zadejte název profilu</string>
|
||||
<string name="profile_enter_name_hint">Název</string>
|
||||
<string name="profile_invalid_name">Neplatný název profilu</string>
|
||||
<string name="profile_empty">Zatím žádné profily</string>
|
||||
<string name="profile_current">Aktuální profil</string>
|
||||
<string name="profile_remove_confirm">Odebrat tento profil?</string>
|
||||
<string name="current_profile">Profil: %s</string>
|
||||
<string name="custom_locale">Nastavit jazyk</string>
|
||||
<string name="follow_system_settings">Podle systému</string>
|
||||
<string name="show_crop_handle">Zobrazovat hrany výřezu</string>
|
||||
@@ -88,15 +112,17 @@
|
||||
<string name="beep_summary">Povolte, pokud chcete pípnutí při rozpoznání kódu.</string>
|
||||
<string name="beep_tone">Tón pípnutí</string>
|
||||
<string name="tone_cdma_confirm">Potvrzení CDMA</string>
|
||||
<string name="tone_sup_confirm">Call supervisory confirm</string>
|
||||
<string name="tone_sup_radio_ack">Call supervisory radio path acknowledgment</string>
|
||||
<string name="tone_sup_confirm">Potvrzení dohledu volání</string>
|
||||
<string name="tone_sup_radio_ack">Hovor na dohledové rádiové cestě potvrzení</string>
|
||||
<string name="tone_prop_ack">Kladné potvrzení příjmu</string>
|
||||
<string name="tone_prop_beep">Obecné pípnutí</string>
|
||||
<string name="tone_prop_beep2">Dvojité obecné pípnutí</string>
|
||||
<string name="use_history">Ukládat historii skenování</string>
|
||||
<string name="use_history_summary">Rozpoznané kódy se uloží do zařízení.</string>
|
||||
<string name="ignore_duplicates">Ignorovat duplicity</string>
|
||||
<string name="ignore_consecutive_duplicates">Ignorovat duplikáty</string>
|
||||
<string name="ignore_consecutive_duplicates_summary">Další skeny stejného kódu se nebudou ukládat.</string>
|
||||
<string name="ignore_any_duplicates">Ignore any duplicates</string>
|
||||
<string name="accept_duplicates">Přijmout duplicity</string>
|
||||
<string name="open_immediately">Otevírat okamžitě</string>
|
||||
<string name="open_immediately_summary">Přeskočí kontrolu a otevře obsah okamžitě. Může otevřít škodlivý obsah.</string>
|
||||
<string name="copy_immediately">Vložit do schránky</string>
|
||||
@@ -105,6 +131,8 @@
|
||||
<string name="show_meta_data_summary">Zobrazí další data o naskenovaném čárovém kódu.</string>
|
||||
<string name="show_hex_dump">Zobrazit hex dump</string>
|
||||
<string name="show_hex_dump_summary">Zobrazí hex dump naskenovaného obsahu.</string>
|
||||
<string name="show_checksum">Zobrazit kontrolní součet</string>
|
||||
<string name="no_checksum">Žádný kontrolní součet</string>
|
||||
<string name="show_recreation">Zobrazit zrekonstruovaný kód</string>
|
||||
<string name="show_recreation_summary">Zrekonstruovat a zobrazit čárový kód z přečteného obrazu, pokud je to možné.</string>
|
||||
<string name="close_automatically">Vrátit se po zkopírování/sdílení</string>
|
||||
@@ -112,6 +140,32 @@
|
||||
<string name="default_search_engine">Nerozpoznaná data otevřít v</string>
|
||||
<string name="always_ask">Vždy se zeptat</string>
|
||||
<string name="open_with_url">Otevřít nerozpoznaná data pomocí URL</string>
|
||||
|
||||
<string name="automated_actions">Automatizované akce</string>
|
||||
<string name="automated_actions_none">Žádné automatizované akce</string>
|
||||
<string name="automated_actions_empty">Zatím žádné automatizované akce</string>
|
||||
<plurals name="automated_actions_count">
|
||||
<item quantity="one">%d automatizovaná akce</item>
|
||||
<item quantity="few">%d automatizované akce</item>
|
||||
<item quantity="many">%d automatizovaných akcí</item>
|
||||
<item quantity="other">%d automatizované akce</item>
|
||||
</plurals>
|
||||
<string name="automated_action_add">Přidat akci</string>
|
||||
<string name="automated_action_edit">Upravit akci</string>
|
||||
<string name="automated_action_regex">Regulární výraz</string>
|
||||
<string name="automated_action_regex_hint">např. ^https?://.*</string>
|
||||
<string name="automated_action_regex_required">Regulární výraz je povinný</string>
|
||||
<string name="automated_action_regex_invalid">Neplatný regulární výraz</string>
|
||||
<string name="automated_action_type">Typ akce</string>
|
||||
<string name="automated_action_type_intent">Otevřít URL</string>
|
||||
<string name="automated_action_url_template">Šablona URL</string>
|
||||
<string name="automated_action_url_hint">https://example.com/?q={RESULT}</string>
|
||||
<string name="automated_action_url_required">Šablona URL je povinná</string>
|
||||
<string name="automated_action_invalid_url">Neplatná šablona URL</string>
|
||||
<string name="automated_action_remove_confirm">Odstranit tuto akci?</string>
|
||||
<string name="automated_action_list_intent">Otevřít URL: %s</string>
|
||||
<string name="automated_action_list_intent_empty">Otevřít URL</string>
|
||||
<string name="automated_action_webview_title">Automatizovaná akce</string>
|
||||
<string name="clear_network_suggestions">Smazat navrhované sítě</string>
|
||||
<string name="clear_network_suggestions_summary">Smazat všechny sítě, které byly navrženy touto aplikací.</string>
|
||||
<string name="really_remove_all_networks">Opravdu smazat všechny sítě?</string>
|
||||
@@ -119,11 +173,11 @@
|
||||
<string name="clear_network_suggestions_nothing_to_remove">Nic ke smazání</string>
|
||||
<string name="send_category">Předávání</string>
|
||||
<string name="send_scan_active">Předávat skeny</string>
|
||||
<string name="send_scan_active_summary">Zapnout předávání skenů na danou URL</string>
|
||||
<string name="send_scan_active_summary">Zapnout předávání skenů na danou URL.</string>
|
||||
<string name="send_scan_url">Posílat každý sken na URL</string>
|
||||
<string name="send_scan_type">Typ požadavku pro každý sken</string>
|
||||
<string name="send_type_get_add_content">GET a simply add content</string>
|
||||
<string name="send_type_get_query_string">GET with complete query string</string>
|
||||
<string name="send_type_get_query_string">GET s kompletním řetězcem dotazu</string>
|
||||
<string name="send_type_post_form">POST application/x-www-form-urlencoded</string>
|
||||
<string name="send_type_post_json">POST application/json</string>
|
||||
<string name="send_type_external_browser">Otevřít v prohlížeči</string>
|
||||
@@ -135,12 +189,15 @@
|
||||
<string name="bluetooth_connect_fail">Nelze se připojit k zařízení Bluetooth.</string>
|
||||
<string name="bluetooth_send_fail">Nelze odesílat na zařízení Bluetooth.</string>
|
||||
<string name="bluetooth_send_success">Odesílání přes Bluetooth se zdařilo.</string>
|
||||
<string name="generation_category">Generace</string>
|
||||
<string name="brighten_screen">Zvýšit jas obrazovky</string>
|
||||
<string name="brighten_screen_summary">Nastavte jas na maximum pro zlepšení skenování čárového kódu ze obrazovky.</string>
|
||||
<string name="really_remove_scan">Opravdu odstranit sken?</string>
|
||||
<string name="really_remove_all_scans">Opravdu odstranit všechny skeny?</string>
|
||||
<string name="really_remove_selected_scans">Opravdu odstranit zvolené skeny?</string>
|
||||
<string name="clear_history">Vymazat historii</string>
|
||||
<string name="copy_scan">Zkopírovat sken</string>
|
||||
<string name="edit_scan">Upravit popisek</string>
|
||||
<string name="edit_label">Upravit popisek</string>
|
||||
<string name="remove_scan">Odstranit sken</string>
|
||||
<string name="enter_name">Napište název popisující sken</string>
|
||||
<string name="enter_name_hint">Název skenu</string>
|
||||
@@ -148,7 +205,7 @@
|
||||
<string name="pick_list_separator">Jak mají být odděleny položky?</string>
|
||||
<string name="separator_line_break">Nový řádek</string>
|
||||
<string name="separator_ruler">Svislice</string>
|
||||
<string name="save_as_file_name">Uložit jako do složky Stahování</string>
|
||||
<string name="save_as_file_name">Uložit do složky Stahování?</string>
|
||||
<string name="file_name">Název souboru</string>
|
||||
<string name="error_saving_file">Soubor se nepodařilo uložit</string>
|
||||
<string name="error_file_exists">Soubor již existuje</string>
|
||||
@@ -168,8 +225,10 @@
|
||||
<string name="otpauth_add">Přidat 2FA</string>
|
||||
<string name="search_web">Vyhledat na webu</string>
|
||||
<string name="search_scan">Vyhledat sken</string>
|
||||
<string name="add_to_history">Přidat do historie</string>
|
||||
<string name="added_to_history">Přidáno do historie</string>
|
||||
<string name="export_to_file">Exportovat do souboru</string>
|
||||
<string name="export_as">Exportovat jako</string>
|
||||
<string name="export_as">Exportovat jako?</string>
|
||||
<string name="export_csv_comma">CSV oddělené čárkami</string>
|
||||
<string name="export_csv_semicolon">CSV oddělené středníky</string>
|
||||
<string name="export_json">soubor JSON</string>
|
||||
@@ -182,6 +241,7 @@
|
||||
<string name="pick_code_to_scan">Vyberte kód k naskenování</string>
|
||||
<string name="shortcut_decode">Skenovat čárový kód</string>
|
||||
<string name="shortcut_encode">Vytvořit čárový kód</string>
|
||||
<string name="shortcut_history">Historie</string>
|
||||
<string name="shortcut_preferences">Nastavení</string>
|
||||
<string name="background_request_failed">Požadavek na pozadí selhal</string>
|
||||
<string name="entry_type">Typ</string>
|
||||
@@ -196,4 +256,18 @@
|
||||
<string name="wifi_phase2">Ověření Phase 2</string>
|
||||
<string name="network_suggestions">Návrhy sítí</string>
|
||||
<string name="remove_suggestion">Odebrat návrh</string>
|
||||
<string name="email_to">Příjemce</string>
|
||||
<string name="email_subject">Předmět</string>
|
||||
<string name="email_body">Tělo zprávy</string>
|
||||
<string name="scheme">Schéma</string>
|
||||
<string name="host">Host</string>
|
||||
<string name="query">Dotaz</string>
|
||||
<string name="automated_action_type_custom_intent">Vlastní intent</string>
|
||||
<string name="automated_action_intent_uri_template">Šablona URI intentu</string>
|
||||
<string name="automated_action_intent_uri_hint">intent:#Intent;action=android.intent.action.SEND;type=text/plain;S.android.intent.extra.TEXT={RESULT};end</string>
|
||||
<string name="automated_action_intent_uri_required">URI intentu je povinné</string>
|
||||
<string name="automated_action_intent_uri_invalid">Neplatné URI intentu</string>
|
||||
<string name="automated_action_list_custom_intent">Vlastní intent: %s</string>
|
||||
<string name="automated_action_list_custom_intent_empty">Vlastní intent</string>
|
||||
|
||||
</resources>
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
<resources>
|
||||
<string name="welcome">Velkommen!</string>
|
||||
<string name="mode_expert">Avanceret</string>
|
||||
<string name="mode_expert_description">Jeg vil kontrollere det læste indhold og åbne det manuelt.</string>
|
||||
<string name="mode_simple">Simpel</string>
|
||||
<string name="mode_simple_description">Jeg vil bare scanne stregkoder og få dem åbnet med det samme.</string>
|
||||
<string name="camera_error">Kan ikke få adgang til kamera, prøv igen</string>
|
||||
<string name="scan_code">Scan kode</string>
|
||||
<string name="compose_barcode">Opret stregkode</string>
|
||||
@@ -14,6 +19,7 @@
|
||||
<string name="error_correction_level_m" formatted="false">Medium (~15% korrigering)</string>
|
||||
<string name="error_correction_level_q" formatted="false">Kvartil (~25% korrigering)</string>
|
||||
<string name="error_correction_level_h" formatted="false">Høj (~30% korrigering)</string>
|
||||
<string name="label">Mærke</string>
|
||||
<string name="colors">For- og baggrund</string>
|
||||
<string name="colors_black_on_white">Sort på hvidt</string>
|
||||
<string name="colors_white_on_black">Hvid på sort</string>
|
||||
@@ -28,23 +34,25 @@
|
||||
<string name="gtin_add_on">UPC EAN udvidelse</string>
|
||||
<string name="barcode_version_number">Version</string>
|
||||
<string name="qr_version_and_modules">%1$s (%2$d moduler)</string>
|
||||
<string name="qr_data_mask">Data mask</string>
|
||||
<string name="toggle_flash">Slå blitz til/fra</string>
|
||||
<string name="error_flash">Der opstod en fejl ved at slå blitz til/fra</string>
|
||||
<string name="share">Del</string>
|
||||
<string name="share_file">Share as file</string>
|
||||
<string name="share_file">Del som fil</string>
|
||||
<string name="share_as">Del som?</string>
|
||||
<string name="copy_to_clipboard">Kopier til udklipsholder</string>
|
||||
<string name="copy_as_deeplink">Kopiér som deeplink</string>
|
||||
<string name="copied_to_clipboard">Kopieret til udklipsholder</string>
|
||||
<string name="copy_password">Kopier adgangskode til udklipsholderen</string>
|
||||
<string name="copied_password_to_clipboard">Adgangskode kopieret til udklipsholderen</string>
|
||||
<string name="open_url">Åbn URL</string>
|
||||
<string name="cannot_resolve_action">Inge applikation kan åbne det</string>
|
||||
<string name="pick_search_engine">Vælg en søgemaskine</string>
|
||||
<string name="format">Format</string>
|
||||
<string name="size_width_by_height">Størrelse i pixels: %1$d×%2$d</string>
|
||||
<string name="margin_size">Margin in pixels: %1$d</string>
|
||||
<string name="size_no_magnification">Ingen forstørrelse</string>
|
||||
<string name="add_quiet_zone">Tilføj stille zone</string>
|
||||
<string name="input_content_here">Indtast indhold her</string>
|
||||
<string name="unescape">Expand escape sequences</string>
|
||||
<string name="unescape">Udvid escape-sekvenser</string>
|
||||
<string name="encode">INDKOD</string>
|
||||
<string name="error_no_content">Manglende indhold</string>
|
||||
<string name="error_encoding_barcode">Stregkode kan ikke genereres</string>
|
||||
@@ -56,7 +64,11 @@
|
||||
<string name="bulk_mode_delay">Forsinkelse mellem kontinuerlige scanninger</string>
|
||||
<string name="restrict_format">Begræns format</string>
|
||||
<string name="remove_restriction">Fjern begrænsningen</string>
|
||||
<string name="scan_format">Scan %s</string>
|
||||
<string name="find_code">Find kode</string>
|
||||
<string name="find_code_with_content">Hvad indhold?</string>
|
||||
<string name="find_code_hint">Term eller regulært udtryk</string>
|
||||
<string name="does_not_match_search_term">Stemmer ikke overens med søgeterm</string>
|
||||
<string name="scan_for">Scan %s</string>
|
||||
<string name="quarter_second">Et kvart sekund</string>
|
||||
<string name="half_second">Et halvt sekund</string>
|
||||
<string name="a_second">Et sekund</string>
|
||||
@@ -67,6 +79,17 @@
|
||||
<string name="scan_category">Scan</string>
|
||||
<string name="content_category">Indhold</string>
|
||||
<string name="locale_category">Sprog</string>
|
||||
<string name="profile_category">Profil</string>
|
||||
<string name="profile">Profil</string>
|
||||
<string name="profile_default">Standard</string>
|
||||
<string name="profile_add">Tilføj ny profil</string>
|
||||
<string name="profile_enter_name">Indtast profilnavn</string>
|
||||
<string name="profile_enter_name_hint">Navn</string>
|
||||
<string name="profile_invalid_name">Ugyldigt profilnavn</string>
|
||||
<string name="profile_empty">Ingen profiler endnu</string>
|
||||
<string name="profile_current">Nuværende profil</string>
|
||||
<string name="profile_remove_confirm">Fjern denne profil?</string>
|
||||
<string name="current_profile">Profil: %s</string>
|
||||
<string name="custom_locale">Brugerdefineret sprog</string>
|
||||
<string name="follow_system_settings">Følg systemindstillingerne</string>
|
||||
<string name="show_crop_handle">Vis beskæringsbegrænser</string>
|
||||
@@ -86,15 +109,17 @@
|
||||
<string name="beep_summary">Aktiver dette, hvis du vil have din enhed til at bippe, når en kode genkendes.</string>
|
||||
<string name="beep_tone">Bip tone</string>
|
||||
<string name="tone_cdma_confirm">CDMA bekræftelse</string>
|
||||
<string name="tone_sup_confirm">Call supervisory confirm</string>
|
||||
<string name="tone_sup_radio_ack">Call supervisory radio path acknowledgment</string>
|
||||
<string name="tone_sup_confirm">Opkald tilsyn bekræft</string>
|
||||
<string name="tone_sup_radio_ack">Opkald tilsynsradio sti anerkendelse</string>
|
||||
<string name="tone_prop_ack">Positiv anerkendelse</string>
|
||||
<string name="tone_prop_beep">Generelt bip</string>
|
||||
<string name="tone_prop_beep2">Generelt dobbelt bip</string>
|
||||
<string name="use_history">Gem scanningshistorik</string>
|
||||
<string name="use_history_summary">Gem genkendte koder på din enhed.</string>
|
||||
<string name="ignore_duplicates">Ignorer duplikater</string>
|
||||
<string name="ignore_consecutive_duplicates">Ignorer dubletter</string>
|
||||
<string name="ignore_consecutive_duplicates_summary">Gem ikke fortløbende dubletter i scanningshistorikken.</string>
|
||||
<string name="ignore_any_duplicates">Ignore any duplicates</string>
|
||||
<string name="accept_duplicates">Accept duplicates</string>
|
||||
<string name="open_immediately">Åbn straks</string>
|
||||
<string name="open_immediately_summary">Spring inspektion over og åbn indholdet med det samme. Kan åbne skadeligt indhold, hvis det er aktiveret.</string>
|
||||
<string name="copy_immediately">Læg i udklipsholderen</string>
|
||||
@@ -103,6 +128,8 @@
|
||||
<string name="show_meta_data_summary">Vis yderligere data om den scannede stregkode.</string>
|
||||
<string name="show_hex_dump">Vis hex-dump</string>
|
||||
<string name="show_hex_dump_summary">Vis et hex-dump af det scannede indhold.</string>
|
||||
<string name="show_checksum">Vis kontrolsum</string>
|
||||
<string name="no_checksum">Ingen kontrolsum</string>
|
||||
<string name="show_recreation">Vis genskabt stregkode</string>
|
||||
<string name="show_recreation_summary">Genskab og vis stregkode fra læst indhold, hvis det er muligt.</string>
|
||||
<string name="close_automatically">Gå tilbage efter Kopier/Del</string>
|
||||
@@ -110,6 +137,30 @@
|
||||
<string name="default_search_engine">Åbn ukendte data i</string>
|
||||
<string name="always_ask">Spørg altid</string>
|
||||
<string name="open_with_url">Åbn ukendte data med URL</string>
|
||||
|
||||
<string name="automated_actions">Automatiserede handlinger</string>
|
||||
<string name="automated_actions_none">Ingen automatiserede handlinger</string>
|
||||
<string name="automated_actions_empty">Ingen automatiserede handlinger endnu</string>
|
||||
<plurals name="automated_actions_count">
|
||||
<item quantity="one">%d automatiseret handling</item>
|
||||
<item quantity="other">%d automatiserede handlinger</item>
|
||||
</plurals>
|
||||
<string name="automated_action_add">Tilføj handling</string>
|
||||
<string name="automated_action_edit">Rediger handling</string>
|
||||
<string name="automated_action_regex">Regex</string>
|
||||
<string name="automated_action_regex_hint">f.eks. ^https?://.*</string>
|
||||
<string name="automated_action_regex_required">Regex påkrævet</string>
|
||||
<string name="automated_action_regex_invalid">Ugyldig regex</string>
|
||||
<string name="automated_action_type">Handlingstype</string>
|
||||
<string name="automated_action_type_intent">Åbn URL</string>
|
||||
<string name="automated_action_url_template">URL-skabelon</string>
|
||||
<string name="automated_action_url_hint">https://example.com/?q={RESULT}</string>
|
||||
<string name="automated_action_url_required">URL-skabelon påkrævet</string>
|
||||
<string name="automated_action_invalid_url">Ugyldig URL-skabelon</string>
|
||||
<string name="automated_action_remove_confirm">Fjern denne handling?</string>
|
||||
<string name="automated_action_list_intent">Åbn URL: %s</string>
|
||||
<string name="automated_action_list_intent_empty">Åbn URL</string>
|
||||
<string name="automated_action_webview_title">Automatiseret handling</string>
|
||||
<string name="clear_network_suggestions">Fjern netværksforslag</string>
|
||||
<string name="clear_network_suggestions_summary">Fjern netværksforslag, der tidligere blev tilføjet med denne app.</string>
|
||||
<string name="really_remove_all_networks">Vil du virkelig fjerne alle netværk?</string>
|
||||
@@ -126,19 +177,22 @@
|
||||
<string name="send_type_post_json">POST application/json</string>
|
||||
<string name="send_type_external_browser">Åbn i ekstern browser</string>
|
||||
<string name="test_url">Test URL</string>
|
||||
<string name="send_scan_bluetooth">Forward scans with Bluetooth</string>
|
||||
<string name="send_scan_bluetooth">Send scanninger med Bluetooth</string>
|
||||
<string name="send_scan_bluetooth_summary">Enable to forwards scans to a given Bluetooth host.</string>
|
||||
<string name="send_scan_bluetooth_host">Host</string>
|
||||
<string name="no_bluetooth_hosts_paired">No Hosts Paired</string>
|
||||
<string name="send_scan_bluetooth_host">Vært</string>
|
||||
<string name="no_bluetooth_hosts_paired">Ingen værter parret</string>
|
||||
<string name="bluetooth_connect_fail">Could not connect to bluetooth device.</string>
|
||||
<string name="bluetooth_send_fail">Could not send to bluetooth device.</string>
|
||||
<string name="bluetooth_send_success">Successful bluetooth send.</string>
|
||||
<string name="generation_category">Generation</string>
|
||||
<string name="brighten_screen">Skærm lysere</string>
|
||||
<string name="brighten_screen_summary">Indstil lysstyrken til maksimum for at forbedre scanning af en stregkode fra skærmen.</string>
|
||||
<string name="really_remove_scan">Vil du virkelig fjerne scanningen?</string>
|
||||
<string name="really_remove_all_scans">Vil du virkelig fjerne alle scanninger?</string>
|
||||
<string name="really_remove_selected_scans">Vil du virkelig fjerne valgte scanninger?</string>
|
||||
<string name="clear_history">Ryd historik</string>
|
||||
<string name="copy_scan">Kopier scanning</string>
|
||||
<string name="edit_scan">Rediger etiket</string>
|
||||
<string name="edit_label">Rediger etiket</string>
|
||||
<string name="remove_scan">Fjern scanning</string>
|
||||
<string name="enter_name">Indtast et navn for at beskrive scanningen</string>
|
||||
<string name="enter_name_hint">Navn på scanningen</string>
|
||||
@@ -166,20 +220,23 @@
|
||||
<string name="otpauth_add">Tilføj 2FA</string>
|
||||
<string name="search_web">Søg på nettet</string>
|
||||
<string name="search_scan">Søg scanning</string>
|
||||
<string name="add_to_history">Tilføj til historik</string>
|
||||
<string name="added_to_history">Tilføjet til historik</string>
|
||||
<string name="export_to_file">Eksporter til fil</string>
|
||||
<string name="export_as">Eksporter som?</string>
|
||||
<string name="export_csv_comma">CSV-fil med kommaer</string>
|
||||
<string name="export_csv_semicolon">CSV-fil med semikoloner</string>
|
||||
<string name="export_json">JSON fil</string>
|
||||
<string name="export_database">SQLite database</string>
|
||||
<string name="export_database">SQLite-database</string>
|
||||
<string name="saved_in_downloads">Gemt i downloads</string>
|
||||
<string name="rotate_image_cw">Roter billedet med uret</string>
|
||||
<string name="toggle_free_rotation">Slå fri rotation til/fra</string>
|
||||
<string name="pick_file">Pick a file</string>
|
||||
<string name="pick_file">Vælg en fil</string>
|
||||
<string name="pick_image_file">Vælg billedfil</string>
|
||||
<string name="pick_code_to_scan">Vælg kode til at scanne</string>
|
||||
<string name="shortcut_decode">Scan en stregkode</string>
|
||||
<string name="shortcut_encode">Opret en stregkode</string>
|
||||
<string name="shortcut_history">Historik</string>
|
||||
<string name="shortcut_preferences">Indstillinger</string>
|
||||
<string name="background_request_failed">Baggrundsanmodning mislykkedes</string>
|
||||
<string name="entry_type">Type</string>
|
||||
@@ -194,4 +251,18 @@
|
||||
<string name="wifi_phase2">Fase 2 metode</string>
|
||||
<string name="network_suggestions">Netværksforslag</string>
|
||||
<string name="remove_suggestion">Fjern forslag</string>
|
||||
<string name="email_to">Modtager</string>
|
||||
<string name="email_subject">Emne</string>
|
||||
<string name="email_body">Brødtekst</string>
|
||||
<string name="scheme">Ordning</string>
|
||||
<string name="host">Vært</string>
|
||||
<string name="query">Forespørgsel</string>
|
||||
<string name="automated_action_type_custom_intent">Brugerdefineret intent</string>
|
||||
<string name="automated_action_intent_uri_template">Intent-URI-skabelon</string>
|
||||
<string name="automated_action_intent_uri_hint">intent:#Intent;action=android.intent.action.SEND;type=text/plain;S.android.intent.extra.TEXT={RESULT};end</string>
|
||||
<string name="automated_action_intent_uri_required">Intent-URI påkrævet</string>
|
||||
<string name="automated_action_intent_uri_invalid">Ugyldig Intent-URI</string>
|
||||
<string name="automated_action_list_custom_intent">Brugerdefineret intent: %s</string>
|
||||
<string name="automated_action_list_custom_intent_empty">Brugerdefineret intent</string>
|
||||
|
||||
</resources>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
<string-array name="search_engines_names">
|
||||
<item>@string/always_ask</item>
|
||||
<item>Google</item>
|
||||
<item>Brave Search</item>
|
||||
<item>DuckDuckGo</item>
|
||||
<item>Qwant</item>
|
||||
<item>Bing</item>
|
||||
@@ -16,6 +17,7 @@
|
||||
<string-array name="search_engines_values">
|
||||
<item></item>
|
||||
<item>https://www.google.de/search?q=</item>
|
||||
<item>https://search.brave.com/search?q=</item>
|
||||
<item>https://duckduckgo.com/?q=</item>
|
||||
<item>https://www.qwant.com/?q=</item>
|
||||
<item>https://bing.com/?q=</item>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user