Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
08be31814f | ||
|
|
c5db04b84e | ||
|
|
9dccc6a7f5 | ||
|
|
a5deab656b | ||
|
|
b773dfa392 | ||
|
|
7b5106f09d | ||
|
|
16ce45093e | ||
|
|
e6345f09b3 | ||
|
|
de00ed779a | ||
|
|
d740aa121b | ||
|
|
38c40d36bb | ||
|
|
3e51f70c7c | ||
|
|
502702c7a5 | ||
|
|
96ff9b6239 | ||
|
|
afd6c74c80 | ||
|
|
5ffea2edd8 | ||
|
|
78d62395e2 | ||
|
|
a5d1465528 | ||
|
|
14517bad17 | ||
|
|
7a5e07fb3c | ||
|
|
c6b3ca08f4 | ||
|
|
73047cdee6 | ||
|
|
bec4aa3120 | ||
|
|
eaaaaf14c2 | ||
|
|
971742aff4 | ||
|
|
28915eacdb | ||
|
|
a2c1d11b5b | ||
|
|
e9dd396f34 | ||
|
|
c7edba746c | ||
|
|
34c8dfd169 | ||
|
|
459980899f | ||
|
|
2e0693cb45 | ||
|
|
187825932e | ||
|
|
a80802b4bc | ||
|
|
aa589a5d53 | ||
|
|
6a42e54f0b | ||
|
|
e134b18bba | ||
|
|
a112484dcb | ||
|
|
4f36030e38 | ||
|
|
90c2c03d12 | ||
|
|
3f853be51a | ||
|
|
835a348fad | ||
|
|
198a35fc15 | ||
|
|
6be1b25c38 | ||
|
|
ac632e3aac | ||
|
|
ddc5a14558 | ||
|
|
8b1ffe3e5a | ||
|
|
e9ba66848f | ||
|
|
7bdef27a50 | ||
|
|
ccd16a0f95 | ||
|
|
a385678de1 | ||
|
|
43c597f6fb | ||
|
|
ecccf34418 | ||
|
|
4027ce2cc6 | ||
|
|
209e6c5c30 | ||
|
|
5e29f3fbe9 | ||
|
|
ba3d63277a | ||
|
|
fe34b7209d | ||
|
|
0efdca7b4a | ||
|
|
827fbedc1f | ||
|
|
107b5bafd4 | ||
|
|
19a71480fa | ||
|
|
909e97a591 | ||
|
|
bc2be96871 | ||
|
|
d834621d99 | ||
|
|
ad83be1a60 | ||
|
|
d65ea02b49 | ||
|
|
c8d1357ceb | ||
|
|
4c05d046e6 | ||
|
|
4423ee62c8 | ||
|
|
bc69f99f8b | ||
|
|
6b088031fd | ||
|
|
b27d03e9c0 | ||
|
|
9a13838f28 | ||
|
|
cce2a460f6 | ||
|
|
36352ecde2 | ||
|
|
9a0bc324cb | ||
|
|
6dabafd6a3 | ||
|
|
dde4b1e0bd | ||
|
|
8a8c7bd1ff | ||
|
|
a2aed42e25 | ||
|
|
a38b7b647f | ||
|
|
76e82ed842 | ||
|
|
fd54bc604e | ||
|
|
59b62ecbc4 | ||
|
|
de6a2863f3 | ||
|
|
4b2eb80612 | ||
|
|
2de40d2277 | ||
|
|
7673b7cb0d | ||
|
|
097c5e4453 | ||
|
|
1fab30089e | ||
|
|
f9bf16dd47 | ||
|
|
7a59126312 | ||
|
|
694c854650 | ||
|
|
b1cd12ec77 | ||
|
|
0c27b15025 | ||
|
|
5de950bdbf | ||
|
|
ae9e304ef6 | ||
|
|
c4cc14554b | ||
|
|
d6afccf562 | ||
|
|
a90bdad117 | ||
|
|
52d4f082f2 | ||
|
|
1c63f5b3ac | ||
|
|
21985b4533 | ||
|
|
012ac644aa | ||
|
|
19f19739fd | ||
|
|
f01f9fcd91 | ||
|
|
4177b248c3 | ||
|
|
e4182a265b | ||
|
|
98df5a447c | ||
|
|
a0a1405359 | ||
|
|
76922c421c | ||
|
|
cac9dd16da | ||
|
|
a1f9ba1056 | ||
|
|
0b67c272b6 | ||
|
|
9b8be5305d | ||
|
|
a026ade13b | ||
|
|
b1771bc5ea | ||
|
|
5199460669 | ||
|
|
fd040e1660 | ||
|
|
2e1528364e | ||
|
|
d21af142bb | ||
|
|
8395e135f9 | ||
|
|
c0caa0ce44 | ||
|
|
96927ef9b2 | ||
|
|
84d9a3b55a | ||
|
|
ffe8e4c5ba | ||
|
|
bc7c5dca97 | ||
|
|
d9737f4df1 | ||
|
|
0e8147713a | ||
|
|
3d425a097a | ||
|
|
c26765cc63 | ||
|
|
fb6dc8ba55 | ||
|
|
70f4d166a0 | ||
|
|
6c136ce719 | ||
|
|
c6dee80c0a | ||
|
|
1f43dd4827 | ||
|
|
a366157977 | ||
|
|
6bb72672a3 | ||
|
|
04774527a0 | ||
|
|
6f3e644aa8 | ||
|
|
5e3176b703 | ||
|
|
0f0aaff284 | ||
|
|
f0d09e81b4 | ||
|
|
771258cb5c | ||
|
|
9482086666 | ||
|
|
bcad50c3e8 | ||
|
|
dbd055699e | ||
|
|
d67f8273d6 | ||
|
|
649be0ceb6 | ||
|
|
f5eb5dd8d2 | ||
|
|
eb805dad2f | ||
|
|
202bb32b29 | ||
|
|
50b8a4d329 | ||
|
|
fa15478fb4 | ||
|
|
20c2a5a8aa | ||
|
|
00b95678ed | ||
|
|
5df3517563 | ||
|
|
fd6946d2c2 | ||
|
|
c74d229e1f | ||
|
|
fa99a19db5 | ||
|
|
b083fcf213 | ||
|
|
46930ddea6 | ||
|
|
7bef77b96e | ||
|
|
c5f4c088b4 | ||
|
|
73f7210e18 | ||
|
|
469429e663 | ||
|
|
1a7ac59740 | ||
|
|
c13c59e728 | ||
|
|
bc17e25717 | ||
|
|
5017bd4c8d | ||
|
|
a44a913eca | ||
|
|
007e83e243 | ||
|
|
de8f97c246 | ||
|
|
1287591e53 | ||
|
|
8c17238572 | ||
|
|
5dbb36e6a9 | ||
|
|
ca7dfb3a33 | ||
|
|
419b7d5765 | ||
|
|
fa38866669 | ||
|
|
19ca521f59 | ||
|
|
be657d86cf | ||
|
|
5199a4a526 | ||
|
|
ff48a543c9 | ||
|
|
2eadd01a87 | ||
|
|
dd2abe7d89 | ||
|
|
8e05caac70 | ||
|
|
46aeced09f | ||
|
|
3681c5d011 | ||
|
|
20b4f7aac5 | ||
|
|
bc6e350271 | ||
|
|
c9c1b2f5bf | ||
|
|
1dbc0f0fa1 | ||
|
|
ab8170ee72 | ||
|
|
551464dc6f | ||
|
|
c3aac0a1e1 | ||
|
|
d678e1cfd0 | ||
|
|
a445ade877 | ||
|
|
1fd0b7637f | ||
|
|
fdc88df897 | ||
|
|
d0f133ae00 | ||
|
|
67646261e7 | ||
|
|
faeae9e904 | ||
|
|
77b21a9e83 | ||
|
|
cbe7860a3d | ||
|
|
fc29267d3d | ||
|
|
890506328b | ||
|
|
04036b6cd6 | ||
|
|
2602021c1c | ||
|
|
17a0bdc0a9 | ||
|
|
d71a220eca | ||
|
|
0c2e711d79 | ||
|
|
ca342a83d3 | ||
|
|
3c44ff56eb | ||
|
|
bcae64c0d0 | ||
|
|
c2f673729b | ||
|
|
2043e0f349 | ||
|
|
b93eb5963e | ||
|
|
5189fc9c5a | ||
|
|
2d311ff92a | ||
|
|
14bb08003d | ||
|
|
42f9887030 | ||
|
|
a59d241b50 | ||
|
|
9f31160c7b | ||
|
|
2eb4e51c17 | ||
|
|
53a554e7a2 | ||
|
|
7f6887716a | ||
|
|
5ef93802ce | ||
|
|
c1eff08bb9 | ||
|
|
8798d44ec7 | ||
|
|
d4e31ff97e | ||
|
|
ae8b08a85a | ||
|
|
09430d776e | ||
|
|
ef88c3179d | ||
|
|
c732ec726b | ||
|
|
801c768df4 | ||
|
|
552a2f6d57 | ||
|
|
7fa8810197 | ||
|
|
254af25bbb | ||
|
|
a27656c174 | ||
|
|
2ca55ebccc | ||
|
|
ec1d40942d | ||
|
|
7852e5ce9d | ||
|
|
f36a81d4a0 | ||
|
|
d86ea4073c | ||
|
|
a69530621d | ||
|
|
3c0f29a768 | ||
|
|
ccf5157fa7 | ||
|
|
c1ebb1c154 | ||
|
|
a4778bcc02 | ||
|
|
24d2c6d8d6 | ||
|
|
0f3a07f602 | ||
|
|
751d8932f8 | ||
|
|
9c2d063c24 | ||
|
|
0f9bbf5b2f | ||
|
|
530280981e | ||
|
|
ddfbb305c7 | ||
|
|
8d1a29922b | ||
|
|
c710f7c6ab | ||
|
|
111c587eef | ||
|
|
264e494487 | ||
|
|
8135b2a3dc | ||
|
|
dce62b70d3 | ||
|
|
533812c638 | ||
|
|
5dc5bed281 | ||
|
|
2a13561962 | ||
|
|
1476f41cda | ||
|
|
0da1ffbe02 | ||
|
|
980679b6a3 | ||
|
|
28cbb55299 | ||
|
|
1a4e7d100a | ||
|
|
265e43b439 | ||
|
|
03a58c3412 | ||
|
|
327cad48f0 | ||
|
|
8e64fe2f56 | ||
|
|
368d4dbb66 | ||
|
|
c8c3c094db | ||
|
|
aa4aefe35f | ||
|
|
536b3d77c1 | ||
|
|
bbf2a80981 | ||
|
|
ae99214d6c | ||
|
|
73b5a0969c | ||
|
|
bf5dc96ac9 | ||
|
|
301d9c2513 | ||
|
|
0a11611b8f | ||
|
|
249e310e43 | ||
|
|
6279c5731a | ||
|
|
df98637951 | ||
|
|
08b3558e46 | ||
|
|
648ef6fdf8 | ||
|
|
807f075a51 | ||
|
|
27789beeed | ||
|
|
97bee27827 | ||
|
|
3fee49301e | ||
|
|
8a6073291b | ||
|
|
47151de4a4 | ||
|
|
248c4edca1 | ||
|
|
2c7386d536 | ||
|
|
e19cad4c87 | ||
|
|
585e0f3372 | ||
|
|
2b7f611774 | ||
|
|
05b2a0d9c9 | ||
|
|
e7287eb088 | ||
|
|
4b45ad3fe7 | ||
|
|
449d8fdbbd | ||
|
|
f44cff9912 | ||
|
|
24730dfde3 | ||
|
|
be6539d41f | ||
|
|
60dd40554e | ||
|
|
837119b6ec | ||
|
|
abe11b82e5 | ||
|
|
4fc1e36d04 | ||
|
|
54aa15e1be | ||
|
|
876d8fc7b3 |
+149
@@ -1,5 +1,154 @@
|
||||
# Change Log
|
||||
|
||||
## 1.72.0
|
||||
* Improve result screen
|
||||
* Update vdstools
|
||||
* Fix camera initialization
|
||||
|
||||
## 1.71.0
|
||||
* Parse VDS and IDB barcodes
|
||||
* Restrict scanning to horizontal crosshair if set
|
||||
* Include barcode format in history search
|
||||
* Add a dedicated barcode icon for PDF417
|
||||
* Add barcode format descriptions
|
||||
* Fix handling deep links
|
||||
* Fix detecting web URLs
|
||||
|
||||
## 1.70.0
|
||||
* Add barcode format icons in history
|
||||
* Add beep audio stream preference
|
||||
* Parse EPC-QR codes and show the data
|
||||
* Improve layout of decode screen
|
||||
* Fix parsing first field of subfile in IDLParser
|
||||
* Use quiet zone from ZXingCpp
|
||||
* Italian language update
|
||||
|
||||
## 1.69.1
|
||||
* Update translations
|
||||
|
||||
## 1.69.0
|
||||
* Add an option to show crosshairs
|
||||
* Add preference for codes that should not be saved
|
||||
* Add option to restrict searching to a specific format
|
||||
* Pin history items
|
||||
* Improve preference icons
|
||||
* Optimize vector drawables
|
||||
* Fix beeping when phone is set to Vibrate mode
|
||||
|
||||
## 1.68.7
|
||||
* Add icons to preferences screen
|
||||
* Update history listing on resume
|
||||
* Fix quiet zone for recreated barcodes
|
||||
* Fix automatic screen brightening
|
||||
* Migrate to CameraX
|
||||
|
||||
## 1.68.6
|
||||
* Update ZXingCpp
|
||||
* Ask for confirmation before deleting a scan from the result screen
|
||||
|
||||
## 1.68.5
|
||||
* Downgrade ZXingCpp for compatibilty
|
||||
|
||||
## 1.68.4
|
||||
* Fix generating 1D barcodes
|
||||
|
||||
## 1.68.3
|
||||
* Stability improvement
|
||||
|
||||
## 1.68.2
|
||||
* Fix barcode regeneration
|
||||
|
||||
## 1.68.1
|
||||
* Improve UI of search barcode dialog
|
||||
* Stability improvements
|
||||
* Update ZXingCpp
|
||||
* Update Russian translation
|
||||
|
||||
## 1.68.0
|
||||
* Add automated actions
|
||||
* Improve managing profiles
|
||||
|
||||
## 1.67.1
|
||||
* Fix opening file:// Uris
|
||||
* Fix auto-return/immediate opening
|
||||
* Improve Bluetooth stability
|
||||
|
||||
## 1.67.0
|
||||
* Add preference profiles
|
||||
* Support regex when searching for a barcode
|
||||
* Improve feedback when searching for a barcode
|
||||
* Update translations
|
||||
|
||||
## 1.66.2
|
||||
* Remove forcing `?content=` on forwarding URL
|
||||
* Update Spanish translation
|
||||
|
||||
## 1.66.1
|
||||
* Show elements of invalid Wifi configurations too
|
||||
* Update translations
|
||||
|
||||
## 1.66.0
|
||||
* Support deep links to encode content via web links
|
||||
* Parse International Driver Licenses (IDL) in PDF417 barcodes
|
||||
* Add WiFis from system settings
|
||||
* Show parsed data items
|
||||
* Update translations
|
||||
|
||||
## 1.65.2
|
||||
* Sanity check ROI metrics
|
||||
|
||||
## 1.65.1
|
||||
* Improve simple mode
|
||||
* Remember quiet zone settings
|
||||
* Reduce height for 1D barcodes
|
||||
* Add execute argument for binaryeye://encode
|
||||
* Update Russian translation
|
||||
|
||||
## 1.65.0
|
||||
* Add a welcome screen to choose a mode
|
||||
* Add a deep link to encode a barcode
|
||||
* Remember zoom level of generated barcodes
|
||||
* Remove Human Readable Interpretation (HRI) decoration
|
||||
* Update Italian translation
|
||||
|
||||
## 1.64.2
|
||||
* Separate crop window settings for image scanning
|
||||
* Fix initializing dataMask
|
||||
* Update Russian translation
|
||||
|
||||
## 1.64.1
|
||||
* Handle empty input for recreation
|
||||
* Fix setting up database
|
||||
|
||||
## 1.64.0
|
||||
* Find barcode by content
|
||||
* Immediately replace toasts
|
||||
* Use detected barcode for recreation
|
||||
* Show data mask of QR Codes
|
||||
* Fix deep linking
|
||||
* Replace margin with quiet zone switch when generating barcodes
|
||||
* Update Russian translation
|
||||
* Update Portuguese (Brazil) translation
|
||||
|
||||
## 1.63.13
|
||||
* Fix UTF-8 conversion of binary escape sequences when creating barcodes
|
||||
* Fix scan order for intervals under one second
|
||||
* Add arabic translation
|
||||
* Update French translation
|
||||
|
||||
## 1.63.12
|
||||
* Fix magnifying barcode recreation
|
||||
|
||||
## 1.63.11
|
||||
* Update parcel tracking URL
|
||||
* Update multiple translations
|
||||
* Only ask for barcode size when exporting/sharing
|
||||
|
||||
## 1.63.10
|
||||
* Use maximum brightness when showing a generated barcode
|
||||
* Allow file input from share menu
|
||||
* Update multiple translations
|
||||
|
||||
## 1.63.9
|
||||
* Export/share as JPEG
|
||||
* Ignore "URL:" before URLs in barcode content
|
||||
|
||||
@@ -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.
|
||||
@@ -83,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
|
||||
@@ -104,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
|
||||
@@ -155,6 +177,21 @@ class YourActivity : AppCompatActivity() {
|
||||
}
|
||||
```
|
||||
|
||||
## 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/
|
||||
@@ -177,6 +214,9 @@ class YourActivity : AppCompatActivity() {
|
||||
[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
|
||||
|
||||
+25
-16
@@ -1,16 +1,15 @@
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
|
||||
android {
|
||||
namespace "de.markusfisch.android.binaryeye"
|
||||
compileSdk sdk_version
|
||||
|
||||
defaultConfig {
|
||||
minSdk 9
|
||||
minSdk 21
|
||||
targetSdk sdk_version
|
||||
|
||||
versionCode 137
|
||||
versionName '1.63.9'
|
||||
versionCode 166
|
||||
versionName '1.72.0'
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
@@ -35,7 +34,7 @@ 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
|
||||
@@ -58,29 +57,39 @@ android {
|
||||
}
|
||||
}
|
||||
|
||||
// Required since Gradle 8.
|
||||
compileOptions {
|
||||
// Required for Gradle 8.
|
||||
sourceCompatibility = 17
|
||||
targetCompatibility = 17
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
targetCompatibility = JavaVersion.VERSION_1_8
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// 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"
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-datetime:0.7.1'
|
||||
implementation "androidx.core:core-ktx:1.17.0"
|
||||
|
||||
// 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 "androidx.appcompat:appcompat:1.7.1"
|
||||
implementation "com.google.android.material:material:1.13.0"
|
||||
implementation "androidx.preference:preference:1.2.1"
|
||||
|
||||
// External
|
||||
implementation 'com.github.markusfisch:CameraView:1.10.0'
|
||||
implementation 'com.github.markusfisch:ScalingImageView:1.4.1'
|
||||
implementation 'com.github.markusfisch:zxing-cpp:v2.2.0.5'
|
||||
implementation 'androidx.camera:camera-core:1.4.2'
|
||||
implementation 'androidx.camera:camera-camera2:1.4.2'
|
||||
implementation 'androidx.camera:camera-lifecycle:1.4.2'
|
||||
implementation 'androidx.camera:camera-view:1.4.2'
|
||||
implementation 'com.github.markusfisch:ScalingImageView:1.4.3'
|
||||
implementation 'com.github.markusfisch:zxing-cpp:v3.0.2.3'
|
||||
implementation 'de.tsenger:vdstools:0.16.0-SNAPSHOT'
|
||||
implementation 'org.bouncycastle:bcpkix-jdk15to18:1.83'
|
||||
|
||||
// Testing
|
||||
testImplementation 'junit:junit:4.13.2'
|
||||
testImplementation 'org.robolectric:robolectric:4.16.1'
|
||||
|
||||
// Debugging
|
||||
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.14'
|
||||
}
|
||||
|
||||
Binary file not shown.
Vendored
+2
@@ -3,3 +3,5 @@
|
||||
-dontwarn android.support.v8.**
|
||||
|
||||
-keep class android.support.v7.widget.SearchView { *; }
|
||||
|
||||
-keep class de.tsenger.vdstools.** { *; }
|
||||
|
||||
@@ -19,8 +19,9 @@
|
||||
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
|
||||
<uses-feature android:name="android.hardware.camera"/>
|
||||
<uses-feature android:name="android.hardware.camera.autofocus"/>
|
||||
<uses-sdk tools:overrideLibrary="android.support.v14.preference"/>
|
||||
<uses-feature android:name="android.hardware.camera.autofocus"
|
||||
android:required="false" />
|
||||
<uses-sdk tools:overrideLibrary="androidx.preference"/>
|
||||
<queries>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
@@ -62,7 +63,7 @@
|
||||
<intent-filter android:label="@string/compose_barcode">
|
||||
<action android:name="android.intent.action.SEND"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<data android:mimeType="text/plain"/>
|
||||
<data android:mimeType="text/*"/>
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="com.google.zxing.client.android.SCAN"/>
|
||||
@@ -75,6 +76,15 @@
|
||||
<data android:host="details" android:scheme="market"
|
||||
android:pathSuffix="id=com.xiaomi.scanner"/>
|
||||
</intent-filter>
|
||||
<intent-filter android:autoVerify="true">
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<category android:name="android.intent.category.BROWSABLE"/>
|
||||
<data android:host="markusfisch.de"/>
|
||||
<data android:pathPrefix="/BinaryEye"/>
|
||||
<data android:scheme="http"/>
|
||||
<data android:scheme="https"/>
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
@@ -99,20 +109,64 @@
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".activity.MainActivity"
|
||||
android:name=".activity.WelcomeActivity"
|
||||
android:exported="true"/>
|
||||
<activity
|
||||
android:name=".activity.PreferencesActivity"
|
||||
android:exported="true"/>
|
||||
<activity
|
||||
android:name=".activity.HistoryActivity"
|
||||
android:exported="true"/>
|
||||
<activity
|
||||
android:name=".activity.EncodeActivity"
|
||||
android:exported="true">
|
||||
<intent-filter android:autoVerify="true">
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<category android:name="android.intent.category.BROWSABLE"/>
|
||||
<data android:host="markusfisch.de"/>
|
||||
<data android:pathPrefix="/encode"/>
|
||||
<data android:scheme="http"/>
|
||||
<data android:scheme="https"/>
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<category android:name="android.intent.category.BROWSABLE"/>
|
||||
<data android:scheme="binaryeye" android:host="encode"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".activity.DecodeActivity"
|
||||
android:exported="false"/>
|
||||
<activity
|
||||
android:name=".activity.BarcodeActivity"
|
||||
android:exported="false"/>
|
||||
<activity
|
||||
android:name=".activity.ProfilesActivity"
|
||||
android:exported="false"/>
|
||||
<activity
|
||||
android:name=".activity.AutomatedActionsActivity"
|
||||
android:exported="false"/>
|
||||
<activity
|
||||
android:name=".activity.IgnoreCodesActivity"
|
||||
android:exported="false"/>
|
||||
<activity
|
||||
android:name=".activity.NetworkSuggestionsActivity"
|
||||
android:exported="false"/>
|
||||
<service
|
||||
android:name=".service.ScanTileService"
|
||||
android:icon="@drawable/ic_action_scan"
|
||||
android:label="@string/scan_code"
|
||||
android:exported="true"
|
||||
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
|
||||
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE"
|
||||
tools:targetApi="n">
|
||||
<intent-filter>
|
||||
<action android:name="android.service.quicksettings.action.QS_TILE"/>
|
||||
</intent-filter>
|
||||
</service>
|
||||
<provider
|
||||
android:name="android.support.v4.content.FileProvider"
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="${applicationId}.provider"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true">
|
||||
|
||||
Binary file not shown.
+11
-9
@@ -6,15 +6,17 @@ import de.markusfisch.android.binaryeye.content.execShareIntent
|
||||
import de.markusfisch.android.binaryeye.content.openUrl
|
||||
import de.markusfisch.android.binaryeye.widget.toast
|
||||
|
||||
interface IAction {
|
||||
val iconResId: Int
|
||||
val titleResId: Int
|
||||
abstract class Action {
|
||||
abstract val iconResId: Int
|
||||
abstract val titleResId: Int
|
||||
|
||||
fun canExecuteOn(data: ByteArray): Boolean
|
||||
suspend fun execute(context: Context, data: ByteArray)
|
||||
var fired: Boolean = false
|
||||
|
||||
abstract fun canExecuteOn(data: ByteArray): Boolean
|
||||
abstract suspend fun execute(context: Context, data: ByteArray)
|
||||
}
|
||||
|
||||
abstract class IntentAction : IAction {
|
||||
abstract class IntentAction : Action() {
|
||||
abstract val errorMsg: Int
|
||||
|
||||
final override suspend fun execute(context: Context, data: ByteArray) {
|
||||
@@ -22,7 +24,7 @@ abstract class IntentAction : IAction {
|
||||
if (intent == null) {
|
||||
context.toast(errorMsg)
|
||||
} else {
|
||||
context.execShareIntent(intent)
|
||||
fired = context.execShareIntent(intent)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,7 +34,7 @@ abstract class IntentAction : IAction {
|
||||
): Intent?
|
||||
}
|
||||
|
||||
abstract class SchemeAction : IAction {
|
||||
abstract class SchemeAction : Action() {
|
||||
abstract val scheme: String
|
||||
open val buildRegex: Boolean = false
|
||||
|
||||
@@ -50,6 +52,6 @@ abstract class SchemeAction : IAction {
|
||||
}
|
||||
|
||||
final override suspend fun execute(context: Context, data: ByteArray) {
|
||||
context.openUrl(String(data))
|
||||
fired = context.openUrl(String(data))
|
||||
}
|
||||
}
|
||||
@@ -12,9 +12,9 @@ import de.markusfisch.android.binaryeye.actions.web.WebAction
|
||||
import de.markusfisch.android.binaryeye.actions.wifi.WifiAction
|
||||
|
||||
object ActionRegistry {
|
||||
val DEFAULT_ACTION: IAction = OpenOrSearchAction
|
||||
val DEFAULT_ACTION: Action = OpenOrSearchAction
|
||||
|
||||
private val REGISTRY: Set<IAction> = setOf(
|
||||
private val REGISTRY: Set<Action> = setOf(
|
||||
MailAction,
|
||||
MatMsgAction,
|
||||
OtpauthAction,
|
||||
@@ -28,7 +28,7 @@ object ActionRegistry {
|
||||
WebAction
|
||||
)
|
||||
|
||||
fun getAction(data: ByteArray): IAction = REGISTRY.find {
|
||||
fun getAction(data: ByteArray): Action = REGISTRY.find {
|
||||
it.canExecuteOn(data)
|
||||
} ?: DEFAULT_ACTION
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ package de.markusfisch.android.binaryeye.actions.mail
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import androidx.core.net.toUri
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.actions.IntentAction
|
||||
|
||||
@@ -24,7 +24,7 @@ object MailAction : IntentAction() {
|
||||
)?.groupValues?.get(1) ?: return null
|
||||
return Intent(
|
||||
Intent.ACTION_SENDTO,
|
||||
Uri.parse("mailto:$mailWithMessage")
|
||||
"mailto:$mailWithMessage".toUri()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ package de.markusfisch.android.binaryeye.actions.mail
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import androidx.core.net.toUri
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.actions.IntentAction
|
||||
|
||||
@@ -26,7 +26,7 @@ object MatMsgAction : IntentAction() {
|
||||
}
|
||||
return Intent(
|
||||
Intent.ACTION_SENDTO,
|
||||
Uri.parse("mailto:")
|
||||
"mailto:".toUri()
|
||||
).apply {
|
||||
mm.to?.let {
|
||||
putExtra(Intent.EXTRA_EMAIL, arrayOf(mm.to))
|
||||
|
||||
+49
-46
@@ -2,62 +2,65 @@ package de.markusfisch.android.binaryeye.actions.search
|
||||
|
||||
import android.content.Context
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.actions.IAction
|
||||
import de.markusfisch.android.binaryeye.actions.Action
|
||||
import de.markusfisch.android.binaryeye.app.alertDialog
|
||||
import de.markusfisch.android.binaryeye.app.prefs
|
||||
import de.markusfisch.android.binaryeye.content.openUrl
|
||||
import de.markusfisch.android.binaryeye.net.urlEncode
|
||||
|
||||
object OpenOrSearchAction : IAction {
|
||||
object OpenOrSearchAction : Action() {
|
||||
override val iconResId: Int = R.drawable.ic_action_search
|
||||
override val titleResId: Int = R.string.search_web
|
||||
|
||||
override fun canExecuteOn(data: ByteArray): Boolean = false
|
||||
|
||||
override suspend fun execute(context: Context, data: ByteArray) {
|
||||
view(context, String(data), true)
|
||||
}
|
||||
|
||||
private suspend fun view(
|
||||
context: Context,
|
||||
url: String,
|
||||
search: Boolean
|
||||
) {
|
||||
if (!context.openUrl(url, silent = true) && search) {
|
||||
openSearch(context, url)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun openSearch(context: Context, query: String) {
|
||||
val defaultSearchUrl = prefs.defaultSearchUrl
|
||||
if (defaultSearchUrl.isNotEmpty()) {
|
||||
view(
|
||||
context,
|
||||
defaultSearchUrl + query.urlEncode(),
|
||||
false
|
||||
)
|
||||
return
|
||||
}
|
||||
val names = context.resources.getStringArray(
|
||||
R.array.search_engines_names
|
||||
).toMutableList()
|
||||
val urls = context.resources.getStringArray(
|
||||
R.array.search_engines_values
|
||||
).toMutableList()
|
||||
// Remove the "Always ask" entry. The arrays search_engines_*
|
||||
// are used in the preferences too.
|
||||
names.removeFirst()
|
||||
urls.removeFirst()
|
||||
if (prefs.openWithUrl.isNotEmpty()) {
|
||||
names.add(prefs.openWithUrl)
|
||||
urls.add(prefs.openWithUrl)
|
||||
}
|
||||
val queryUri = alertDialog<String>(context) { resume ->
|
||||
setTitle(R.string.pick_search_engine)
|
||||
setItems(names.toTypedArray()) { _, which ->
|
||||
resume(urls[which] + query.urlEncode())
|
||||
}
|
||||
} ?: return
|
||||
view(context, queryUri, false)
|
||||
fired = openUrlOrSearch(context, String(data))
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun openUrlOrSearch(
|
||||
context: Context,
|
||||
url: String,
|
||||
search: Boolean = true
|
||||
): Boolean {
|
||||
if (context.openUrl(url, silent = true)) {
|
||||
return true
|
||||
}
|
||||
if (search) {
|
||||
return prependSearchUrl(context, url)?.let {
|
||||
openUrlOrSearch(context, it, false)
|
||||
} ?: false
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private suspend fun prependSearchUrl(
|
||||
context: Context,
|
||||
query: String
|
||||
): String? {
|
||||
val defaultSearchUrl = prefs.defaultSearchUrl
|
||||
if (defaultSearchUrl.isNotEmpty()) {
|
||||
return defaultSearchUrl + query.urlEncode()
|
||||
}
|
||||
val names = context.resources.getStringArray(
|
||||
R.array.search_engines_names
|
||||
).toMutableList()
|
||||
val urls = context.resources.getStringArray(
|
||||
R.array.search_engines_values
|
||||
).toMutableList()
|
||||
// Remove the "Always ask" entry. The arrays search_engines_*
|
||||
// are used in the preferences too.
|
||||
names.removeAt(0)
|
||||
urls.removeAt(0)
|
||||
if (prefs.openWithUrl.isNotEmpty()) {
|
||||
names.add(prefs.openWithUrl)
|
||||
urls.add(prefs.openWithUrl)
|
||||
}
|
||||
return alertDialog(context) { resume ->
|
||||
setTitle(R.string.pick_search_engine)
|
||||
setItems(names.toTypedArray()) { _, which ->
|
||||
resume(urls[which] + query.urlEncode())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ package de.markusfisch.android.binaryeye.actions.sms
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import androidx.core.net.toUri
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.actions.IntentAction
|
||||
|
||||
@@ -27,7 +27,7 @@ object SmsAction : IntentAction() {
|
||||
} ?: return null
|
||||
return Intent(
|
||||
Intent.ACTION_SENDTO,
|
||||
Uri.parse("smsto:$number")
|
||||
"smsto:$number".toUri()
|
||||
).apply {
|
||||
if (message.isNotEmpty()) {
|
||||
putExtra("sms_body", message)
|
||||
|
||||
+27
-30
@@ -3,7 +3,6 @@ package de.markusfisch.android.binaryeye.actions.vtype.vcard
|
||||
import android.content.ContentValues
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.provider.ContactsContract
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.actions.IntentAction
|
||||
@@ -98,38 +97,36 @@ object VCardAction : IntentAction() {
|
||||
}
|
||||
putExtra(ContactsContract.Intents.Insert.POSTAL, addr.value.locationFormat)
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
|
||||
val urls = info["URL"]?.mapNotNull {
|
||||
it.value.takeIf { url -> url.isNotBlank() }?.let { url ->
|
||||
ContentValues(2).apply {
|
||||
put(
|
||||
ContactsContract.Data.MIMETYPE,
|
||||
ContactsContract.CommonDataKinds.Website.CONTENT_ITEM_TYPE
|
||||
)
|
||||
put(ContactsContract.CommonDataKinds.Website.URL, url)
|
||||
}
|
||||
val urls = info["URL"]?.mapNotNull {
|
||||
it.value.takeIf { url -> url.isNotBlank() }?.let { url ->
|
||||
ContentValues(2).apply {
|
||||
put(
|
||||
ContactsContract.Data.MIMETYPE,
|
||||
ContactsContract.CommonDataKinds.Website.CONTENT_ITEM_TYPE
|
||||
)
|
||||
put(ContactsContract.CommonDataKinds.Website.URL, url)
|
||||
}
|
||||
}
|
||||
val bDays = info["BDAY"]?.mapNotNull {
|
||||
it.value.takeIf { day -> day.isNotBlank() }?.let { day ->
|
||||
ContentValues(3).apply {
|
||||
put(
|
||||
ContactsContract.Data.MIMETYPE,
|
||||
ContactsContract.CommonDataKinds.Event.CONTENT_ITEM_TYPE
|
||||
)
|
||||
put(
|
||||
ContactsContract.CommonDataKinds.Event.TYPE,
|
||||
ContactsContract.CommonDataKinds.Event.TYPE_BIRTHDAY
|
||||
)
|
||||
put(ContactsContract.CommonDataKinds.Event.START_DATE, day)
|
||||
}
|
||||
}
|
||||
}
|
||||
putParcelableArrayListExtra(
|
||||
ContactsContract.Intents.Insert.DATA,
|
||||
ArrayList(listOfNotNull(urls, bDays).flatten())
|
||||
)
|
||||
}
|
||||
val bDays = info["BDAY"]?.mapNotNull {
|
||||
it.value.takeIf { day -> day.isNotBlank() }?.let { day ->
|
||||
ContentValues(3).apply {
|
||||
put(
|
||||
ContactsContract.Data.MIMETYPE,
|
||||
ContactsContract.CommonDataKinds.Event.CONTENT_ITEM_TYPE
|
||||
)
|
||||
put(
|
||||
ContactsContract.CommonDataKinds.Event.TYPE,
|
||||
ContactsContract.CommonDataKinds.Event.TYPE_BIRTHDAY
|
||||
)
|
||||
put(ContactsContract.CommonDataKinds.Event.START_DATE, day)
|
||||
}
|
||||
}
|
||||
}
|
||||
putParcelableArrayListExtra(
|
||||
ContactsContract.Intents.Insert.DATA,
|
||||
ArrayList(listOfNotNull(urls, bDays).flatten())
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-7
@@ -2,9 +2,7 @@ package de.markusfisch.android.binaryeye.actions.vtype.vevent
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.provider.CalendarContract
|
||||
import android.support.annotation.RequiresApi
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.actions.IntentAction
|
||||
import de.markusfisch.android.binaryeye.actions.vtype.VTypeParser
|
||||
@@ -21,14 +19,10 @@ object VEventAction : IntentAction() {
|
||||
get() = R.string.vevent_failed
|
||||
|
||||
override fun canExecuteOn(data: ByteArray): Boolean {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
|
||||
return false
|
||||
}
|
||||
val type = VTypeParser.parseVType(String(data))
|
||||
return type == "VEVENT" || type == "VCALENDAR"
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
|
||||
override suspend fun createIntent(context: Context, data: ByteArray): Intent? {
|
||||
val info = VTypeParser.parseMap(String(data))
|
||||
|
||||
@@ -88,6 +82,6 @@ private fun List<String>.simpleFindParse(date: String): Date? {
|
||||
|
||||
private fun SimpleDateFormat.simpleParse(date: String): Date? = try {
|
||||
parse(date)
|
||||
} catch (e: Exception) {
|
||||
} catch (_: Exception) {
|
||||
null
|
||||
}
|
||||
|
||||
@@ -2,12 +2,12 @@ package de.markusfisch.android.binaryeye.actions.web
|
||||
|
||||
import android.content.Context
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.actions.IAction
|
||||
import de.markusfisch.android.binaryeye.actions.Action
|
||||
import de.markusfisch.android.binaryeye.content.openUrl
|
||||
|
||||
object WebAction : IAction {
|
||||
object WebAction : Action() {
|
||||
private val colloquialRegex =
|
||||
"^(URL:[ ]*)*(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
|
||||
@@ -24,6 +24,6 @@ object WebAction : IAction {
|
||||
if (!url.startsWith("http") && !url.startsWith("ftp")) {
|
||||
url = "http://${url}"
|
||||
}
|
||||
context.openUrl(url)
|
||||
fired = context.openUrl(url)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,31 +2,39 @@ package de.markusfisch.android.binaryeye.actions.wifi
|
||||
|
||||
import android.content.Context
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.actions.IAction
|
||||
import de.markusfisch.android.binaryeye.actions.Action
|
||||
import de.markusfisch.android.binaryeye.widget.toast
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
object WifiAction : IAction {
|
||||
object WifiAction : Action() {
|
||||
override val iconResId = R.drawable.ic_action_wifi
|
||||
override val titleResId = R.string.connect_to_wifi
|
||||
|
||||
var password: String? = null
|
||||
|
||||
override fun canExecuteOn(data: ByteArray): Boolean =
|
||||
WifiConnector.parse(String(data)) {
|
||||
password = it
|
||||
} != null
|
||||
override fun canExecuteOn(data: ByteArray): Boolean {
|
||||
// Just check if the data can be parsed as the app should also
|
||||
// show invalid configurations so users can still access them.
|
||||
val inputMap = WifiConnector.parseMap(String(data)) ?: return false
|
||||
password = inputMap["P"]
|
||||
return !inputMap["S"].isNullOrEmpty()
|
||||
}
|
||||
|
||||
override suspend fun execute(
|
||||
context: Context,
|
||||
data: ByteArray
|
||||
) = withContext(Dispatchers.IO) {
|
||||
val message = WifiConnector.parse(String(data))?.let {
|
||||
WifiConnector.addNetwork(context, it)
|
||||
} ?: R.string.wifi_config_failed
|
||||
withContext(Dispatchers.Main) {
|
||||
context.toast(message)
|
||||
override suspend fun execute(context: Context, data: ByteArray) {
|
||||
withContext(Dispatchers.IO) {
|
||||
fired = WifiConnector.parse(String(data))?.let {
|
||||
WifiConnector.addNetwork(context, it)
|
||||
} ?: false
|
||||
withContext(Dispatchers.Main) {
|
||||
context.toast(
|
||||
if (fired) {
|
||||
R.string.wifi_added
|
||||
} else {
|
||||
R.string.wifi_config_failed
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+61
-53
@@ -1,14 +1,15 @@
|
||||
package de.markusfisch.android.binaryeye.actions.wifi
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.wifi.WifiConfiguration
|
||||
import android.net.wifi.WifiEnterpriseConfig
|
||||
import android.net.wifi.WifiManager
|
||||
import android.net.wifi.WifiNetworkSuggestion
|
||||
import android.os.Build
|
||||
import android.support.annotation.RequiresApi
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import java.util.*
|
||||
import android.provider.Settings
|
||||
import androidx.annotation.RequiresApi
|
||||
import java.util.Locale
|
||||
|
||||
/**
|
||||
* Normal:
|
||||
@@ -35,15 +36,9 @@ import java.util.*
|
||||
* The fields can appear in any order. Only "S:" is required.
|
||||
*/
|
||||
object WifiConnector {
|
||||
fun parse(
|
||||
input: String,
|
||||
passwordBlock: ((password: String?) -> Unit)? = null
|
||||
): Any? {
|
||||
fun parse(input: String): Any? {
|
||||
val inputMap = parseMap(input) ?: return null
|
||||
val parsedData = SimpleDataAccessor.of(inputMap) ?: return null
|
||||
passwordBlock?.apply {
|
||||
invoke(parsedData.password)
|
||||
}
|
||||
return try {
|
||||
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q) {
|
||||
// WifiConfiguration is deprecated in Android Q but
|
||||
@@ -54,7 +49,7 @@ object WifiConnector {
|
||||
} else {
|
||||
WifiNetworkSuggestion.Builder().apply(parsedData)
|
||||
}
|
||||
} catch (e: IllegalArgumentException) {
|
||||
} catch (_: IllegalArgumentException) {
|
||||
// Some arguments may be out of range, e.g. SSIDs cannot
|
||||
// be longer than 32 characters in some versions of Android.
|
||||
// See: https://github.com/markusfisch/BinaryEye/issues/402
|
||||
@@ -62,24 +57,26 @@ object WifiConnector {
|
||||
}
|
||||
}
|
||||
|
||||
fun addNetwork(context: Context, config: Any): Int {
|
||||
fun addNetwork(context: Context, config: Any): Boolean {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R &&
|
||||
config is WifiNetworkSuggestion.Builder
|
||||
) {
|
||||
context.suggestNetworkFromBuilder(config)
|
||||
return true
|
||||
}
|
||||
|
||||
val wifiManager = context.applicationContext.getSystemService(
|
||||
Context.WIFI_SERVICE
|
||||
) as WifiManager
|
||||
// WifiConfiguration is deprecated in Android Q.
|
||||
@Suppress("DEPRECATION")
|
||||
return if ((config is WifiConfiguration &&
|
||||
wifiManager.enableWifi() &&
|
||||
wifiManager.removeOldNetwork(config) &&
|
||||
wifiManager.enableNewNetwork(config)) ||
|
||||
(Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q &&
|
||||
config is WifiNetworkSuggestion.Builder &&
|
||||
wifiManager.addNetworkFromBuilder(config))
|
||||
) {
|
||||
R.string.wifi_added
|
||||
} else {
|
||||
R.string.wifi_config_failed
|
||||
}
|
||||
return (config is WifiConfiguration &&
|
||||
wifiManager.enableWifi() &&
|
||||
wifiManager.removeOldNetwork(config) &&
|
||||
wifiManager.enableNewNetwork(config)) ||
|
||||
(Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q &&
|
||||
config is WifiNetworkSuggestion.Builder &&
|
||||
wifiManager.addNetworkFromBuilder(config))
|
||||
}
|
||||
|
||||
internal fun parseMap(input: String): Map<String, String>? {
|
||||
@@ -93,7 +90,7 @@ object WifiConnector {
|
||||
input.trimAndTerminate()
|
||||
)?.groupValues?.get(1)?.let { pairs ->
|
||||
pairRegex.findAll(pairs).map { pair ->
|
||||
pair.groupValues[1].uppercase(Locale.US) to pair.groupValues[2]
|
||||
pair.groupValues[1].uppercase(Locale.US) to pair.groupValues[2].unescape
|
||||
}.toMap()
|
||||
}
|
||||
}
|
||||
@@ -103,47 +100,43 @@ object WifiConnector {
|
||||
) {
|
||||
// This should be private but because of testing it isn't possible.
|
||||
internal val ssid: String
|
||||
get() = inputMap.getValue("S").unescape
|
||||
get() = inputMap.getValue("S")
|
||||
internal val securityType: String
|
||||
get() = inputMap["T"]?.unescape ?: ""
|
||||
get() = inputMap["T"] ?: ""
|
||||
internal val password: String?
|
||||
get() = inputMap["P"]?.unescape
|
||||
get() = inputMap["P"]
|
||||
internal val hidden: Boolean
|
||||
get() = inputMap["H"]?.unescape == "true"
|
||||
get() = inputMap["H"] == "true"
|
||||
internal val anonymousIdentity: String
|
||||
get() = inputMap["AI"]?.unescape ?: inputMap["A"]?.unescape ?: ""
|
||||
get() = inputMap["AI"] ?: inputMap["A"] ?: ""
|
||||
internal val identity: String
|
||||
get() = inputMap["I"]?.unescape ?: ""
|
||||
get() = inputMap["I"] ?: ""
|
||||
internal val eapMethod: Int?
|
||||
get() = if (inputMap["E"].isNullOrEmpty()) {
|
||||
requireSdk(Build.VERSION_CODES.JELLY_BEAN_MR2) {
|
||||
WifiEnterpriseConfig.Eap.NONE
|
||||
}
|
||||
WifiEnterpriseConfig.Eap.NONE
|
||||
} else when (inputMap["E"]?.uppercase()) {
|
||||
"AKA" -> requireSdk(Build.VERSION_CODES.LOLLIPOP) { WifiEnterpriseConfig.Eap.AKA }
|
||||
"AKA" -> WifiEnterpriseConfig.Eap.AKA
|
||||
"AKA_PRIME" -> requireSdk(Build.VERSION_CODES.M) { WifiEnterpriseConfig.Eap.AKA_PRIME }
|
||||
"NONE" -> requireSdk(Build.VERSION_CODES.JELLY_BEAN_MR2) { WifiEnterpriseConfig.Eap.NONE }
|
||||
"PEAP" -> requireSdk(Build.VERSION_CODES.JELLY_BEAN_MR2) { WifiEnterpriseConfig.Eap.PEAP }
|
||||
"PWD" -> requireSdk(Build.VERSION_CODES.JELLY_BEAN_MR2) { WifiEnterpriseConfig.Eap.PWD }
|
||||
"SIM" -> requireSdk(Build.VERSION_CODES.LOLLIPOP) { WifiEnterpriseConfig.Eap.SIM }
|
||||
"TLS" -> requireSdk(Build.VERSION_CODES.JELLY_BEAN_MR2) { WifiEnterpriseConfig.Eap.TLS }
|
||||
"TTLS" -> requireSdk(Build.VERSION_CODES.JELLY_BEAN_MR2) { WifiEnterpriseConfig.Eap.TTLS }
|
||||
"NONE" -> WifiEnterpriseConfig.Eap.NONE
|
||||
"PEAP" -> WifiEnterpriseConfig.Eap.PEAP
|
||||
"PWD" -> WifiEnterpriseConfig.Eap.PWD
|
||||
"SIM" -> WifiEnterpriseConfig.Eap.SIM
|
||||
"TLS" -> WifiEnterpriseConfig.Eap.TLS
|
||||
"TTLS" -> WifiEnterpriseConfig.Eap.TTLS
|
||||
"UNAUTH_TLS" -> requireSdk(Build.VERSION_CODES.N) { WifiEnterpriseConfig.Eap.UNAUTH_TLS }
|
||||
else -> null
|
||||
}
|
||||
internal val phase2Method: Int?
|
||||
get() = if (inputMap["PH2"].isNullOrEmpty()) {
|
||||
requireSdk(Build.VERSION_CODES.JELLY_BEAN_MR2) {
|
||||
WifiEnterpriseConfig.Phase2.NONE
|
||||
}
|
||||
WifiEnterpriseConfig.Phase2.NONE
|
||||
} else when (inputMap["PH2"]?.uppercase()) {
|
||||
"AKA" -> requireSdk(Build.VERSION_CODES.O) { WifiEnterpriseConfig.Phase2.AKA }
|
||||
"AKA_PRIME" -> requireSdk(Build.VERSION_CODES.O) { WifiEnterpriseConfig.Phase2.AKA_PRIME }
|
||||
"GTC" -> requireSdk(Build.VERSION_CODES.JELLY_BEAN_MR2) { WifiEnterpriseConfig.Phase2.GTC }
|
||||
"MSCHAP" -> requireSdk(Build.VERSION_CODES.JELLY_BEAN_MR2) { WifiEnterpriseConfig.Phase2.MSCHAP }
|
||||
"MSCHAPV2" -> requireSdk(Build.VERSION_CODES.JELLY_BEAN_MR2) { WifiEnterpriseConfig.Phase2.MSCHAPV2 }
|
||||
"NONE" -> requireSdk(Build.VERSION_CODES.JELLY_BEAN_MR2) { WifiEnterpriseConfig.Phase2.NONE }
|
||||
"PAP" -> requireSdk(Build.VERSION_CODES.JELLY_BEAN_MR2) { WifiEnterpriseConfig.Phase2.PAP }
|
||||
"GTC" -> WifiEnterpriseConfig.Phase2.GTC
|
||||
"MSCHAP" -> WifiEnterpriseConfig.Phase2.MSCHAP
|
||||
"MSCHAPV2" -> WifiEnterpriseConfig.Phase2.MSCHAPV2
|
||||
"NONE" -> WifiEnterpriseConfig.Phase2.NONE
|
||||
"PAP" -> WifiEnterpriseConfig.Phase2.PAP
|
||||
"SIM" -> requireSdk(Build.VERSION_CODES.O) { WifiEnterpriseConfig.Phase2.SIM }
|
||||
else -> null
|
||||
}
|
||||
@@ -201,7 +194,7 @@ object WifiConnector {
|
||||
})
|
||||
}
|
||||
}
|
||||
} catch (e: IllegalArgumentException) {
|
||||
} catch (_: IllegalArgumentException) {
|
||||
// Don't accept passphrases that aren't ASCII encodeable.
|
||||
return null
|
||||
}
|
||||
@@ -268,7 +261,7 @@ object WifiConnector {
|
||||
allowedGroupCiphers.set(WifiConfiguration.GroupCipher.CCMP)
|
||||
}
|
||||
|
||||
"WPA2-EAP" -> requireSdk(Build.VERSION_CODES.JELLY_BEAN_MR2) {
|
||||
"WPA2-EAP" -> {
|
||||
passwordWithQuotes?.let {
|
||||
preSharedKey = it
|
||||
} ?: return null
|
||||
@@ -288,7 +281,7 @@ object WifiConnector {
|
||||
data.eapMethod ?: return null // non valid eapMethod
|
||||
enterpriseConfig.phase2Method =
|
||||
data.phase2Method ?: return null // non valid phase2Method
|
||||
} ?: return null // api isn't high enough
|
||||
}
|
||||
}
|
||||
return this
|
||||
}
|
||||
@@ -313,6 +306,21 @@ private fun WifiManager.addNetworkFromBuilder(
|
||||
return result == WifiManager.STATUS_NETWORK_SUGGESTIONS_SUCCESS
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.R)
|
||||
private fun Context.suggestNetworkFromBuilder(
|
||||
builder: WifiNetworkSuggestion.Builder
|
||||
) {
|
||||
val suggestion = builder.build()
|
||||
startActivity(
|
||||
Intent(Settings.ACTION_WIFI_ADD_NETWORKS).apply {
|
||||
putParcelableArrayListExtra(
|
||||
Settings.EXTRA_WIFI_NETWORK_LIST,
|
||||
arrayListOf(suggestion)
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// Make sure the string ends with a semicolon.
|
||||
private fun String.trimAndTerminate(): String {
|
||||
var trimmed = trim()
|
||||
@@ -366,7 +374,7 @@ private fun WifiManager.removeOldNetwork(
|
||||
}?.networkId?.also {
|
||||
removeNetwork(it)
|
||||
}
|
||||
} catch (e: SecurityException) {
|
||||
} catch (_: SecurityException) {
|
||||
// The user didn't allow ACCESS_FINE_LOCATION which is
|
||||
// required to access configuredNetworks and that's fine.
|
||||
}
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
package de.markusfisch.android.binaryeye.activity
|
||||
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import androidx.annotation.LayoutRes
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.view.isNotEmpty
|
||||
import de.markusfisch.android.binaryeye.app.PERMISSION_LOCATION
|
||||
import de.markusfisch.android.binaryeye.app.PERMISSION_WRITE
|
||||
import de.markusfisch.android.binaryeye.app.applyLocale
|
||||
import de.markusfisch.android.binaryeye.app.permissionGrantedCallback
|
||||
import de.markusfisch.android.binaryeye.app.prefs
|
||||
import de.markusfisch.android.binaryeye.view.initBars
|
||||
|
||||
abstract class AbstractBaseActivity : AppCompatActivity() {
|
||||
override fun attachBaseContext(base: Context?) {
|
||||
base?.applyLocale(prefs.customLocale)
|
||||
super.attachBaseContext(base)
|
||||
}
|
||||
|
||||
protected fun setScreenContentView(@LayoutRes layoutId: Int) {
|
||||
setContentView(layoutId)
|
||||
initBars()
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
}
|
||||
|
||||
override fun onSupportNavigateUp(): Boolean {
|
||||
finish()
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onRequestPermissionsResult(
|
||||
requestCode: Int,
|
||||
permissions: Array<String>,
|
||||
grantResults: IntArray
|
||||
) {
|
||||
super.onRequestPermissionsResult(
|
||||
requestCode,
|
||||
permissions,
|
||||
grantResults
|
||||
)
|
||||
when (requestCode) {
|
||||
PERMISSION_LOCATION, PERMISSION_WRITE -> {
|
||||
if (grantResults.isNotEmpty() &&
|
||||
grantResults[0] == PackageManager.PERMISSION_GRANTED
|
||||
) {
|
||||
permissionGrantedCallback?.invoke()
|
||||
permissionGrantedCallback = null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||
onCreateOptionsMenu(menu, menuInflater)
|
||||
return menu.isNotEmpty() || super.onCreateOptionsMenu(menu)
|
||||
}
|
||||
|
||||
protected open fun onCreateOptionsMenu(
|
||||
menu: Menu,
|
||||
inflater: MenuInflater
|
||||
) = Unit
|
||||
}
|
||||
+304
@@ -0,0 +1,304 @@
|
||||
package de.markusfisch.android.binaryeye.activity
|
||||
|
||||
import android.app.AlertDialog
|
||||
import android.app.Dialog
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.EditText
|
||||
import android.widget.ListView
|
||||
import android.widget.RadioGroup
|
||||
import android.widget.Spinner
|
||||
import android.widget.TextView
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.adapter.prettifyFormatName
|
||||
import de.markusfisch.android.binaryeye.app.prefs
|
||||
import de.markusfisch.android.binaryeye.automation.AutomatedAction
|
||||
import de.markusfisch.android.binaryeye.view.setPaddingFromWindowInsets
|
||||
import de.markusfisch.android.binaryeye.view.systemBarListViewScrollListener
|
||||
|
||||
class AutomatedActionsActivity : AbstractBaseActivity() {
|
||||
private val actions = ArrayList<AutomatedAction>()
|
||||
|
||||
private lateinit var adapter: ActionsAdapter
|
||||
|
||||
override fun onCreate(state: Bundle?) {
|
||||
super.onCreate(state)
|
||||
setScreenContentView(R.layout.activity_automated_actions)
|
||||
setTitle(R.string.automated_actions)
|
||||
|
||||
actions.clear()
|
||||
actions.addAll(prefs.automatedActions)
|
||||
|
||||
val listView = findViewById<ListView>(R.id.actions)
|
||||
listView.emptyView = findViewById(R.id.no_actions)
|
||||
adapter = ActionsAdapter(this, actions)
|
||||
listView.adapter = adapter
|
||||
listView.setOnScrollListener(systemBarListViewScrollListener)
|
||||
listView.setOnItemClickListener { _, _, position, _ ->
|
||||
editAction(position)
|
||||
}
|
||||
listView.setOnItemLongClickListener { _, _, position, _ ->
|
||||
confirmRemoveAction(position)
|
||||
true
|
||||
}
|
||||
|
||||
findViewById<View>(R.id.add).setOnClickListener {
|
||||
editAction(null)
|
||||
}
|
||||
|
||||
findViewById<View>(R.id.inset_layout).setPaddingFromWindowInsets()
|
||||
listView.setPaddingFromWindowInsets()
|
||||
}
|
||||
|
||||
private fun editAction(position: Int?) {
|
||||
val action = position?.let { actions[it] }
|
||||
val view = layoutInflater.inflate(
|
||||
R.layout.dialog_automated_action,
|
||||
null
|
||||
)
|
||||
val regexView = view.findViewById<EditText>(R.id.regex)
|
||||
val formatView = view.findViewById<Spinner>(R.id.format)
|
||||
val urlView = view.findViewById<EditText>(R.id.url_template)
|
||||
val typeGroup = view.findViewById<RadioGroup>(R.id.action_type)
|
||||
val urlSection = view.findViewById<View>(R.id.url_section)
|
||||
val intentSection = view.findViewById<View>(R.id.intent_section)
|
||||
val intentUriView = view.findViewById<EditText>(
|
||||
R.id.intent_uri_template
|
||||
)
|
||||
val names = resources.getStringArray(
|
||||
R.array.barcode_formats_names
|
||||
).toMutableList().apply {
|
||||
add(0, getString(R.string.all_formats))
|
||||
}
|
||||
val values = resources.getStringArray(
|
||||
R.array.barcode_formats_values
|
||||
).toMutableList().apply {
|
||||
add(0, "")
|
||||
}
|
||||
formatView.adapter = ArrayAdapter(
|
||||
this,
|
||||
android.R.layout.simple_spinner_item,
|
||||
names
|
||||
).apply {
|
||||
setDropDownViewResource(
|
||||
android.R.layout.simple_spinner_dropdown_item
|
||||
)
|
||||
}
|
||||
|
||||
if (action != null) {
|
||||
regexView.setText(action.pattern)
|
||||
formatView.setSelection(
|
||||
values.indexOf(action.format ?: "").coerceAtLeast(0)
|
||||
)
|
||||
when (action.type) {
|
||||
AutomatedAction.Type.Intent -> {
|
||||
urlView.setText(action.urlTemplate ?: "")
|
||||
}
|
||||
|
||||
AutomatedAction.Type.CustomIntent -> {
|
||||
intentUriView.setText(action.intentUriTemplate ?: "")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
formatView.setSelection(0)
|
||||
}
|
||||
|
||||
fun updateTypeSections(checkedId: Int) {
|
||||
val isIntent = checkedId == R.id.action_type_intent
|
||||
urlSection.visibility = if (isIntent) View.VISIBLE else View.GONE
|
||||
intentSection.visibility = if (isIntent) View.GONE else View.VISIBLE
|
||||
}
|
||||
|
||||
typeGroup.setOnCheckedChangeListener { _, checkedId ->
|
||||
updateTypeSections(checkedId)
|
||||
}
|
||||
val initialTypeId = if (
|
||||
action?.type == AutomatedAction.Type.CustomIntent
|
||||
) {
|
||||
R.id.action_type_custom_intent
|
||||
} else {
|
||||
R.id.action_type_intent
|
||||
}
|
||||
typeGroup.check(initialTypeId)
|
||||
updateTypeSections(initialTypeId)
|
||||
|
||||
val dialog = AlertDialog.Builder(this)
|
||||
.setTitle(
|
||||
if (action == null) {
|
||||
R.string.automated_action_add
|
||||
} else {
|
||||
R.string.automated_action_edit
|
||||
}
|
||||
)
|
||||
.setView(view)
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
||||
.create()
|
||||
|
||||
dialog.setOnShowListener {
|
||||
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener {
|
||||
addOrUpdateAction(
|
||||
position,
|
||||
regexView,
|
||||
formatView,
|
||||
values,
|
||||
urlView,
|
||||
typeGroup,
|
||||
intentUriView,
|
||||
dialog
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
dialog.show()
|
||||
}
|
||||
|
||||
private fun addOrUpdateAction(
|
||||
position: Int?,
|
||||
regexView: EditText,
|
||||
formatView: Spinner,
|
||||
values: List<String>,
|
||||
urlView: EditText,
|
||||
typeGroup: RadioGroup,
|
||||
intentUriView: EditText,
|
||||
dialog: Dialog,
|
||||
) {
|
||||
val pattern = regexView.text.toString().trim()
|
||||
if (pattern.isEmpty()) {
|
||||
regexView.error = getString(R.string.automated_action_regex_required)
|
||||
return
|
||||
}
|
||||
try {
|
||||
Regex(pattern)
|
||||
} catch (_: Exception) {
|
||||
regexView.error = getString(R.string.automated_action_regex_invalid)
|
||||
return
|
||||
}
|
||||
val isIntent = typeGroup.checkedRadioButtonId == R.id.action_type_intent
|
||||
val format = formatView.selectedItemPosition.let {
|
||||
values[it].ifEmpty { null }
|
||||
}
|
||||
val newAction = if (isIntent) {
|
||||
val urlTemplate = urlView.text.toString().trim()
|
||||
if (urlTemplate.isEmpty()) {
|
||||
urlView.error = getString(R.string.automated_action_url_required)
|
||||
return
|
||||
}
|
||||
AutomatedAction(
|
||||
pattern = pattern,
|
||||
type = AutomatedAction.Type.Intent,
|
||||
format = format,
|
||||
urlTemplate = urlTemplate
|
||||
)
|
||||
} else {
|
||||
val intentUriTemplate = intentUriView.text.toString().trim()
|
||||
if (intentUriTemplate.isEmpty()) {
|
||||
intentUriView.error = getString(
|
||||
R.string.automated_action_intent_uri_required
|
||||
)
|
||||
return
|
||||
}
|
||||
if (!intentUriTemplate.startsWith(
|
||||
"intent:",
|
||||
ignoreCase = true
|
||||
)
|
||||
) {
|
||||
intentUriView.error = getString(
|
||||
R.string.automated_action_intent_uri_invalid
|
||||
)
|
||||
return
|
||||
}
|
||||
AutomatedAction(
|
||||
pattern = pattern,
|
||||
type = AutomatedAction.Type.CustomIntent,
|
||||
format = format,
|
||||
intentUriTemplate = intentUriTemplate
|
||||
)
|
||||
}
|
||||
if (position == null) {
|
||||
actions.add(newAction)
|
||||
} else {
|
||||
actions[position] = newAction
|
||||
}
|
||||
prefs.setAutomatedActions(actions)
|
||||
adapter.notifyDataSetChanged()
|
||||
dialog.dismiss()
|
||||
}
|
||||
|
||||
private fun confirmRemoveAction(position: Int) {
|
||||
AlertDialog.Builder(this)
|
||||
.setMessage(R.string.automated_action_remove_confirm)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
actions.removeAt(position)
|
||||
prefs.setAutomatedActions(actions)
|
||||
adapter.notifyDataSetChanged()
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
||||
.show()
|
||||
}
|
||||
|
||||
private class ActionsAdapter(
|
||||
context: Context,
|
||||
private val items: List<AutomatedAction>
|
||||
) : ArrayAdapter<AutomatedAction>(
|
||||
context,
|
||||
android.R.layout.simple_list_item_2,
|
||||
android.R.id.text1,
|
||||
items
|
||||
) {
|
||||
override fun getView(
|
||||
position: Int,
|
||||
convertView: View?,
|
||||
parent: ViewGroup
|
||||
): View {
|
||||
val view = super.getView(position, convertView, parent)
|
||||
val action = items[position]
|
||||
getViewHolder(view).apply {
|
||||
title.text = action.pattern
|
||||
subtitle.text = when (action.type) {
|
||||
AutomatedAction.Type.Intent -> if (!action.urlTemplate.isNullOrEmpty()) {
|
||||
context.getString(
|
||||
R.string.automated_action_list_intent,
|
||||
action.urlTemplate
|
||||
)
|
||||
} else {
|
||||
context.getString(R.string.automated_action_list_intent_empty)
|
||||
}
|
||||
|
||||
AutomatedAction.Type.CustomIntent ->
|
||||
if (!action.intentUriTemplate.isNullOrEmpty()) {
|
||||
context.getString(
|
||||
R.string.automated_action_list_custom_intent,
|
||||
action.intentUriTemplate
|
||||
)
|
||||
} else {
|
||||
context.getString(
|
||||
R.string.automated_action_list_custom_intent_empty
|
||||
)
|
||||
}
|
||||
}
|
||||
action.format?.let { format ->
|
||||
subtitle.text = "${subtitle.text} (${format.prettifyFormatName()})"
|
||||
}
|
||||
}
|
||||
return view
|
||||
}
|
||||
|
||||
private fun getViewHolder(
|
||||
view: View
|
||||
): ViewHolder = view.tag as ViewHolder? ?: ViewHolder(
|
||||
view.findViewById(android.R.id.text1),
|
||||
view.findViewById(android.R.id.text2),
|
||||
).also {
|
||||
view.tag = it
|
||||
}
|
||||
|
||||
private data class ViewHolder(
|
||||
val title: TextView,
|
||||
val subtitle: TextView,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,594 @@
|
||||
package de.markusfisch.android.binaryeye.activity
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.AlertDialog
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Matrix
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.widget.EditText
|
||||
import android.widget.ImageView
|
||||
import android.widget.SeekBar
|
||||
import android.widget.TextView
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.app.db
|
||||
import de.markusfisch.android.binaryeye.app.hasWritePermission
|
||||
import de.markusfisch.android.binaryeye.app.prefs
|
||||
import de.markusfisch.android.binaryeye.content.Barcode
|
||||
import de.markusfisch.android.binaryeye.content.BarcodeColors
|
||||
import de.markusfisch.android.binaryeye.content.BitMatrixBarcode
|
||||
import de.markusfisch.android.binaryeye.content.ContentBarcode
|
||||
import de.markusfisch.android.binaryeye.content.copyToClipboard
|
||||
import de.markusfisch.android.binaryeye.content.shareFile
|
||||
import de.markusfisch.android.binaryeye.content.shareText
|
||||
import de.markusfisch.android.binaryeye.database.toScan
|
||||
import de.markusfisch.android.binaryeye.io.addSuffixIfNotGiven
|
||||
import de.markusfisch.android.binaryeye.io.toSaveResult
|
||||
import de.markusfisch.android.binaryeye.io.writeExternalFile
|
||||
import de.markusfisch.android.binaryeye.net.createEncodeDeeplink
|
||||
import de.markusfisch.android.binaryeye.os.getScreenBrightness
|
||||
import de.markusfisch.android.binaryeye.os.setScreenBrightness
|
||||
import de.markusfisch.android.binaryeye.view.doOnApplyWindowInsets
|
||||
import de.markusfisch.android.binaryeye.view.setPaddingFromWindowInsets
|
||||
import de.markusfisch.android.binaryeye.widget.ConfinedScalingImageView
|
||||
import de.markusfisch.android.binaryeye.widget.toast
|
||||
import de.markusfisch.android.zxingcpp.ZxingCpp
|
||||
import de.markusfisch.android.zxingcpp.ZxingCpp.BarcodeFormat
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
import java.io.OutputStream
|
||||
import java.util.Locale
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
|
||||
class BarcodeActivity : AbstractBaseActivity() {
|
||||
private enum class FileType {
|
||||
PNG, JPG, SVG, TXT
|
||||
}
|
||||
|
||||
private val parentJob = Job()
|
||||
private val scope = CoroutineScope(Dispatchers.IO + parentJob)
|
||||
|
||||
private lateinit var imageView: ConfinedScalingImageView
|
||||
private lateinit var barcode: Barcode<*>
|
||||
|
||||
private var contentView: View? = null
|
||||
private var addToHistoryItem: MenuItem? = null
|
||||
private var brightenScreenItem: MenuItem? = null
|
||||
private var currentBrightness = -1f
|
||||
|
||||
override fun onCreate(state: Bundle?) {
|
||||
super.onCreate(state)
|
||||
setScreenContentView(R.layout.activity_barcode)
|
||||
setTitle(R.string.view_barcode)
|
||||
contentView = findViewById(android.R.id.content)
|
||||
|
||||
// Make `imageView` available for `onPause()`.
|
||||
imageView = findViewById(R.id.barcode)
|
||||
|
||||
val bitmap: Bitmap
|
||||
try {
|
||||
barcode = intent?.extras?.toBarcode() ?: throw IllegalArgumentException(
|
||||
"Illegal arguments"
|
||||
)
|
||||
// Catch exceptions from encoding.
|
||||
bitmap = barcode.bitmap()
|
||||
} catch (e: Exception) {
|
||||
var message = e.message
|
||||
if (message.isNullOrEmpty()) {
|
||||
message = getString(R.string.error_encoding_barcode)
|
||||
}
|
||||
toast(message)
|
||||
finish()
|
||||
return
|
||||
}
|
||||
|
||||
imageView.setImageBitmap(bitmap)
|
||||
imageView.runAfterLayout = {
|
||||
val scale = prefs.previewScale
|
||||
if (scale > 0) {
|
||||
imageView.setScale(
|
||||
scale,
|
||||
bitmap.width / 2f,
|
||||
bitmap.height / 2f
|
||||
)
|
||||
}
|
||||
|
||||
imageView.minWidth = min(
|
||||
imageView.minWidth / 2f,
|
||||
max(bitmap.width, bitmap.height).toFloat()
|
||||
)
|
||||
}
|
||||
|
||||
findViewById<View>(R.id.share).setOnClickListener {
|
||||
it.context?.apply {
|
||||
pickFileType(R.string.share_as) { fileType ->
|
||||
shareAs(fileType)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
findViewById<View>(R.id.inset_layout).setPaddingFromWindowInsets()
|
||||
imageView.doOnApplyWindowInsets { _, insets ->
|
||||
imageView.insets.set(insets)
|
||||
}
|
||||
}
|
||||
|
||||
private fun Bundle.toBarcode(): Barcode<*> {
|
||||
val content =
|
||||
getString(CONTENT_TEXT) ?: getByteArray(CONTENT_RAW) ?: throw IllegalArgumentException(
|
||||
"content cannot be null"
|
||||
)
|
||||
val barcodeFormat = BarcodeFormat.valueOf(
|
||||
getString(FORMAT) ?: throw IllegalArgumentException(
|
||||
"format cannot be null"
|
||||
)
|
||||
)
|
||||
val colors = BarcodeColors.entries[getInt(COLORS)]
|
||||
val data = getByteArray(BIT_MATRIX_DATA)
|
||||
return if (data != null) {
|
||||
BitMatrixBarcode(
|
||||
ZxingCpp.BitMatrix(
|
||||
getInt(BIT_MATRIX_WIDTH),
|
||||
getInt(BIT_MATRIX_HEIGHT),
|
||||
data
|
||||
),
|
||||
content,
|
||||
barcodeFormat,
|
||||
colors
|
||||
)
|
||||
} else {
|
||||
ContentBarcode(
|
||||
content,
|
||||
barcodeFormat,
|
||||
getInt(EC_LEVEL),
|
||||
getBoolean(ADD_QUIET_ZONE),
|
||||
colors
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
if (prefs.brightenScreen) {
|
||||
// Post to make sure brightenScreenItem is initialized.
|
||||
contentView?.postDelayed({
|
||||
brightenScreen()
|
||||
}, 500)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
if (currentBrightness > -1f) {
|
||||
restoreScreenBrightness()
|
||||
}
|
||||
if (imageView.isInBounds()) {
|
||||
prefs.previewScale = imageView.getScale()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
parentJob.cancel()
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
inflater.inflate(R.menu.fragment_barcode, menu)
|
||||
addToHistoryItem = menu.findItem(R.id.add_to_history)
|
||||
brightenScreenItem = menu.findItem(R.id.brighten_screen)
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
return when (item.itemId) {
|
||||
R.id.add_to_history -> {
|
||||
readAndAddToHistory(
|
||||
barcode.bitmap(),
|
||||
barcode.format
|
||||
)
|
||||
addToHistoryItem?.isVisible = false
|
||||
toast(R.string.added_to_history)
|
||||
true
|
||||
}
|
||||
|
||||
R.id.copy_to_clipboard -> {
|
||||
copyToClipboard(barcode.text())
|
||||
true
|
||||
}
|
||||
|
||||
R.id.copy_as_deeplink -> {
|
||||
copyToClipboard(deeplinkToCopy())
|
||||
true
|
||||
}
|
||||
|
||||
R.id.export_to_file -> {
|
||||
pickFileType(R.string.export_as) { fileType ->
|
||||
when (fileType) {
|
||||
FileType.PNG,
|
||||
FileType.JPG -> askForSize { size ->
|
||||
askForFileNameAndSave(fileType, size)
|
||||
}
|
||||
|
||||
else -> askForFileNameAndSave(fileType, 0)
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
R.id.brighten_screen -> {
|
||||
prefs.brightenScreen = if (currentBrightness > -1f) {
|
||||
restoreScreenBrightness()
|
||||
false
|
||||
} else {
|
||||
brightenScreen()
|
||||
true
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
}
|
||||
|
||||
private fun copyToClipboard(text: String, isSensitive: Boolean = false) {
|
||||
(this as android.content.Context).copyToClipboard(text, isSensitive)
|
||||
toast(R.string.copied_to_clipboard)
|
||||
}
|
||||
|
||||
private fun deeplinkToCopy() = createEncodeDeeplink(
|
||||
format = barcode.format.name,
|
||||
content = barcode.textOrHex(),
|
||||
)
|
||||
|
||||
private fun Context.pickFileType(
|
||||
title: Int,
|
||||
action: (FileType) -> Unit
|
||||
) {
|
||||
val fileTypes = FileType.entries.toTypedArray()
|
||||
AlertDialog.Builder(this)
|
||||
.setTitle(title)
|
||||
.setItems(fileTypes.map { it.name }.toTypedArray()) { _, which ->
|
||||
action(fileTypes[which])
|
||||
}
|
||||
.show()
|
||||
}
|
||||
|
||||
// Dialogs do not have a parent view.
|
||||
@SuppressLint("InflateParams")
|
||||
private fun askForFileNameAndSave(fileType: FileType, bitmapSize: Int) {
|
||||
// Write permission is only required before Android Q.
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q &&
|
||||
!hasWritePermission {
|
||||
askForFileNameAndSave(fileType, bitmapSize)
|
||||
}
|
||||
) {
|
||||
return
|
||||
}
|
||||
val view = layoutInflater.inflate(R.layout.dialog_save_file, null)
|
||||
val editText = view.findViewById<EditText>(R.id.file_name)
|
||||
editText.setText(
|
||||
encodeFileName("${barcode.format}_${barcode.content}")
|
||||
)
|
||||
AlertDialog.Builder(this)
|
||||
.setView(view)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
val fileName = editText.text.toString()
|
||||
when (fileType) {
|
||||
FileType.PNG -> saveAs(
|
||||
addSuffixIfNotGiven(fileName, ".png"),
|
||||
MIME_PNG
|
||||
) {
|
||||
barcode.bitmap(bitmapSize).saveAsPng(it)
|
||||
}
|
||||
|
||||
FileType.JPG -> saveAs(
|
||||
addSuffixIfNotGiven(fileName, ".jpg"),
|
||||
MIME_JPG
|
||||
) {
|
||||
barcode.bitmap(bitmapSize).saveAsJpg(it)
|
||||
}
|
||||
|
||||
FileType.SVG -> saveAs(
|
||||
addSuffixIfNotGiven(fileName, ".svg"),
|
||||
MIME_SVG
|
||||
) {
|
||||
it.write(barcode.svg().toByteArray())
|
||||
}
|
||||
|
||||
FileType.TXT -> saveAs(
|
||||
addSuffixIfNotGiven(fileName, ".txt"),
|
||||
MIME_TXT
|
||||
) {
|
||||
it.write(barcode.text().toByteArray())
|
||||
}
|
||||
}
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||
}
|
||||
.show()
|
||||
}
|
||||
|
||||
// Dialogs do not have a parent view.
|
||||
@SuppressLint("InflateParams")
|
||||
private fun askForSize(write: (size: Int) -> Unit) {
|
||||
val view = layoutInflater.inflate(R.layout.dialog_size, null)
|
||||
val sizeView = view.findViewById<TextView>(R.id.size_display)
|
||||
val sizeBarView = view.findViewById<SeekBar>(R.id.size_bar)
|
||||
sizeBarView.initSizeBar(sizeView)
|
||||
AlertDialog.Builder(this)
|
||||
.setView(view)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
write(getSize(sizeBarView.progress))
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||
}
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun SeekBar.initSizeBar(sizeView: TextView) {
|
||||
sizeView.updateSize(progress)
|
||||
setOnSeekBarChangeListener(
|
||||
object : SeekBar.OnSeekBarChangeListener {
|
||||
override fun onProgressChanged(
|
||||
seekBar: SeekBar,
|
||||
progressValue: Int,
|
||||
fromUser: Boolean
|
||||
) {
|
||||
sizeView.updateSize(progressValue)
|
||||
}
|
||||
|
||||
override fun onStartTrackingTouch(seekBar: SeekBar) {}
|
||||
|
||||
override fun onStopTrackingTouch(seekBar: SeekBar) {}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun TextView.updateSize(power: Int) {
|
||||
val size = getSize(power)
|
||||
text = if (size > 0) {
|
||||
getString(R.string.size_width_by_height, size, size)
|
||||
} else {
|
||||
getString(R.string.size_no_magnification)
|
||||
}
|
||||
}
|
||||
|
||||
private fun saveAs(
|
||||
fileName: String,
|
||||
mimeType: String,
|
||||
write: (outputStream: OutputStream) -> Unit
|
||||
) {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
val message = writeExternalFile(
|
||||
fileName, mimeType, write
|
||||
).toSaveResult()
|
||||
withContext(Dispatchers.Main) {
|
||||
toast(message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun Context.shareAs(fileType: FileType) {
|
||||
when (fileType) {
|
||||
FileType.PNG -> askForSize {
|
||||
share(barcode.bitmap(it), MIME_PNG, "png")
|
||||
}
|
||||
|
||||
FileType.JPG -> askForSize {
|
||||
share(barcode.bitmap(it), MIME_JPG, "jpg")
|
||||
}
|
||||
|
||||
FileType.SVG -> shareText(barcode.svg(), MIME_SVG)
|
||||
FileType.TXT -> shareText(barcode.text())
|
||||
}
|
||||
}
|
||||
|
||||
private fun Context.share(bitmap: Bitmap, mimeType: String, ext: String) {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
val file = File(
|
||||
externalCacheDir,
|
||||
"shared_barcode.$ext"
|
||||
)
|
||||
val success = try {
|
||||
FileOutputStream(file).use {
|
||||
when (mimeType) {
|
||||
MIME_PNG -> bitmap.saveAsPng(it)
|
||||
MIME_JPG -> bitmap.saveAsJpg(it)
|
||||
else -> throw IllegalArgumentException(
|
||||
"Invalid mime type"
|
||||
)
|
||||
}
|
||||
}
|
||||
true
|
||||
} catch (_: IOException) {
|
||||
false
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
if (success) {
|
||||
shareFile(file, mimeType)
|
||||
} else {
|
||||
toast(R.string.error_saving_file)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun brightenScreen() {
|
||||
if (isFinishing || isDestroyed) {
|
||||
return
|
||||
}
|
||||
currentBrightness = getScreenBrightness()
|
||||
setScreenBrightness(1f)
|
||||
brightenScreenItem?.isChecked = true
|
||||
}
|
||||
|
||||
private fun restoreScreenBrightness() {
|
||||
if (currentBrightness > -1f) {
|
||||
setScreenBrightness(currentBrightness)
|
||||
currentBrightness = -1f
|
||||
brightenScreenItem?.isChecked = false
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val BIT_MATRIX_WIDTH = "bit_matrix_width"
|
||||
private const val BIT_MATRIX_HEIGHT = "bit_matrix_height"
|
||||
private const val BIT_MATRIX_DATA = "bit_matrix_data"
|
||||
private const val CONTENT_TEXT = "content_text"
|
||||
private const val CONTENT_RAW = "content_raw"
|
||||
private const val FORMAT = "format"
|
||||
private const val EC_LEVEL = "ec_level"
|
||||
private const val ADD_QUIET_ZONE = "add_quiet_zone"
|
||||
private const val COLORS = "colors"
|
||||
private const val MIME_PNG = "image/png"
|
||||
private const val MIME_JPG = "image/jpeg"
|
||||
private const val MIME_SVG = "image/svg+xmg"
|
||||
private const val MIME_TXT = "text/plain"
|
||||
|
||||
fun newIntent(
|
||||
context: Context,
|
||||
barcode: Barcode<*>,
|
||||
colors: Int = 0
|
||||
): Intent {
|
||||
val args = Bundle()
|
||||
when (barcode) {
|
||||
is ContentBarcode -> args.putEcLevelQuietZone(
|
||||
barcode.ecLevel,
|
||||
barcode.addQuietZone
|
||||
)
|
||||
|
||||
is BitMatrixBarcode -> {
|
||||
args.putInt(BIT_MATRIX_WIDTH, barcode.bitMatrix.width)
|
||||
args.putInt(BIT_MATRIX_HEIGHT, barcode.bitMatrix.height)
|
||||
args.putByteArray(BIT_MATRIX_DATA, barcode.bitMatrix.data)
|
||||
}
|
||||
}
|
||||
args.putContentFormatColors(
|
||||
barcode.content,
|
||||
barcode.format,
|
||||
colors
|
||||
)
|
||||
return Intent(context, BarcodeActivity::class.java).apply {
|
||||
putExtras(args)
|
||||
}
|
||||
}
|
||||
|
||||
fun <T> newIntent(
|
||||
context: Context,
|
||||
content: T,
|
||||
format: BarcodeFormat,
|
||||
ecLevel: Int = -1,
|
||||
addQuietZone: Boolean = false,
|
||||
colors: Int = 0
|
||||
): Intent {
|
||||
val args = Bundle()
|
||||
args.putContentFormatColors(content, format, colors)
|
||||
args.putEcLevelQuietZone(ecLevel, addQuietZone)
|
||||
return Intent(context, BarcodeActivity::class.java).apply {
|
||||
putExtras(args)
|
||||
}
|
||||
}
|
||||
|
||||
private fun <T> Bundle.putContentFormatColors(
|
||||
content: T,
|
||||
format: BarcodeFormat,
|
||||
colors: Int
|
||||
) {
|
||||
when (content) {
|
||||
is String -> {
|
||||
putString(CONTENT_TEXT, content)
|
||||
}
|
||||
|
||||
is ByteArray -> {
|
||||
putByteArray(CONTENT_RAW, content)
|
||||
}
|
||||
|
||||
else -> {
|
||||
throw IllegalArgumentException(
|
||||
"content must be a String of a ByteArray"
|
||||
)
|
||||
}
|
||||
}
|
||||
putString(FORMAT, format.name)
|
||||
putInt(COLORS, colors)
|
||||
}
|
||||
|
||||
private fun Bundle.putEcLevelQuietZone(
|
||||
ecLevel: Int,
|
||||
addQuietZone: Boolean
|
||||
) {
|
||||
putInt(EC_LEVEL, ecLevel)
|
||||
putBoolean(ADD_QUIET_ZONE, addQuietZone)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun ImageView.setScale(scale: Float, x: Float, y: Float) {
|
||||
imageMatrix = Matrix().apply {
|
||||
setTranslate(-x, -y)
|
||||
postScale(scale, scale)
|
||||
postTranslate(x, y)
|
||||
}
|
||||
}
|
||||
|
||||
private fun ImageView.getScale(): Float {
|
||||
val values = FloatArray(9)
|
||||
imageMatrix.getValues(values)
|
||||
return values[Matrix.MSCALE_X]
|
||||
}
|
||||
|
||||
private fun Bitmap.saveAsPng(outputStream: OutputStream, quality: Int = 90) =
|
||||
compress(Bitmap.CompressFormat.PNG, quality, outputStream)
|
||||
|
||||
private fun Bitmap.saveAsJpg(outputStream: OutputStream, quality: Int = 90) =
|
||||
compress(Bitmap.CompressFormat.JPEG, quality, outputStream)
|
||||
|
||||
private val fileNameCharacters = "[^A-Za-z0-9]".toRegex()
|
||||
private fun encodeFileName(name: String): String = fileNameCharacters
|
||||
.replace(name, "_")
|
||||
.take(16)
|
||||
.trim('_')
|
||||
.lowercase(Locale.getDefault())
|
||||
|
||||
private fun getSize(step: Int) = when (step) {
|
||||
0 -> 0
|
||||
else -> 128 shl (step - 1)
|
||||
}
|
||||
|
||||
private fun readAndAddToHistory(
|
||||
bitmap: Bitmap,
|
||||
format: BarcodeFormat
|
||||
) {
|
||||
ZxingCpp.readBitmap(
|
||||
bitmap,
|
||||
0, 0,
|
||||
bitmap.width, bitmap.height,
|
||||
0,
|
||||
ZxingCpp.ReaderOptions(
|
||||
tryHarder = true,
|
||||
tryRotate = true,
|
||||
tryInvert = true,
|
||||
tryDownscale = true,
|
||||
maxNumberOfSymbols = 1,
|
||||
formats = setOf(format)
|
||||
)
|
||||
)?.first {
|
||||
val scan = it.toScan()
|
||||
if (!prefs.shouldIgnoreHistoryContent(scan.text, scan.format.name)) {
|
||||
db.insertScan(scan)
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+207
-214
@@ -1,66 +1,74 @@
|
||||
package de.markusfisch.android.binaryeye.fragment
|
||||
package de.markusfisch.android.binaryeye.activity
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.support.v4.app.Fragment
|
||||
import android.view.LayoutInflater
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.AdapterView
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.CheckBox
|
||||
import android.widget.EditText
|
||||
import android.widget.SeekBar
|
||||
import android.widget.Spinner
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.widget.SwitchCompat
|
||||
import androidx.core.net.toUri
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.adapter.prettifyFormatName
|
||||
import de.markusfisch.android.binaryeye.app.addFragment
|
||||
import de.markusfisch.android.binaryeye.adapter.toFormatDescriptionResId
|
||||
import de.markusfisch.android.binaryeye.app.prefs
|
||||
import de.markusfisch.android.binaryeye.net.isEncodeDeeplink
|
||||
import de.markusfisch.android.binaryeye.text.unescape
|
||||
import de.markusfisch.android.binaryeye.view.hideSoftKeyboard
|
||||
import de.markusfisch.android.binaryeye.view.setPaddingFromWindowInsets
|
||||
import de.markusfisch.android.binaryeye.widget.toast
|
||||
import de.markusfisch.android.zxingcpp.ZxingCpp.BarcodeFormat
|
||||
import java.io.InputStream
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
|
||||
class EncodeFragment : Fragment() {
|
||||
class EncodeActivity : AbstractBaseActivity() {
|
||||
private lateinit var formatView: Spinner
|
||||
private lateinit var formatDescriptionView: TextView
|
||||
private lateinit var ecLabel: TextView
|
||||
private lateinit var ecSpinner: Spinner
|
||||
private lateinit var colorsLabel: TextView
|
||||
private lateinit var colorsSpinner: Spinner
|
||||
private lateinit var sizeView: TextView
|
||||
private lateinit var sizeBarView: SeekBar
|
||||
private lateinit var marginLayout: View
|
||||
private lateinit var marginView: TextView
|
||||
private lateinit var marginBarView: SeekBar
|
||||
private lateinit var addQuietZoneSwitch: SwitchCompat
|
||||
private lateinit var contentView: EditText
|
||||
private lateinit var unescapeCheckBox: CheckBox
|
||||
|
||||
private val formats = arrayListOf(
|
||||
BarcodeFormat.AZTEC,
|
||||
BarcodeFormat.CODABAR,
|
||||
BarcodeFormat.CODE_39,
|
||||
BarcodeFormat.CODE_128,
|
||||
BarcodeFormat.DATA_MATRIX,
|
||||
BarcodeFormat.EAN_8,
|
||||
BarcodeFormat.EAN_13,
|
||||
private val writeableFormats = arrayListOf(
|
||||
BarcodeFormat.Aztec,
|
||||
BarcodeFormat.Codabar,
|
||||
BarcodeFormat.Code39Std,
|
||||
BarcodeFormat.Code39Ext,
|
||||
BarcodeFormat.Code32,
|
||||
BarcodeFormat.PZN,
|
||||
BarcodeFormat.Code93,
|
||||
BarcodeFormat.Code128,
|
||||
BarcodeFormat.DataBar,
|
||||
BarcodeFormat.DataBarOmni,
|
||||
BarcodeFormat.DataBarStk,
|
||||
BarcodeFormat.DataBarLtd,
|
||||
BarcodeFormat.DataBarExp,
|
||||
BarcodeFormat.DataBarExpStk,
|
||||
BarcodeFormat.DataMatrix,
|
||||
BarcodeFormat.DXFilmEdge,
|
||||
BarcodeFormat.EAN8,
|
||||
BarcodeFormat.EAN13,
|
||||
BarcodeFormat.ITF,
|
||||
BarcodeFormat.PDF_417,
|
||||
BarcodeFormat.QR_CODE,
|
||||
BarcodeFormat.UPC_A
|
||||
BarcodeFormat.MaxiCode,
|
||||
BarcodeFormat.PDF417,
|
||||
BarcodeFormat.QRCode,
|
||||
BarcodeFormat.MicroQRCode,
|
||||
BarcodeFormat.RMQRCode,
|
||||
BarcodeFormat.UPCA,
|
||||
BarcodeFormat.UPCE,
|
||||
)
|
||||
|
||||
private var minMargin = 0
|
||||
private var bytes: ByteArray? = null
|
||||
|
||||
override fun onActivityResult(
|
||||
@@ -68,16 +76,16 @@ class EncodeFragment : Fragment() {
|
||||
resultCode: Int,
|
||||
resultData: Intent?
|
||||
) {
|
||||
super.onActivityResult(requestCode, resultCode, resultData)
|
||||
when (requestCode) {
|
||||
PICK_FILE_RESULT_CODE -> {
|
||||
if (resultCode == Activity.RESULT_OK &&
|
||||
resultData != null &&
|
||||
resultData.data != null
|
||||
) {
|
||||
val ac = activity ?: return
|
||||
ac.hideSoftKeyboard(contentView)
|
||||
hideSoftKeyboard(contentView)
|
||||
val uri = resultData.data ?: return
|
||||
bytes = ac.contentResolver?.openInputStream(uri)?.use {
|
||||
bytes = contentResolver?.openInputStream(uri)?.use {
|
||||
it.readBytesMax(4096)
|
||||
} ?: return
|
||||
setEncodeByteArray()
|
||||
@@ -88,28 +96,15 @@ class EncodeFragment : Fragment() {
|
||||
|
||||
override fun onCreate(state: Bundle?) {
|
||||
super.onCreate(state)
|
||||
setHasOptionsMenu(true)
|
||||
}
|
||||
setScreenContentView(R.layout.activity_encode)
|
||||
setTitle(R.string.compose_barcode)
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
state: Bundle?
|
||||
): View? {
|
||||
val ac = activity ?: return null
|
||||
ac.setTitle(R.string.compose_barcode)
|
||||
|
||||
val view = inflater.inflate(
|
||||
R.layout.fragment_encode,
|
||||
container,
|
||||
false
|
||||
)
|
||||
|
||||
formatView = view.findViewById(R.id.format)
|
||||
formatView = findViewById(R.id.format)
|
||||
formatDescriptionView = findViewById(R.id.format_description)
|
||||
val formatAdapter = ArrayAdapter(
|
||||
ac,
|
||||
this,
|
||||
android.R.layout.simple_spinner_item,
|
||||
formats.map { prettifyFormatName(it.name) }
|
||||
writeableFormats.map { it.name.prettifyFormatName() }
|
||||
)
|
||||
formatAdapter.setDropDownViewResource(
|
||||
android.R.layout.simple_spinner_dropdown_item
|
||||
@@ -122,11 +117,20 @@ class EncodeFragment : Fragment() {
|
||||
position: Int,
|
||||
id: Long
|
||||
) {
|
||||
val format = formats[position]
|
||||
val format = writeableFormats[position]
|
||||
val descResId = format.name.toFormatDescriptionResId()
|
||||
if (descResId != 0) {
|
||||
formatDescriptionView.setText(descResId)
|
||||
formatDescriptionView.visibility = View.VISIBLE
|
||||
} else {
|
||||
formatDescriptionView.visibility = View.GONE
|
||||
}
|
||||
val arrayId = when (format) {
|
||||
BarcodeFormat.AZTEC -> R.array.aztec_error_correction_levels
|
||||
BarcodeFormat.QR_CODE -> R.array.qr_error_correction_levels
|
||||
BarcodeFormat.PDF_417 -> R.array.pdf417_error_correction_levels
|
||||
BarcodeFormat.Aztec -> R.array.aztec_error_correction_levels
|
||||
BarcodeFormat.QRCode -> R.array.qr_error_correction_levels
|
||||
BarcodeFormat.MicroQRCode -> R.array.mqr_error_correction_levels
|
||||
BarcodeFormat.RMQRCode -> R.array.rmqr_error_correction_levels
|
||||
BarcodeFormat.PDF417 -> R.array.pdf417_error_correction_levels
|
||||
else -> 0
|
||||
}
|
||||
if (arrayId > 0) {
|
||||
@@ -144,20 +148,13 @@ class EncodeFragment : Fragment() {
|
||||
format.canBeInverted().setVisibilityFor(
|
||||
colorsLabel, colorsSpinner
|
||||
)
|
||||
when (format) {
|
||||
BarcodeFormat.AZTEC -> setMarginBar(4, 4)
|
||||
BarcodeFormat.DATA_MATRIX -> setMarginBar(1, 1)
|
||||
BarcodeFormat.QR_CODE -> setMarginBar(4, 4)
|
||||
BarcodeFormat.PDF_417 -> setMarginBar(0, 30)
|
||||
else -> false
|
||||
}.setVisibilityFor(marginLayout)
|
||||
}
|
||||
|
||||
override fun onNothingSelected(parentView: AdapterView<*>?) {}
|
||||
}
|
||||
|
||||
ecLabel = view.findViewById(R.id.error_correction_label)
|
||||
ecSpinner = view.findViewById(R.id.error_correction_level)
|
||||
ecLabel = findViewById(R.id.error_correction_label)
|
||||
ecSpinner = findViewById(R.id.error_correction_level)
|
||||
ecSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
|
||||
override fun onItemSelected(
|
||||
parentView: AdapterView<*>?,
|
||||
@@ -165,7 +162,7 @@ class EncodeFragment : Fragment() {
|
||||
position: Int,
|
||||
id: Long
|
||||
) {
|
||||
val format = formats[formatView.selectedItemPosition]
|
||||
val format = writeableFormats[formatView.selectedItemPosition]
|
||||
prefs.indexOfLastSelectedEcLevel = format.packEcLevel(
|
||||
prefs.indexOfLastSelectedEcLevel,
|
||||
position
|
||||
@@ -175,50 +172,29 @@ class EncodeFragment : Fragment() {
|
||||
override fun onNothingSelected(parentView: AdapterView<*>?) {}
|
||||
}
|
||||
|
||||
colorsLabel = view.findViewById(R.id.colors_label)
|
||||
colorsSpinner = view.findViewById(R.id.colors)
|
||||
colorsLabel = findViewById(R.id.colors_label)
|
||||
colorsSpinner = findViewById(R.id.colors)
|
||||
addQuietZoneSwitch = findViewById(R.id.add_quiet_zone)
|
||||
addQuietZoneSwitch.isChecked = prefs.addQuietZone
|
||||
|
||||
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()
|
||||
|
||||
contentView = view.findViewById(R.id.content)
|
||||
unescapeCheckBox = view.findViewById(R.id.unescape)
|
||||
contentView = findViewById(R.id.content)
|
||||
unescapeCheckBox = findViewById(R.id.unescape)
|
||||
unescapeCheckBox.isChecked = prefs.expandEscapeSequences
|
||||
|
||||
val args = arguments
|
||||
args?.getString(CONTENT_TEXT)?.let {
|
||||
contentView.setText(it)
|
||||
}
|
||||
args?.getByteArray(CONTENT_RAW)?.let {
|
||||
bytes = it
|
||||
setEncodeByteArray()
|
||||
}
|
||||
applyIntent(state == null)
|
||||
|
||||
val barcodeFormat = args?.getString(FORMAT)
|
||||
if (barcodeFormat != null) {
|
||||
formatView.setSelection(
|
||||
formats.indexOf(barcodeFormat.toFormat())
|
||||
)
|
||||
} else if (state == null) {
|
||||
formatView.post {
|
||||
formatView.setSelection(prefs.indexOfLastSelectedFormat)
|
||||
}
|
||||
}
|
||||
|
||||
view.findViewById<View>(R.id.encode).setOnClickListener {
|
||||
findViewById<View>(R.id.encode).setOnClickListener {
|
||||
it.context.encode()
|
||||
}
|
||||
|
||||
(view.findViewById(R.id.inset_layout) as View).setPaddingFromWindowInsets()
|
||||
(view.findViewById(R.id.scroll_view) as View).setPaddingFromWindowInsets()
|
||||
findViewById<View>(R.id.inset_layout).setPaddingFromWindowInsets()
|
||||
findViewById<View>(R.id.scroll_view).setPaddingFromWindowInsets()
|
||||
}
|
||||
|
||||
return view
|
||||
override fun onNewIntent(intent: Intent) {
|
||||
super.onNewIntent(intent)
|
||||
setIntent(intent)
|
||||
applyIntent(true)
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
@@ -248,31 +224,78 @@ class EncodeFragment : Fragment() {
|
||||
super.onPause()
|
||||
prefs.indexOfLastSelectedFormat = formatView.selectedItemPosition
|
||||
prefs.expandEscapeSequences = unescapeCheckBox.isChecked
|
||||
prefs.lastMargin = marginBarView.progress
|
||||
prefs.addQuietZone = addQuietZoneSwitch.isChecked
|
||||
}
|
||||
|
||||
private fun applyIntent(applyDefaultFormat: Boolean) {
|
||||
val args = intent?.extras ?: getExtrasFromDeeplink()
|
||||
val hasTextContent = args?.containsKey(CONTENT_TEXT) == true
|
||||
val hasRawContent = args?.containsKey(CONTENT_RAW) == true
|
||||
var complete = bytes?.isNotEmpty() == true ||
|
||||
contentView.text?.isNotEmpty() == true
|
||||
|
||||
if (hasTextContent || hasRawContent) {
|
||||
bytes = null
|
||||
contentView.isEnabled = true
|
||||
contentView.hint = null
|
||||
unescapeCheckBox.isEnabled = true
|
||||
}
|
||||
|
||||
if (hasTextContent) {
|
||||
args.getString(CONTENT_TEXT)?.let {
|
||||
contentView.setText(it)
|
||||
complete = it.isNotEmpty()
|
||||
}
|
||||
}
|
||||
if (hasRawContent) {
|
||||
args.getByteArray(CONTENT_RAW)?.let {
|
||||
bytes = it
|
||||
complete = it.isNotEmpty()
|
||||
setEncodeByteArray()
|
||||
}
|
||||
}
|
||||
|
||||
val barcodeFormat = args?.getString(FORMAT)
|
||||
if (barcodeFormat != null) {
|
||||
formatView.setSelection(
|
||||
writeableFormats.indexOf(barcodeFormat.toFormat())
|
||||
)
|
||||
} else if (applyDefaultFormat) {
|
||||
formatView.post {
|
||||
formatView.setSelection(prefs.indexOfLastSelectedFormat)
|
||||
}
|
||||
}
|
||||
|
||||
if (complete &&
|
||||
args?.getBoolean(EXECUTE, false) == true
|
||||
) {
|
||||
contentView.post {
|
||||
val content = contentView.context.getContent() ?: return@post
|
||||
startActivity(newBarcodeIntent(content))
|
||||
finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun Context.encode() {
|
||||
hideSoftKeyboard(contentView)
|
||||
val content = getContent() ?: return
|
||||
val format = formats[formatView.selectedItemPosition]
|
||||
fragmentManager?.addFragment(
|
||||
BarcodeFragment.newInstance(
|
||||
content,
|
||||
format,
|
||||
getSize(sizeBarView.progress),
|
||||
when (format) {
|
||||
BarcodeFormat.AZTEC,
|
||||
BarcodeFormat.DATA_MATRIX,
|
||||
BarcodeFormat.QR_CODE,
|
||||
BarcodeFormat.PDF_417 -> marginBarView.progress
|
||||
contentView.post {
|
||||
startActivity(newBarcodeIntent(content))
|
||||
}
|
||||
}
|
||||
|
||||
else -> -1
|
||||
},
|
||||
format.getErrorCorrectionLevel(ecSpinner.selectedItemPosition),
|
||||
if (format.canBeInverted()) {
|
||||
colorsSpinner.selectedItemPosition
|
||||
} else 0
|
||||
)
|
||||
private fun newBarcodeIntent(content: Any): Intent {
|
||||
val format = writeableFormats[formatView.selectedItemPosition]
|
||||
return BarcodeActivity.newIntent(
|
||||
this,
|
||||
content,
|
||||
format,
|
||||
format.getErrorCorrectionLevel(ecSpinner.selectedItemPosition),
|
||||
addQuietZoneSwitch.isChecked,
|
||||
if (format.canBeInverted()) {
|
||||
colorsSpinner.selectedItemPosition
|
||||
} else 0
|
||||
)
|
||||
}
|
||||
|
||||
@@ -281,90 +304,27 @@ class EncodeFragment : Fragment() {
|
||||
return bytes
|
||||
}
|
||||
var text = contentView.text.toString()
|
||||
if (unescapeCheckBox.isChecked) {
|
||||
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 (like `\0`). This means the content
|
||||
// will be encoded in binary mode, what would be wrong for
|
||||
// "something\nlike\tthüs".
|
||||
if (text.any { it.code < 32 && it.code !in setOf(9, 10, 13) }) {
|
||||
return text.toByteArray()
|
||||
}
|
||||
}
|
||||
if (text.isEmpty()) {
|
||||
toast(R.string.error_no_content)
|
||||
return null
|
||||
}
|
||||
return text
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
)
|
||||
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() {
|
||||
@@ -374,16 +334,41 @@ class EncodeFragment : Fragment() {
|
||||
unescapeCheckBox.isEnabled = false
|
||||
}
|
||||
|
||||
private fun getExtrasFromDeeplink(): Bundle? {
|
||||
val data = intent?.dataString ?: return null
|
||||
if (!isEncodeDeeplink(data)) {
|
||||
return null
|
||||
}
|
||||
val uri = data.toUri()
|
||||
return Bundle().apply {
|
||||
uri.getQueryParameter("content")?.let {
|
||||
putString(CONTENT_TEXT, it)
|
||||
}
|
||||
uri.getQueryParameter("format")?.let {
|
||||
putString(FORMAT, it)
|
||||
}
|
||||
putBoolean(
|
||||
EXECUTE,
|
||||
uri.getQueryParameter("execute").let {
|
||||
it == "" || it?.toBoolean() == true
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val CONTENT_TEXT = "content_text"
|
||||
private const val CONTENT_RAW = "content_raw"
|
||||
private const val FORMAT = "format"
|
||||
private const val EXECUTE = "execute"
|
||||
private const val PICK_FILE_RESULT_CODE = 1
|
||||
|
||||
fun <T> newInstance(
|
||||
fun <T> newIntent(
|
||||
context: Context,
|
||||
content: T? = null,
|
||||
format: String? = null
|
||||
): Fragment {
|
||||
format: String? = null,
|
||||
execute: Boolean = false
|
||||
): Intent {
|
||||
val args = Bundle()
|
||||
content?.let {
|
||||
when (content) {
|
||||
@@ -393,11 +378,12 @@ class EncodeFragment : Fragment() {
|
||||
"content must be a String or a ByteArray"
|
||||
)
|
||||
}
|
||||
args.putBoolean(EXECUTE, execute)
|
||||
}
|
||||
format?.let { args.putString(FORMAT, it) }
|
||||
val fragment = EncodeFragment()
|
||||
fragment.arguments = args
|
||||
return fragment
|
||||
return Intent(context, EncodeActivity::class.java).apply {
|
||||
putExtras(args)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -418,30 +404,39 @@ private fun BarcodeFormat.unpackEcLevel(packed: Int) =
|
||||
(packed shr ecLevelShift()) and 15
|
||||
|
||||
private fun BarcodeFormat.ecLevelShift() = when (this) {
|
||||
BarcodeFormat.AZTEC -> 0
|
||||
BarcodeFormat.QR_CODE -> 4
|
||||
BarcodeFormat.PDF_417 -> 8
|
||||
BarcodeFormat.Aztec -> 0
|
||||
BarcodeFormat.QRCode -> 4
|
||||
BarcodeFormat.MicroQRCode -> 3
|
||||
BarcodeFormat.RMQRCode -> 2
|
||||
BarcodeFormat.PDF417 -> 8
|
||||
else -> throw IllegalArgumentException("$this does not have error levels")
|
||||
}
|
||||
|
||||
private fun BarcodeFormat.canBeInverted() = when (this) {
|
||||
BarcodeFormat.AZTEC,
|
||||
BarcodeFormat.DATA_MATRIX,
|
||||
BarcodeFormat.QR_CODE -> true
|
||||
BarcodeFormat.Aztec,
|
||||
BarcodeFormat.DataMatrix,
|
||||
BarcodeFormat.QRCode,
|
||||
BarcodeFormat.MicroQRCode,
|
||||
BarcodeFormat.RMQRCode -> true
|
||||
|
||||
else -> false
|
||||
}
|
||||
|
||||
private fun String.toFormat(default: BarcodeFormat = BarcodeFormat.QR_CODE): BarcodeFormat = try {
|
||||
private fun String.toFormat(
|
||||
default: BarcodeFormat = BarcodeFormat.QRCode
|
||||
): BarcodeFormat = try {
|
||||
BarcodeFormat.valueOf(this)
|
||||
} catch (_: IllegalArgumentException) {
|
||||
default
|
||||
}
|
||||
|
||||
private fun BarcodeFormat.getErrorCorrectionLevel(position: Int) = when (this) {
|
||||
BarcodeFormat.AZTEC -> position
|
||||
BarcodeFormat.QR_CODE -> (position + 1) * 2
|
||||
BarcodeFormat.PDF_417 -> position
|
||||
BarcodeFormat.Aztec -> position
|
||||
BarcodeFormat.QRCode,
|
||||
BarcodeFormat.MicroQRCode -> (position + 1) * 2
|
||||
|
||||
BarcodeFormat.RMQRCode -> (position + 1) * 4
|
||||
BarcodeFormat.PDF417 -> position
|
||||
else -> 0
|
||||
}.coerceIn(0, 8)
|
||||
|
||||
@@ -456,8 +451,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)
|
||||
+70
-82
@@ -1,4 +1,4 @@
|
||||
package de.markusfisch.android.binaryeye.fragment
|
||||
package de.markusfisch.android.binaryeye.activity
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.AlertDialog
|
||||
@@ -8,24 +8,19 @@ import android.database.Cursor
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Parcelable
|
||||
import android.support.v4.app.ActivityCompat
|
||||
import android.support.v4.app.Fragment
|
||||
import android.support.v4.content.ContextCompat
|
||||
import android.support.v7.app.AppCompatActivity
|
||||
import android.support.v7.view.ActionMode
|
||||
import android.support.v7.widget.SearchView
|
||||
import android.support.v7.widget.SwitchCompat
|
||||
import android.view.LayoutInflater
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.EditText
|
||||
import android.widget.ListView
|
||||
import androidx.appcompat.view.ActionMode
|
||||
import androidx.appcompat.widget.SearchView
|
||||
import androidx.appcompat.widget.SwitchCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.adapter.ScansAdapter
|
||||
import de.markusfisch.android.binaryeye.app.addFragment
|
||||
import de.markusfisch.android.binaryeye.app.alertDialog
|
||||
import de.markusfisch.android.binaryeye.app.db
|
||||
import de.markusfisch.android.binaryeye.app.hasWritePermission
|
||||
@@ -38,7 +33,6 @@ 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.lockStatusBarColor
|
||||
@@ -53,7 +47,7 @@ import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class HistoryFragment : Fragment() {
|
||||
class HistoryActivity : AbstractBaseActivity() {
|
||||
private lateinit var useHistorySwitch: SwitchCompat
|
||||
private lateinit var listView: ListView
|
||||
private lateinit var fab: View
|
||||
@@ -70,9 +64,11 @@ class HistoryFragment : Fragment() {
|
||||
R.menu.fragment_history_edit,
|
||||
menu
|
||||
)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
// SDK35+ doesn't draw a status bar background anymore.
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.VANILLA_ICE_CREAM) {
|
||||
lockStatusBarColor()
|
||||
val ac = activity ?: return false
|
||||
val ac = this@HistoryActivity
|
||||
@Suppress("DEPRECATION")
|
||||
ac.window.statusBarColor = ContextCompat.getColor(
|
||||
ac,
|
||||
R.color.accent_dark
|
||||
@@ -92,7 +88,7 @@ class HistoryFragment : Fragment() {
|
||||
mode: ActionMode,
|
||||
item: MenuItem
|
||||
): Boolean {
|
||||
val ac = activity ?: return false
|
||||
val ac = this@HistoryActivity
|
||||
return when (item.itemId) {
|
||||
R.id.copy_scan -> {
|
||||
scansAdapter?.getSelectedContent("\n")?.let {
|
||||
@@ -144,29 +140,15 @@ class HistoryFragment : Fragment() {
|
||||
|
||||
override fun onCreate(state: Bundle?) {
|
||||
super.onCreate(state)
|
||||
setHasOptionsMenu(true)
|
||||
}
|
||||
setScreenContentView(R.layout.activity_history)
|
||||
setTitle(R.string.history)
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
state: Bundle?
|
||||
): View? {
|
||||
val ac = activity ?: return null
|
||||
ac.setTitle(R.string.history)
|
||||
|
||||
val view = inflater.inflate(
|
||||
R.layout.fragment_history,
|
||||
container,
|
||||
false
|
||||
)
|
||||
|
||||
useHistorySwitch = view.findViewById(
|
||||
useHistorySwitch = findViewById(
|
||||
R.id.use_history
|
||||
) as SwitchCompat
|
||||
)
|
||||
initHistorySwitch(useHistorySwitch)
|
||||
|
||||
listView = view.findViewById(R.id.scans)
|
||||
listView = findViewById(R.id.scans)
|
||||
listView.setOnItemClickListener { _, v, position, id ->
|
||||
if (actionMode != null) {
|
||||
scansAdapter?.select(v, id, position)
|
||||
@@ -176,8 +158,8 @@ class HistoryFragment : Fragment() {
|
||||
}
|
||||
listView.setOnItemLongClickListener { _, v, position, id ->
|
||||
scansAdapter?.select(v, id, position)
|
||||
if (actionMode == null && ac is AppCompatActivity) {
|
||||
actionMode = ac.delegate.startSupportActionMode(
|
||||
if (actionMode == null) {
|
||||
actionMode = delegate.startSupportActionMode(
|
||||
actionModeCallback
|
||||
)
|
||||
}
|
||||
@@ -185,19 +167,15 @@ class HistoryFragment : Fragment() {
|
||||
}
|
||||
listView.setOnScrollListener(systemBarListViewScrollListener)
|
||||
|
||||
fab = view.findViewById(R.id.share)
|
||||
fab = findViewById(R.id.share)
|
||||
fab.setOnClickListener { v ->
|
||||
v.context.pickListSeparatorAndShare(false)
|
||||
}
|
||||
|
||||
progressView = view.findViewById(R.id.progress_view)
|
||||
progressView = findViewById(R.id.progress_view)
|
||||
|
||||
(view.findViewById(R.id.inset_layout) as View).setPaddingFromWindowInsets()
|
||||
findViewById<View>(R.id.inset_layout).setPaddingFromWindowInsets()
|
||||
listView.setPaddingFromWindowInsets()
|
||||
|
||||
update()
|
||||
|
||||
return view
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
@@ -207,6 +185,11 @@ class HistoryFragment : Fragment() {
|
||||
wipeLastShareFile()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
update()
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
listViewState = listView.onSaveInstanceState()
|
||||
@@ -231,35 +214,31 @@ class HistoryFragment : Fragment() {
|
||||
|
||||
private fun initSearchView(item: MenuItem?) {
|
||||
item ?: return
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) {
|
||||
item.isVisible = false
|
||||
} else {
|
||||
val ac = activity ?: return
|
||||
val searchView = item.actionView as SearchView
|
||||
val searchManager = ac.getSystemService(
|
||||
Context.SEARCH_SERVICE
|
||||
) as SearchManager
|
||||
searchView.setSearchableInfo(
|
||||
searchManager.getSearchableInfo(ac.componentName)
|
||||
)
|
||||
searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
|
||||
override fun onQueryTextSubmit(query: String): Boolean {
|
||||
update(query)
|
||||
return false
|
||||
}
|
||||
val ac = this
|
||||
val searchView = item.actionView as SearchView
|
||||
val searchManager = ac.getSystemService(
|
||||
Context.SEARCH_SERVICE
|
||||
) as SearchManager
|
||||
searchView.setSearchableInfo(
|
||||
searchManager.getSearchableInfo(ac.componentName)
|
||||
)
|
||||
searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
|
||||
override fun onQueryTextSubmit(query: String): Boolean {
|
||||
update(query)
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onQueryTextChange(query: String): Boolean {
|
||||
update(query)
|
||||
return false
|
||||
}
|
||||
})
|
||||
}
|
||||
override fun onQueryTextChange(query: String): Boolean {
|
||||
update(query)
|
||||
return false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
return when (item.itemId) {
|
||||
R.id.clear -> {
|
||||
context.askToRemoveScans()
|
||||
this.askToRemoveScans()
|
||||
true
|
||||
}
|
||||
|
||||
@@ -269,7 +248,7 @@ class HistoryFragment : Fragment() {
|
||||
}
|
||||
|
||||
R.id.share_as_file -> {
|
||||
context.pickListSeparatorAndShare(true)
|
||||
this.pickListSeparatorAndShare(true)
|
||||
true
|
||||
}
|
||||
|
||||
@@ -296,13 +275,13 @@ class HistoryFragment : Fragment() {
|
||||
scope.launch {
|
||||
val cursor = db.getScans(filter)
|
||||
withContext(Dispatchers.Main) {
|
||||
val ac = activity ?: return@withContext
|
||||
val hasScans = cursor != null && cursor.count > 0
|
||||
val ac = this@HistoryActivity
|
||||
val hasScans = cursor.count > 0
|
||||
if (filter == null) {
|
||||
if (!hasScans) {
|
||||
listView.emptyView = useHistorySwitch
|
||||
}
|
||||
ActivityCompat.invalidateOptionsMenu(ac)
|
||||
ac.invalidateOptionsMenu()
|
||||
}
|
||||
enableMenuItems(hasScans)
|
||||
fab.visibility = if (hasScans) {
|
||||
@@ -310,10 +289,19 @@ class HistoryFragment : Fragment() {
|
||||
} else {
|
||||
View.GONE
|
||||
}
|
||||
cursor?.let { cursor ->
|
||||
cursor.let { cursor ->
|
||||
// Close previous cursor.
|
||||
scansAdapter?.also { it.changeCursor(null) }
|
||||
scansAdapter = ScansAdapter(ac, cursor)
|
||||
scansAdapter = ScansAdapter(ac, cursor) { id, pinned ->
|
||||
if (actionMode == null) {
|
||||
scope.launch {
|
||||
db.setPinned(id, pinned)
|
||||
withContext(Dispatchers.Main) {
|
||||
update()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
listView.adapter = scansAdapter
|
||||
listViewState?.also {
|
||||
listView.onRestoreInstanceState(it)
|
||||
@@ -340,8 +328,8 @@ class HistoryFragment : Fragment() {
|
||||
private fun showScan(id: Long) = db.getScan(id)?.also { scan ->
|
||||
closeActionMode()
|
||||
try {
|
||||
fragmentManager?.addFragment(DecodeFragment.newInstance(scan))
|
||||
} catch (e: IllegalArgumentException) {
|
||||
startActivity(DecodeActivity.newIntent(this, scan))
|
||||
} catch (_: IllegalArgumentException) {
|
||||
// Ignore, can never happen.
|
||||
}
|
||||
}
|
||||
@@ -414,7 +402,7 @@ class HistoryFragment : Fragment() {
|
||||
|
||||
private fun askToExportToFile() {
|
||||
scope.launch {
|
||||
val ac = activity ?: return@launch
|
||||
val ac = this@HistoryActivity
|
||||
progressView.useVisibility {
|
||||
// Write permission is only required before Android Q.
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q &&
|
||||
@@ -442,12 +430,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)
|
||||
@@ -475,23 +463,23 @@ class HistoryFragment : Fragment() {
|
||||
db.getScansDetailed(selectedIds.toLongArray())
|
||||
} else {
|
||||
db.getScansDetailed(filter)
|
||||
}?.use { cursor ->
|
||||
}.use { cursor ->
|
||||
val details = format.split(":")
|
||||
when (details[0]) {
|
||||
"text" -> Pair(cursor.exportText(details[1]), "txt")
|
||||
"csv" -> Pair(cursor.exportCsv(details[1]), "csv")
|
||||
else -> Pair(cursor.exportJson(), "json")
|
||||
}
|
||||
}?.let { (text, ext) ->
|
||||
}.let { (text, ext) ->
|
||||
text?.let {
|
||||
withContext(Dispatchers.Main) {
|
||||
if (asFile) {
|
||||
context.shareAsFile(
|
||||
this@HistoryActivity.shareAsFile(
|
||||
it,
|
||||
String.format("scans.%s", ext)
|
||||
)
|
||||
} else {
|
||||
context.shareText(it)
|
||||
this@HistoryActivity.shareText(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -502,7 +490,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)
|
||||
@@ -0,0 +1,171 @@
|
||||
package de.markusfisch.android.binaryeye.activity
|
||||
|
||||
import android.app.AlertDialog
|
||||
import android.app.Dialog
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.EditText
|
||||
import android.widget.ListView
|
||||
import android.widget.Spinner
|
||||
import android.widget.TextView
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.adapter.prettifyFormatName
|
||||
import de.markusfisch.android.binaryeye.adapter.setupFormatSpinner
|
||||
import de.markusfisch.android.binaryeye.app.prefs
|
||||
import de.markusfisch.android.binaryeye.preference.IgnoreCode
|
||||
import de.markusfisch.android.binaryeye.view.setPaddingFromWindowInsets
|
||||
import de.markusfisch.android.binaryeye.view.systemBarListViewScrollListener
|
||||
import de.markusfisch.android.binaryeye.widget.toast
|
||||
|
||||
class IgnoreCodesActivity : AbstractBaseActivity() {
|
||||
private val patterns = ArrayList<IgnoreCode>()
|
||||
|
||||
private lateinit var adapter: PatternsAdapter
|
||||
|
||||
override fun onCreate(state: Bundle?) {
|
||||
super.onCreate(state)
|
||||
setScreenContentView(R.layout.activity_ignore_codes)
|
||||
setTitle(R.string.ignore_codes)
|
||||
|
||||
patterns.addAll(prefs.ignoreCodes)
|
||||
|
||||
val listView = findViewById<ListView>(R.id.actions)
|
||||
listView.emptyView = findViewById(R.id.no_actions)
|
||||
findViewById<TextView>(R.id.no_actions).setText(
|
||||
R.string.ignore_codes_empty
|
||||
)
|
||||
adapter = PatternsAdapter(patterns)
|
||||
listView.adapter = adapter
|
||||
listView.setOnScrollListener(systemBarListViewScrollListener)
|
||||
listView.setOnItemClickListener { _, _, position, _ ->
|
||||
editPattern(position)
|
||||
}
|
||||
listView.setOnItemLongClickListener { _, _, position, _ ->
|
||||
confirmRemovePattern(position)
|
||||
true
|
||||
}
|
||||
|
||||
findViewById<View>(R.id.add).apply {
|
||||
contentDescription = getString(R.string.ignore_code_add)
|
||||
setOnClickListener {
|
||||
editPattern(null)
|
||||
}
|
||||
}
|
||||
|
||||
findViewById<View>(R.id.inset_layout).setPaddingFromWindowInsets()
|
||||
listView.setPaddingFromWindowInsets()
|
||||
}
|
||||
|
||||
private fun editPattern(position: Int?) {
|
||||
val pattern = position?.let { patterns[it] }
|
||||
val view = layoutInflater.inflate(R.layout.dialog_ignore_code, null)
|
||||
val regexView = view.findViewById<EditText>(R.id.regex)
|
||||
val formatView = view.findViewById<Spinner>(R.id.format)
|
||||
val values = setupFormatSpinner(formatView)
|
||||
if (pattern != null) {
|
||||
regexView.setText(pattern.pattern)
|
||||
regexView.setSelection(pattern.pattern.length)
|
||||
formatView.setSelection(
|
||||
values.indexOf(pattern.format ?: "").coerceAtLeast(0)
|
||||
)
|
||||
} else {
|
||||
formatView.setSelection(0)
|
||||
}
|
||||
|
||||
val dialog = AlertDialog.Builder(this)
|
||||
.setTitle(
|
||||
if (pattern == null) {
|
||||
R.string.ignore_code_add
|
||||
} else {
|
||||
R.string.ignore_code_edit
|
||||
}
|
||||
)
|
||||
.setView(view)
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
||||
.create()
|
||||
|
||||
dialog.setOnShowListener {
|
||||
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener {
|
||||
addOrUpdatePattern(position, regexView, formatView, values, dialog)
|
||||
}
|
||||
}
|
||||
|
||||
dialog.show()
|
||||
}
|
||||
|
||||
private fun addOrUpdatePattern(
|
||||
position: Int?,
|
||||
regexView: EditText,
|
||||
formatView: Spinner,
|
||||
values: List<String>,
|
||||
dialog: Dialog,
|
||||
) {
|
||||
val pattern = regexView.text.toString().trim()
|
||||
if (pattern.isEmpty()) {
|
||||
regexView.error = getString(R.string.ignore_code_regex_required)
|
||||
return
|
||||
}
|
||||
try {
|
||||
Regex(pattern)
|
||||
} catch (e: Exception) {
|
||||
val message = e.message ?: getString(
|
||||
R.string.ignore_code_regex_invalid
|
||||
)
|
||||
regexView.error = message
|
||||
toast(message)
|
||||
return
|
||||
}
|
||||
val item = IgnoreCode(
|
||||
pattern = pattern,
|
||||
format = values[formatView.selectedItemPosition].ifEmpty { null }
|
||||
)
|
||||
if (position == null) {
|
||||
patterns.add(item)
|
||||
} else {
|
||||
patterns[position] = item
|
||||
}
|
||||
prefs.setIgnoreCodes(patterns)
|
||||
adapter.notifyDataSetChanged()
|
||||
dialog.dismiss()
|
||||
}
|
||||
|
||||
private fun confirmRemovePattern(position: Int) {
|
||||
AlertDialog.Builder(this)
|
||||
.setMessage(R.string.ignore_code_remove_confirm)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
patterns.removeAt(position)
|
||||
prefs.setIgnoreCodes(patterns)
|
||||
adapter.notifyDataSetChanged()
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
||||
.show()
|
||||
}
|
||||
|
||||
private inner class PatternsAdapter(
|
||||
items: List<IgnoreCode>
|
||||
) : ArrayAdapter<IgnoreCode>(
|
||||
this,
|
||||
android.R.layout.simple_list_item_2,
|
||||
android.R.id.text1,
|
||||
items
|
||||
) {
|
||||
override fun getView(
|
||||
position: Int,
|
||||
convertView: View?,
|
||||
parent: ViewGroup
|
||||
): View {
|
||||
val view = super.getView(position, convertView, parent)
|
||||
val item = patterns[position]
|
||||
val title = view.findViewById<TextView>(android.R.id.text1)
|
||||
val subtitle = view.findViewById<TextView>(android.R.id.text2)
|
||||
title.text = item.pattern
|
||||
subtitle.text = item.format?.prettifyFormatName() ?: getString(
|
||||
R.string.all_formats
|
||||
)
|
||||
return view
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,148 +0,0 @@
|
||||
package de.markusfisch.android.binaryeye.activity
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.support.v4.app.Fragment
|
||||
import android.support.v7.app.AppCompatActivity
|
||||
import android.support.v7.widget.Toolbar
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.app.PERMISSION_LOCATION
|
||||
import de.markusfisch.android.binaryeye.app.PERMISSION_WRITE
|
||||
import de.markusfisch.android.binaryeye.app.applyLocale
|
||||
import de.markusfisch.android.binaryeye.app.permissionGrantedCallback
|
||||
import de.markusfisch.android.binaryeye.app.prefs
|
||||
import de.markusfisch.android.binaryeye.app.setFragment
|
||||
import de.markusfisch.android.binaryeye.database.Scan
|
||||
import de.markusfisch.android.binaryeye.fragment.DecodeFragment
|
||||
import de.markusfisch.android.binaryeye.fragment.EncodeFragment
|
||||
import de.markusfisch.android.binaryeye.fragment.HistoryFragment
|
||||
import de.markusfisch.android.binaryeye.fragment.PreferencesFragment
|
||||
import de.markusfisch.android.binaryeye.view.colorSystemAndToolBars
|
||||
import de.markusfisch.android.binaryeye.view.initSystemBars
|
||||
import de.markusfisch.android.binaryeye.view.recordToolbarHeight
|
||||
|
||||
class MainActivity : AppCompatActivity() {
|
||||
override fun onRequestPermissionsResult(
|
||||
requestCode: Int,
|
||||
permissions: Array<String>,
|
||||
grantResults: IntArray
|
||||
) {
|
||||
when (requestCode) {
|
||||
PERMISSION_LOCATION, PERMISSION_WRITE -> if (grantResults.isNotEmpty() &&
|
||||
grantResults[0] == PackageManager.PERMISSION_GRANTED
|
||||
) {
|
||||
permissionGrantedCallback?.invoke()
|
||||
permissionGrantedCallback = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSupportNavigateUp(): Boolean {
|
||||
val fm = supportFragmentManager
|
||||
if (fm != null && fm.backStackEntryCount > 0) {
|
||||
fm.popBackStack()
|
||||
} else {
|
||||
finish()
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
override fun attachBaseContext(base: Context?) {
|
||||
base?.applyLocale(prefs.customLocale)
|
||||
super.attachBaseContext(base)
|
||||
}
|
||||
|
||||
override fun onCreate(state: Bundle?) {
|
||||
super.onCreate(state)
|
||||
setContentView(R.layout.activity_main)
|
||||
|
||||
initSystemBars(this)
|
||||
val toolbar = findViewById(R.id.toolbar) as Toolbar
|
||||
recordToolbarHeight(toolbar)
|
||||
setSupportActionBar(toolbar)
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
|
||||
supportFragmentManager.addOnBackStackChangedListener {
|
||||
colorSystemAndToolBars(this@MainActivity)
|
||||
}
|
||||
|
||||
if (state == null) {
|
||||
supportFragmentManager?.setFragment(getFragmentForIntent(intent))
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val PREFERENCES = "preferences"
|
||||
private const val HISTORY = "history"
|
||||
private const val ENCODE = "encode"
|
||||
const val DECODED = "decoded"
|
||||
|
||||
private fun getFragmentForIntent(intent: Intent?): Fragment {
|
||||
intent ?: return PreferencesFragment()
|
||||
return when {
|
||||
intent.hasExtra(PREFERENCES) -> PreferencesFragment()
|
||||
intent.hasExtra(HISTORY) -> HistoryFragment()
|
||||
intent.hasExtra(ENCODE) -> EncodeFragment.newInstance(
|
||||
intent.getStringExtra(ENCODE)
|
||||
)
|
||||
|
||||
intent.hasExtra(DECODED) -> intent.getScanExtra(DECODED)?.let {
|
||||
DecodeFragment.newInstance(it)
|
||||
} ?: DecodeFragment()
|
||||
|
||||
else -> PreferencesFragment()
|
||||
}
|
||||
}
|
||||
|
||||
fun getPreferencesIntent(context: Context): Intent {
|
||||
val intent = Intent(context, MainActivity::class.java)
|
||||
intent.putExtra(PREFERENCES, true)
|
||||
return intent
|
||||
}
|
||||
|
||||
fun getHistoryIntent(context: Context): Intent {
|
||||
val intent = Intent(context, MainActivity::class.java)
|
||||
intent.putExtra(HISTORY, true)
|
||||
return intent
|
||||
}
|
||||
|
||||
fun getEncodeIntent(
|
||||
context: Context,
|
||||
text: String? = "",
|
||||
isExternal: Boolean = false
|
||||
): Intent {
|
||||
val intent = Intent(context, MainActivity::class.java)
|
||||
intent.putExtra(ENCODE, text)
|
||||
if (isExternal) {
|
||||
val flagActivityClearTask =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
|
||||
Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||
} else 0
|
||||
intent.addFlags(
|
||||
Intent.FLAG_ACTIVITY_NO_HISTORY or
|
||||
flagActivityClearTask or
|
||||
Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
)
|
||||
}
|
||||
return intent
|
||||
}
|
||||
|
||||
fun getDecodeIntent(context: Context, scan: Scan): Intent {
|
||||
val intent = Intent(context, MainActivity::class.java)
|
||||
intent.putExtra(DECODED, scan)
|
||||
return intent
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun Intent.getScanExtra(name: String): Scan? = if (
|
||||
Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU
|
||||
) {
|
||||
@Suppress("DEPRECATION")
|
||||
getParcelableExtra(name)
|
||||
} else {
|
||||
getParcelableExtra(name, Scan::class.java)
|
||||
}
|
||||
+16
-29
@@ -1,43 +1,32 @@
|
||||
package de.markusfisch.android.binaryeye.fragment
|
||||
package de.markusfisch.android.binaryeye.activity
|
||||
|
||||
import android.content.Context
|
||||
import android.net.wifi.WifiManager
|
||||
import android.net.wifi.WifiNetworkSuggestion
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.support.annotation.RequiresApi
|
||||
import android.support.v4.app.Fragment
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.ListView
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.core.util.size
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.view.setPaddingFromWindowInsets
|
||||
import de.markusfisch.android.binaryeye.view.systemBarListViewScrollListener
|
||||
import de.markusfisch.android.binaryeye.widget.toast
|
||||
|
||||
class NetworkSuggestionsFragment : Fragment() {
|
||||
class NetworkSuggestionsActivity : AbstractBaseActivity() {
|
||||
@RequiresApi(Build.VERSION_CODES.R)
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
state: Bundle?
|
||||
): View? {
|
||||
val ac = activity ?: return null
|
||||
ac.setTitle(R.string.network_suggestions)
|
||||
override fun onCreate(state: Bundle?) {
|
||||
super.onCreate(state)
|
||||
setScreenContentView(R.layout.activity_network_suggestions)
|
||||
setTitle(R.string.network_suggestions)
|
||||
|
||||
val view = inflater.inflate(
|
||||
R.layout.fragment_network_suggestions,
|
||||
container,
|
||||
false
|
||||
)
|
||||
|
||||
val wm = ac.applicationContext.getSystemService(
|
||||
val wm = applicationContext.getSystemService(
|
||||
Context.WIFI_SERVICE
|
||||
) as WifiManager
|
||||
val suggestionArrayAdapter = ArrayAdapter(
|
||||
ac,
|
||||
this,
|
||||
android.R.layout.simple_list_item_checked,
|
||||
wm.networkSuggestions.map {
|
||||
Suggestion(
|
||||
@@ -47,18 +36,18 @@ class NetworkSuggestionsFragment : Fragment() {
|
||||
}
|
||||
)
|
||||
|
||||
val listView = view.findViewById<ListView>(R.id.suggestions)
|
||||
listView.emptyView = view.findViewById(R.id.no_suggestions)
|
||||
val listView = findViewById<ListView>(R.id.suggestions)
|
||||
listView.emptyView = findViewById(R.id.no_suggestions)
|
||||
listView.adapter = suggestionArrayAdapter
|
||||
listView.setOnScrollListener(systemBarListViewScrollListener)
|
||||
(view.findViewById(R.id.inset_layout) as View).setPaddingFromWindowInsets()
|
||||
findViewById<View>(R.id.inset_layout).setPaddingFromWindowInsets()
|
||||
listView.setPaddingFromWindowInsets()
|
||||
|
||||
view.findViewById<View>(R.id.remove).setOnClickListener {
|
||||
findViewById<View>(R.id.remove).setOnClickListener {
|
||||
val removeList = ArrayList<WifiNetworkSuggestion>()
|
||||
val removeFromAdapter = ArrayList<Suggestion>()
|
||||
val checked = listView.checkedItemPositions
|
||||
val size = checked.size()
|
||||
val size = checked.size
|
||||
for (i in 0 until size) {
|
||||
if (checked.valueAt(i)) {
|
||||
val pos = checked.keyAt(i)
|
||||
@@ -76,13 +65,11 @@ class NetworkSuggestionsFragment : Fragment() {
|
||||
}
|
||||
wm.removeNetworkSuggestions(removeList)
|
||||
} else {
|
||||
ac.toast(
|
||||
toast(
|
||||
R.string.clear_network_suggestions_nothing_to_remove
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return view
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,10 +9,10 @@ import android.graphics.RectF
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.support.v7.app.AppCompatActivity
|
||||
import android.support.v7.widget.Toolbar
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.app.applyLocale
|
||||
import de.markusfisch.android.binaryeye.app.prefs
|
||||
@@ -22,8 +22,7 @@ import de.markusfisch.android.binaryeye.graphics.loadImageUri
|
||||
import de.markusfisch.android.binaryeye.graphics.mapPosition
|
||||
import de.markusfisch.android.binaryeye.media.releaseToneGenerators
|
||||
import de.markusfisch.android.binaryeye.view.colorSystemAndToolBars
|
||||
import de.markusfisch.android.binaryeye.view.initSystemBars
|
||||
import de.markusfisch.android.binaryeye.view.recordToolbarHeight
|
||||
import de.markusfisch.android.binaryeye.view.initBars
|
||||
import de.markusfisch.android.binaryeye.view.scanFeedback
|
||||
import de.markusfisch.android.binaryeye.view.setPaddingFromWindowInsets
|
||||
import de.markusfisch.android.binaryeye.widget.CropImageView
|
||||
@@ -51,15 +50,13 @@ class PickActivity : AppCompatActivity() {
|
||||
tryRotate = true,
|
||||
tryInvert = true,
|
||||
tryDownscale = true,
|
||||
returnCodabarStartEnd = true,
|
||||
maxNumberOfSymbols = 1,
|
||||
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?) {
|
||||
@@ -75,11 +72,7 @@ class PickActivity : AppCompatActivity() {
|
||||
// locale.
|
||||
setTitle(R.string.pick_code_to_scan)
|
||||
|
||||
initSystemBars(this)
|
||||
val toolbar = findViewById(R.id.toolbar) as Toolbar
|
||||
recordToolbarHeight(toolbar)
|
||||
setSupportActionBar(toolbar)
|
||||
|
||||
initBars()
|
||||
supportFragmentManager.addOnBackStackChangedListener {
|
||||
colorSystemAndToolBars(this@PickActivity)
|
||||
}
|
||||
@@ -88,25 +81,36 @@ class PickActivity : AppCompatActivity() {
|
||||
if (bitmap == null) {
|
||||
applicationContext.toast(R.string.error_no_content)
|
||||
finish()
|
||||
// This is why cropImageView and detectorView aren't lateinit.
|
||||
// If they were, a kotlin.UninitializedPropertyAccessException
|
||||
// would happen here.
|
||||
return
|
||||
}
|
||||
|
||||
cropImageView = findViewById(R.id.image) as CropImageView
|
||||
cropImageView.restrictTranslation = false
|
||||
cropImageView.freeRotation = prefs.freeRotation
|
||||
cropImageView.setImageBitmap(bitmap)
|
||||
cropImageView.onScan = {
|
||||
scanWithinBounds(bitmap)
|
||||
cropImageView = (findViewById(R.id.image) as CropImageView).apply {
|
||||
restrictTranslation = false
|
||||
freeRotation = prefs.freeRotation
|
||||
setImageBitmap(bitmap)
|
||||
onScan = {
|
||||
scanWithinBounds(bitmap)
|
||||
}
|
||||
runAfterLayout = {
|
||||
minWidth = min(
|
||||
minWidth / 2f,
|
||||
max(bitmap.width, bitmap.height).toFloat()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
detectorView = findViewById(R.id.detector_view) as DetectorView
|
||||
detectorView.onRoiChanged = {
|
||||
scanWithinBounds(bitmap)
|
||||
detectorView = (findViewById(R.id.detector_view) as DetectorView).apply {
|
||||
onRoiChanged = {
|
||||
scanWithinBounds(bitmap)
|
||||
}
|
||||
setPaddingFromWindowInsets()
|
||||
cropHandleName = "picker_crop_handle"
|
||||
}
|
||||
detectorView.setPaddingFromWindowInsets()
|
||||
detectorView.restoreCropHandlePos()
|
||||
|
||||
findViewById(R.id.scan).setOnClickListener {
|
||||
findViewById<View>(R.id.scan).setOnClickListener {
|
||||
showResult()
|
||||
}
|
||||
}
|
||||
@@ -126,17 +130,19 @@ class PickActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
private fun scanWithinBounds(bitmap: Bitmap) {
|
||||
val dv = detectorView ?: return
|
||||
val civ = cropImageView ?: return
|
||||
val viewRoi = Rect(
|
||||
if (detectorView.roi.width() < 1) {
|
||||
cropImageView.getBoundsRect()
|
||||
if (dv.roi.width() < 1) {
|
||||
civ.getBoundsRect()
|
||||
} else {
|
||||
detectorView.roi
|
||||
dv.roi
|
||||
}
|
||||
)
|
||||
val mappedRect = RectF(cropImageView.mappedRect)
|
||||
val imageRotation = cropImageView.imageRotation
|
||||
val pivotX = cropImageView.pivotX
|
||||
val pivotY = cropImageView.pivotY
|
||||
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),
|
||||
@@ -167,10 +173,10 @@ class PickActivity : AppCompatActivity() {
|
||||
}
|
||||
result = it
|
||||
scanFeedback()
|
||||
detectorView.update(
|
||||
dv.update(
|
||||
matrix.mapPosition(
|
||||
it.position,
|
||||
detectorView.coordinates
|
||||
dv.coordinates
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -200,7 +206,7 @@ class PickActivity : AppCompatActivity() {
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
detectorView.saveCropHandlePos()
|
||||
detectorView?.storeCropHandlePos()
|
||||
parentJob.cancel()
|
||||
releaseToneGenerators()
|
||||
}
|
||||
@@ -222,8 +228,8 @@ class PickActivity : AppCompatActivity() {
|
||||
|
||||
R.id.toggle_free -> {
|
||||
prefs.freeRotation = prefs.freeRotation xor true
|
||||
cropImageView.freeRotation = prefs.freeRotation
|
||||
freeRotationItem.updateFreeRotationIcon()
|
||||
cropImageView?.freeRotation = prefs.freeRotation
|
||||
freeRotationItem?.updateFreeRotationIcon()
|
||||
true
|
||||
}
|
||||
|
||||
@@ -242,7 +248,8 @@ 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? {
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
package de.markusfisch.android.binaryeye.activity
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.app.applyLocale
|
||||
import de.markusfisch.android.binaryeye.app.prefs
|
||||
import de.markusfisch.android.binaryeye.fragment.PreferencesFragment
|
||||
import de.markusfisch.android.binaryeye.view.initBars
|
||||
|
||||
class PreferencesActivity : AppCompatActivity() {
|
||||
override fun attachBaseContext(base: Context?) {
|
||||
base?.applyLocale(prefs.customLocale)
|
||||
super.attachBaseContext(base)
|
||||
}
|
||||
|
||||
override fun onCreate(state: Bundle?) {
|
||||
super.onCreate(state)
|
||||
setContentView(R.layout.activity_main)
|
||||
initBars()
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
if (state == null) {
|
||||
supportFragmentManager.beginTransaction()
|
||||
.replace(R.id.content_frame, PreferencesFragment())
|
||||
.commit()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSupportNavigateUp(): Boolean {
|
||||
finish()
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
package de.markusfisch.android.binaryeye.activity
|
||||
|
||||
import android.app.AlertDialog
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.EditText
|
||||
import android.widget.ListView
|
||||
import android.widget.TextView
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.app.prefs
|
||||
import de.markusfisch.android.binaryeye.view.setPaddingFromWindowInsets
|
||||
import de.markusfisch.android.binaryeye.view.systemBarListViewScrollListener
|
||||
import de.markusfisch.android.binaryeye.widget.toast
|
||||
|
||||
class ProfilesActivity : AbstractBaseActivity() {
|
||||
private val profiles = ArrayList<ProfileItem>()
|
||||
|
||||
private lateinit var adapter: ProfilesAdapter
|
||||
|
||||
override fun onCreate(state: Bundle?) {
|
||||
super.onCreate(state)
|
||||
setScreenContentView(R.layout.activity_profiles)
|
||||
setTitle(R.string.profile)
|
||||
|
||||
refreshProfiles()
|
||||
|
||||
val listView = findViewById<ListView>(R.id.profiles)
|
||||
listView.emptyView = findViewById(R.id.no_profiles)
|
||||
adapter = ProfilesAdapter(this, profiles)
|
||||
listView.adapter = adapter
|
||||
listView.setOnScrollListener(systemBarListViewScrollListener)
|
||||
listView.setOnItemClickListener { _, _, position, _ ->
|
||||
selectProfile(position)
|
||||
}
|
||||
listView.setOnItemLongClickListener { _, _, position, _ ->
|
||||
confirmRemoveProfile(position)
|
||||
true
|
||||
}
|
||||
|
||||
findViewById<View>(R.id.add).setOnClickListener {
|
||||
addProfile()
|
||||
}
|
||||
|
||||
findViewById<View>(R.id.inset_layout).setPaddingFromWindowInsets()
|
||||
listView.setPaddingFromWindowInsets()
|
||||
}
|
||||
|
||||
private fun refreshProfiles() {
|
||||
profiles.clear()
|
||||
profiles.add(
|
||||
ProfileItem(
|
||||
null,
|
||||
getString(R.string.profile_default)
|
||||
)
|
||||
)
|
||||
prefs.profiles.forEach { name ->
|
||||
profiles.add(ProfileItem(name, name))
|
||||
}
|
||||
if (this::adapter.isInitialized) {
|
||||
adapter.notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
|
||||
private fun selectProfile(position: Int) {
|
||||
val profile = profiles[position].name
|
||||
if (prefs.profile == profile) {
|
||||
return
|
||||
}
|
||||
prefs.load(this, profile)
|
||||
adapter.notifyDataSetChanged()
|
||||
}
|
||||
|
||||
// Dialogs do not have a parent view.
|
||||
@Suppress("InflateParams")
|
||||
private fun addProfile() {
|
||||
val view = layoutInflater.inflate(
|
||||
R.layout.dialog_profile_name, null
|
||||
)
|
||||
val editText = view.findViewById<EditText>(R.id.name)
|
||||
AlertDialog.Builder(this)
|
||||
.setView(view)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
val name = editText.text.toString().trim()
|
||||
if (name.isNotEmpty() && prefs.addProfile(name)) {
|
||||
prefs.load(this, name)
|
||||
refreshProfiles()
|
||||
} else {
|
||||
toast(R.string.profile_invalid_name)
|
||||
}
|
||||
}
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun confirmRemoveProfile(position: Int) {
|
||||
val profile = profiles[position]
|
||||
val profileName = profile.name ?: return
|
||||
AlertDialog.Builder(this)
|
||||
.setMessage(R.string.profile_remove_confirm)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
val isCurrent = prefs.profile == profileName
|
||||
prefs.removeProfile(profileName)
|
||||
if (isCurrent) {
|
||||
prefs.load(this, null)
|
||||
}
|
||||
refreshProfiles()
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
||||
.show()
|
||||
}
|
||||
|
||||
private class ProfilesAdapter(
|
||||
context: Context,
|
||||
private val items: List<ProfileItem>
|
||||
) : ArrayAdapter<ProfileItem>(
|
||||
context,
|
||||
android.R.layout.simple_list_item_2,
|
||||
android.R.id.text1,
|
||||
items
|
||||
) {
|
||||
override fun getView(
|
||||
position: Int,
|
||||
convertView: View?,
|
||||
parent: ViewGroup
|
||||
): View {
|
||||
val view = super.getView(position, convertView, parent)
|
||||
val item = items[position]
|
||||
val isCurrent = item.name == prefs.profile
|
||||
getViewHolder(view).apply {
|
||||
title.text = item.label
|
||||
if (isCurrent) {
|
||||
subtitle.text = context.getString(R.string.profile_current)
|
||||
subtitle.visibility = View.VISIBLE
|
||||
} else {
|
||||
subtitle.text = ""
|
||||
subtitle.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
return view
|
||||
}
|
||||
|
||||
private fun getViewHolder(
|
||||
view: View
|
||||
): ViewHolder = view.tag as ViewHolder? ?: ViewHolder(
|
||||
view.findViewById(android.R.id.text1),
|
||||
view.findViewById(android.R.id.text2),
|
||||
).also {
|
||||
view.tag = it
|
||||
}
|
||||
|
||||
private data class ViewHolder(
|
||||
val title: TextView,
|
||||
val subtitle: TextView,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private data class ProfileItem(
|
||||
val name: String?,
|
||||
val label: String
|
||||
)
|
||||
@@ -2,7 +2,7 @@ package de.markusfisch.android.binaryeye.activity
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.support.v7.app.AppCompatActivity
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
|
||||
class SplashActivity : AppCompatActivity() {
|
||||
override fun onCreate(state: Bundle?) {
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
package de.markusfisch.android.binaryeye.activity
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.app.prefs
|
||||
|
||||
class WelcomeActivity : AppCompatActivity() {
|
||||
override fun onCreate(state: Bundle?) {
|
||||
super.onCreate(state)
|
||||
setContentView(R.layout.activity_welcome)
|
||||
findViewById<View>(R.id.expert).setOnClickListener {
|
||||
finish()
|
||||
}
|
||||
findViewById<View>(R.id.simple).setOnClickListener {
|
||||
prefs.apply {
|
||||
showCropHandle = false
|
||||
zoomBySwiping = false
|
||||
beep = true
|
||||
copyImmediately = true
|
||||
openImmediately = true
|
||||
showMetaData = false
|
||||
showHexDump = false
|
||||
defaultSearchUrl = "https://www.google.com/search?q="
|
||||
openWithUrl = defaultSearchUrl
|
||||
expandEscapeSequences = false
|
||||
brightenScreen = true
|
||||
}
|
||||
finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,12 +2,16 @@ package de.markusfisch.android.binaryeye.adapter
|
||||
|
||||
import android.content.Context
|
||||
import android.database.Cursor
|
||||
import android.support.v4.content.ContextCompat
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.CursorAdapter
|
||||
import android.widget.ImageButton
|
||||
import android.widget.ImageView
|
||||
import android.widget.Spinner
|
||||
import android.widget.TextView
|
||||
import androidx.core.content.ContextCompat
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.database.Database
|
||||
import java.text.DateFormat
|
||||
@@ -15,12 +19,17 @@ import java.text.ParseException
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
|
||||
class ScansAdapter(context: Context, cursor: Cursor) :
|
||||
class ScansAdapter(
|
||||
context: Context,
|
||||
cursor: Cursor,
|
||||
private val onPinClick: (Long, Boolean) -> Unit
|
||||
) :
|
||||
CursorAdapter(context, cursor, false) {
|
||||
private val idIndex = cursor.getColumnIndex(Database.SCANS_ID)
|
||||
private val pinnedIndex = cursor.getColumnIndex(Database.SCANS_PINNED)
|
||||
private val timeIndex = cursor.getColumnIndex(Database.SCANS_DATETIME)
|
||||
private val 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 +83,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,
|
||||
@@ -89,10 +98,18 @@ class ScansAdapter(context: Context, cursor: Cursor) :
|
||||
context: Context,
|
||||
cursor: Cursor
|
||||
) {
|
||||
val id = cursor.getLong(idIndex)
|
||||
val pinned = cursor.getInt(pinnedIndex) != 0
|
||||
val format = cursor.getString(formatIndex)
|
||||
getViewHolder(view).apply {
|
||||
timeView.text = formatDateTime(cursor.getString(timeIndex))
|
||||
metaView.text = context.getString(
|
||||
R.string.scan_meta_data,
|
||||
formatDateTime(cursor.getString(timeIndex)),
|
||||
format.prettifyFormatName()
|
||||
)
|
||||
formatIconView.setImageResource(format.toBarcodeIcon())
|
||||
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,18 +117,36 @@ 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))
|
||||
pinView.apply {
|
||||
setImageResource(
|
||||
if (pinned) {
|
||||
R.drawable.ic_action_pin
|
||||
} else {
|
||||
R.drawable.ic_action_pin_outline
|
||||
}
|
||||
)
|
||||
contentDescription = context.getString(
|
||||
if (pinned) {
|
||||
R.string.unpin_item
|
||||
} else {
|
||||
R.string.pin_item
|
||||
}
|
||||
)
|
||||
setOnClickListener {
|
||||
onPinClick(id, !pinned)
|
||||
}
|
||||
}
|
||||
}
|
||||
val selected = selections[cursor.getLong(idIndex)] != null
|
||||
val selected = selections[id] != null
|
||||
view.post {
|
||||
// Needs to be put on the queue to work.
|
||||
view.select(selected)
|
||||
@@ -125,21 +160,97 @@ class ScansAdapter(context: Context, cursor: Cursor) :
|
||||
private fun getViewHolder(
|
||||
view: View
|
||||
): ViewHolder = view.tag as ViewHolder? ?: ViewHolder(
|
||||
view.findViewById(R.id.time),
|
||||
view.findViewById(R.id.format_icon),
|
||||
view.findViewById(R.id.meta),
|
||||
view.findViewById(R.id.content),
|
||||
view.findViewById(R.id.format)
|
||||
view.findViewById(R.id.pin)
|
||||
).also {
|
||||
view.tag = it
|
||||
}
|
||||
|
||||
private data class ViewHolder(
|
||||
val timeView: TextView,
|
||||
val formatIconView: ImageView,
|
||||
val metaView: TextView,
|
||||
val contentView: TextView,
|
||||
val formatView: TextView
|
||||
val pinView: ImageButton
|
||||
)
|
||||
}
|
||||
|
||||
fun prettifyFormatName(format: String) = format.replace("_", " ")
|
||||
private val formatWordBoundary =
|
||||
"(?<=[a-z0-9])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])|(?<=[A-Za-z])(?=\\d)|(?<=\\d)(?=[A-Za-z])".toRegex()
|
||||
|
||||
fun Context.setupFormatSpinner(spinner: Spinner): List<String> {
|
||||
val names = resources.getStringArray(
|
||||
R.array.barcode_formats_names
|
||||
).toMutableList().apply {
|
||||
add(0, getString(R.string.all_formats))
|
||||
}
|
||||
val values = resources.getStringArray(
|
||||
R.array.barcode_formats_values
|
||||
).toMutableList().apply {
|
||||
add(0, "")
|
||||
}
|
||||
spinner.adapter = ArrayAdapter(
|
||||
this,
|
||||
android.R.layout.simple_spinner_item,
|
||||
names
|
||||
).apply {
|
||||
setDropDownViewResource(
|
||||
android.R.layout.simple_spinner_dropdown_item
|
||||
)
|
||||
}
|
||||
return values
|
||||
}
|
||||
|
||||
fun String.prettifyFormatName() = replace("_", " ")
|
||||
.replace(formatWordBoundary, " ")
|
||||
|
||||
fun String.toFormatDescriptionResId(): Int = when (this) {
|
||||
"Aztec" -> R.string.format_description_aztec
|
||||
"Codabar" -> R.string.format_description_codabar
|
||||
"Code39",
|
||||
"Code39Std" -> R.string.format_description_code39
|
||||
|
||||
"Code39Ext" -> R.string.format_description_code39ext
|
||||
"Code32" -> R.string.format_description_code32
|
||||
"PZN" -> R.string.format_description_pzn
|
||||
"Code93" -> R.string.format_description_code93
|
||||
"Code128" -> R.string.format_description_code128
|
||||
"DataBar" -> R.string.format_description_databar
|
||||
"DataBarOmni" -> R.string.format_description_databar_omni
|
||||
"DataBarStk" -> R.string.format_description_databar_stk
|
||||
"DataBarLtd" -> R.string.format_description_databar_ltd
|
||||
"DataBarExp" -> R.string.format_description_databar_exp
|
||||
"DataBarExpStk" -> R.string.format_description_databar_exp_stk
|
||||
"DataMatrix" -> R.string.format_description_data_matrix
|
||||
"DXFilmEdge" -> R.string.format_description_dx_film_edge
|
||||
"EAN8" -> R.string.format_description_ean8
|
||||
"EAN13" -> R.string.format_description_ean13
|
||||
"ITF" -> R.string.format_description_itf
|
||||
"MaxiCode" -> R.string.format_description_maxicode
|
||||
"PDF417" -> R.string.format_description_pdf417
|
||||
"QRCode" -> R.string.format_description_qr_code
|
||||
"MicroQRCode" -> R.string.format_description_micro_qr_code
|
||||
"RMQRCode" -> R.string.format_description_rmqr_code
|
||||
"UPCA" -> R.string.format_description_upca
|
||||
"UPCE" -> R.string.format_description_upce
|
||||
else -> 0
|
||||
}
|
||||
|
||||
private fun String?.toBarcodeIcon(): Int = when (this) {
|
||||
"QRCode",
|
||||
"RMQRCode" -> R.drawable.ic_barcode_qr_code
|
||||
|
||||
"MicroQRCode" -> R.drawable.ic_barcode_micro_qr
|
||||
|
||||
"Aztec" -> R.drawable.ic_barcode_aztec
|
||||
"DataMatrix",
|
||||
"MaxiCode" -> R.drawable.ic_barcode_data_matrix
|
||||
|
||||
"PDF417" -> R.drawable.ic_barcode_pdf417
|
||||
|
||||
else -> R.drawable.ic_barcode_linear
|
||||
}
|
||||
|
||||
private fun formatDateTime(rfc: String): String {
|
||||
try {
|
||||
@@ -149,7 +260,7 @@ private fun formatDateTime(rfc: String): String {
|
||||
DateFormat.SHORT
|
||||
).format(it)
|
||||
}
|
||||
} catch (e: ParseException) {
|
||||
} catch (_: ParseException) {
|
||||
// Ignore and fall through.
|
||||
}
|
||||
return rfc
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
package de.markusfisch.android.binaryeye.app
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.support.v4.app.Fragment
|
||||
import android.support.v4.app.FragmentManager
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
|
||||
fun FragmentManager.setFragment(fragment: Fragment) {
|
||||
getTransaction(fragment).commit()
|
||||
}
|
||||
|
||||
fun FragmentManager.addFragment(fragment: Fragment) {
|
||||
getTransaction(fragment).addToBackStack(null).commit()
|
||||
}
|
||||
|
||||
@SuppressLint("CommitTransaction")
|
||||
private fun FragmentManager.getTransaction(
|
||||
fragment: Fragment
|
||||
) = beginTransaction().replace(R.id.content_frame, fragment)
|
||||
@@ -4,8 +4,8 @@ import android.Manifest
|
||||
import android.app.Activity
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import android.support.v4.app.ActivityCompat
|
||||
import android.support.v4.content.ContextCompat
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
|
||||
var permissionGrantedCallback: (() -> Any)? = null
|
||||
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
package de.markusfisch.android.binaryeye.automation
|
||||
|
||||
import de.markusfisch.android.binaryeye.database.Scan
|
||||
import de.markusfisch.android.binaryeye.net.urlEncode
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
import java.util.Locale
|
||||
|
||||
data class AutomatedAction(
|
||||
val pattern: String,
|
||||
val type: Type,
|
||||
val format: String? = null,
|
||||
val urlTemplate: String? = null,
|
||||
val intentUriTemplate: String? = null
|
||||
) {
|
||||
enum class Type {
|
||||
Intent, CustomIntent
|
||||
}
|
||||
|
||||
fun matches(content: String, format: String): Boolean = try {
|
||||
(this.format == null || this.format == format) &&
|
||||
Regex(pattern).matches(content)
|
||||
} catch (_: Exception) {
|
||||
false
|
||||
}
|
||||
|
||||
fun toJson(): JSONObject = JSONObject().apply {
|
||||
put(KEY_PATTERN, pattern)
|
||||
put(KEY_TYPE, type.name.lowercase(Locale.ROOT))
|
||||
put(KEY_FORMAT, format ?: "")
|
||||
put(KEY_URL_TEMPLATE, urlTemplate ?: "")
|
||||
put(KEY_INTENT_URI_TEMPLATE, intentUriTemplate ?: "")
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val KEY_PATTERN = "pattern"
|
||||
private const val KEY_TYPE = "type"
|
||||
private const val KEY_FORMAT = "format"
|
||||
private const val KEY_URL_TEMPLATE = "url_template"
|
||||
private const val KEY_INTENT_URI_TEMPLATE = "intent_uri_template"
|
||||
|
||||
fun fromJson(obj: JSONObject): AutomatedAction? {
|
||||
val pattern = obj.optString(KEY_PATTERN, "").trim()
|
||||
if (pattern.isEmpty()) {
|
||||
return null
|
||||
}
|
||||
val type = when (obj.optString(KEY_TYPE, "")) {
|
||||
"custom_intent" -> Type.CustomIntent
|
||||
"intent" -> Type.Intent
|
||||
"html" -> return null
|
||||
else -> Type.Intent
|
||||
}
|
||||
val format = obj.optString(KEY_FORMAT, "").trim()
|
||||
val urlTemplate = obj.optString(KEY_URL_TEMPLATE, "").trim()
|
||||
val intentUriTemplate = obj.optString(
|
||||
KEY_INTENT_URI_TEMPLATE,
|
||||
""
|
||||
).trim()
|
||||
return AutomatedAction(
|
||||
pattern = pattern,
|
||||
type = type,
|
||||
format = format.ifEmpty { null },
|
||||
urlTemplate = urlTemplate.ifEmpty { null },
|
||||
intentUriTemplate = intentUriTemplate.ifEmpty { null }
|
||||
)
|
||||
}
|
||||
|
||||
fun fromJsonArray(json: String): MutableList<AutomatedAction> {
|
||||
val actions = ArrayList<AutomatedAction>()
|
||||
val array = try {
|
||||
JSONArray(json)
|
||||
} catch (_: Exception) {
|
||||
JSONArray()
|
||||
}
|
||||
for (i in 0 until array.length()) {
|
||||
val obj = array.optJSONObject(i) ?: continue
|
||||
fromJson(obj)?.let { actions.add(it) }
|
||||
}
|
||||
return actions
|
||||
}
|
||||
|
||||
fun toJsonArray(actions: List<AutomatedAction>): String {
|
||||
val array = JSONArray()
|
||||
actions.forEach { array.put(it.toJson()) }
|
||||
return array.toString()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun String.buildUrl(scan: Scan): String {
|
||||
return buildTemplate(scan, encodeResult = true)
|
||||
}
|
||||
|
||||
fun String.buildTemplate(scan: Scan, encodeResult: Boolean): String {
|
||||
val text = scan.text
|
||||
val raw = scan.raw ?: text.toByteArray()
|
||||
val result = if (encodeResult) {
|
||||
text.urlEncode()
|
||||
} else {
|
||||
text
|
||||
}
|
||||
val format = if (encodeResult) {
|
||||
scan.format.name.urlEncode()
|
||||
} else {
|
||||
scan.format.name
|
||||
}
|
||||
return this
|
||||
.replace("{RESULT}", result)
|
||||
.replace("{RESULT_BYTES}", raw.toHexString())
|
||||
.replace("{FORMAT}", format)
|
||||
}
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
package de.markusfisch.android.binaryeye.automation
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.app.prefs
|
||||
import de.markusfisch.android.binaryeye.content.execShareIntent
|
||||
import de.markusfisch.android.binaryeye.content.openUrl
|
||||
import de.markusfisch.android.binaryeye.database.Scan
|
||||
import de.markusfisch.android.binaryeye.widget.toast
|
||||
|
||||
fun Context.runAutomatedActions(scan: Scan): Boolean {
|
||||
val content = scan.text
|
||||
if (content.isEmpty()) {
|
||||
return false
|
||||
}
|
||||
val actions = prefs.automatedActions
|
||||
if (actions.isEmpty()) {
|
||||
return false
|
||||
}
|
||||
for (action in actions) {
|
||||
if (action.matches(content, scan.format.name)) {
|
||||
return when (action.type) {
|
||||
AutomatedAction.Type.Intent -> {
|
||||
val url = action.urlTemplate?.buildUrl(scan)
|
||||
if (url.isNullOrEmpty()) {
|
||||
toast(R.string.automated_action_invalid_url)
|
||||
false
|
||||
} else {
|
||||
openUrl(url)
|
||||
}
|
||||
}
|
||||
|
||||
AutomatedAction.Type.CustomIntent ->
|
||||
execCustomIntent(action, scan)
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private fun Context.execCustomIntent(
|
||||
action: AutomatedAction,
|
||||
scan: Scan
|
||||
): Boolean {
|
||||
val template = action.intentUriTemplate
|
||||
if (template.isNullOrEmpty()) {
|
||||
toast(R.string.automated_action_intent_uri_required)
|
||||
return false
|
||||
}
|
||||
val intentUri = template.buildTemplate(scan, encodeResult = false)
|
||||
val intent = try {
|
||||
Intent.parseUri(intentUri, Intent.URI_INTENT_SCHEME)
|
||||
} catch (_: Exception) {
|
||||
toast(R.string.automated_action_intent_uri_invalid)
|
||||
return false
|
||||
}
|
||||
return execShareIntent(intent)
|
||||
}
|
||||
@@ -3,7 +3,7 @@ package de.markusfisch.android.binaryeye.bluetooth
|
||||
import android.bluetooth.BluetoothAdapter
|
||||
import android.bluetooth.BluetoothDevice
|
||||
import android.bluetooth.BluetoothSocket
|
||||
import android.support.v7.preference.ListPreference
|
||||
import androidx.preference.ListPreference
|
||||
import de.markusfisch.android.binaryeye.database.Scan
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -24,7 +24,7 @@ fun Scan.sendBluetoothAsync(
|
||||
connect(host, true)
|
||||
}
|
||||
val sent = if (connected) {
|
||||
send(content, host, true)
|
||||
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
|
||||
@@ -71,39 +70,41 @@ private fun connect(deviceName: String, onceMore: Boolean): Boolean = try {
|
||||
writer = socket?.outputStream?.writer()
|
||||
isConnected = true
|
||||
true
|
||||
} catch (e: Exception) {
|
||||
if (onceMore)
|
||||
} catch (_: Exception) {
|
||||
if (onceMore) {
|
||||
connect(deviceName, false)
|
||||
else
|
||||
} else {
|
||||
close()
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
private fun send(message: String, host: String, onceMore: Boolean): 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()
|
||||
|
||||
if (connect(host, false))
|
||||
send(message, host, false)
|
||||
else
|
||||
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.
|
||||
}
|
||||
@@ -113,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,218 @@
|
||||
package de.markusfisch.android.binaryeye.content
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.util.Size
|
||||
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(),
|
||||
addQuietZone = true
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
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 addQuietZone: Boolean = false,
|
||||
colors: BarcodeColors = BarcodeColors.BLACK_ON_WHITE
|
||||
) : Barcode<T>(content, format, colors) {
|
||||
override fun toBitmap(size: Int): Bitmap {
|
||||
val s = format.resolveSize(size)
|
||||
return ZxingCpp.encodeAsBitmap(
|
||||
content, format, s.width, s.height, addQuietZone, ecLevel,
|
||||
setColor = colors.foregroundColor(),
|
||||
unsetColor = colors.backgroundColor()
|
||||
)
|
||||
}
|
||||
|
||||
override fun toSvg() = ZxingCpp.encodeAsSvg(
|
||||
content, format, addQuietZone, ecLevel
|
||||
)
|
||||
|
||||
override fun toText() = ZxingCpp.encodeAsText(
|
||||
content, format, addQuietZone, 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.resolveSize(size: Int): Size = when (this) {
|
||||
// 1D barcodes don't need as much vertical space.
|
||||
BarcodeFormat.Codabar,
|
||||
BarcodeFormat.Code39,
|
||||
BarcodeFormat.Code39Std,
|
||||
BarcodeFormat.Code39Ext,
|
||||
BarcodeFormat.Code32,
|
||||
BarcodeFormat.PZN,
|
||||
BarcodeFormat.Code93,
|
||||
BarcodeFormat.Code128,
|
||||
BarcodeFormat.DataBarOmni,
|
||||
BarcodeFormat.DataBarStk,
|
||||
BarcodeFormat.DataBarLtd,
|
||||
BarcodeFormat.DataBarExp,
|
||||
BarcodeFormat.DataBarExpStk,
|
||||
BarcodeFormat.DXFilmEdge,
|
||||
BarcodeFormat.EAN8,
|
||||
BarcodeFormat.EAN13,
|
||||
BarcodeFormat.ITF,
|
||||
BarcodeFormat.UPCA,
|
||||
BarcodeFormat.UPCE -> Size(size, size / 3)
|
||||
|
||||
// PDF417 requires more horizontal space.
|
||||
BarcodeFormat.PDF417 -> Size(size * 3, size / 2)
|
||||
|
||||
else -> Size(size, size)
|
||||
}
|
||||
@@ -8,16 +8,11 @@ import android.os.PersistableBundle
|
||||
|
||||
fun Context.copyToClipboard(text: String, isSensitive: Boolean = false) {
|
||||
val service = getSystemService(Context.CLIPBOARD_SERVICE)
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) {
|
||||
@Suppress("DEPRECATION")
|
||||
(service as android.text.ClipboardManager).text = text
|
||||
} else {
|
||||
(service as android.content.ClipboardManager).setPrimaryClip(
|
||||
ClipData.newPlainText("plain text", text).apply {
|
||||
setSensitive(isSensitive)
|
||||
}
|
||||
)
|
||||
}
|
||||
(service as android.content.ClipboardManager).setPrimaryClip(
|
||||
ClipData.newPlainText("plain text", text).apply {
|
||||
setSensitive(isSensitive)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun ClipData.setSensitive(isSensitive: Boolean) {
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
package de.markusfisch.android.binaryeye.content
|
||||
|
||||
import android.content.Context
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
|
||||
private val elementToLabel = mapOf(
|
||||
EpcQrElement.SERVICE_TAG to R.string.epc_service_tag,
|
||||
EpcQrElement.VERSION to R.string.epc_version,
|
||||
EpcQrElement.CHARSET to R.string.epc_charset,
|
||||
EpcQrElement.IDENTIFICATION to R.string.epc_identification,
|
||||
EpcQrElement.BIC to R.string.epc_bic,
|
||||
EpcQrElement.NAME to R.string.epc_name,
|
||||
EpcQrElement.IBAN to R.string.epc_iban,
|
||||
EpcQrElement.AMOUNT to R.string.epc_amount,
|
||||
EpcQrElement.PURPOSE to R.string.epc_purpose,
|
||||
EpcQrElement.REFERENCE to R.string.epc_reference,
|
||||
EpcQrElement.TEXT to R.string.epc_text,
|
||||
EpcQrElement.INFORMATION to R.string.epc_information
|
||||
)
|
||||
|
||||
fun Context.epcQrToRes(element: EpcQrElement): String {
|
||||
return elementToLabel[element]?.let { getString(it) } ?: element.name
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
package de.markusfisch.android.binaryeye.content
|
||||
|
||||
enum class EpcQrElement {
|
||||
SERVICE_TAG, VERSION, CHARSET, IDENTIFICATION,
|
||||
BIC, NAME, IBAN, AMOUNT, PURPOSE, REFERENCE, TEXT, INFORMATION
|
||||
}
|
||||
|
||||
object EpcQrParser {
|
||||
private val BIC_REGEX = Regex("[A-Z0-9]{8}([A-Z0-9]{3})?")
|
||||
private val IBAN_REGEX = Regex("[A-Z]{2}[0-9A-Z]{13,32}")
|
||||
private val AMOUNT_REGEX = Regex("EUR\\d{1,10}(\\.\\d{2})?")
|
||||
private val PURPOSE_REGEX = Regex("[A-Z]{4}")
|
||||
|
||||
private val charsetMap = mapOf(
|
||||
"1" to "UTF-8",
|
||||
"2" to "ISO 8859-1",
|
||||
"3" to "ISO 8859-2",
|
||||
"4" to "ISO 8859-4",
|
||||
"5" to "ISO 8859-5",
|
||||
"6" to "ISO 8859-7",
|
||||
"7" to "ISO 8859-10",
|
||||
"8" to "ISO 8859-15"
|
||||
)
|
||||
|
||||
private fun splitLines(s: String): List<String>? {
|
||||
val normalized = s
|
||||
.replace("\r\n", "\n")
|
||||
.replace('\r', '\n')
|
||||
.removeSuffix("\n")
|
||||
if (normalized.isEmpty()) {
|
||||
return null
|
||||
}
|
||||
return normalized.split('\n')
|
||||
}
|
||||
|
||||
private fun isValidVersion(value: String): Boolean {
|
||||
return value == "001" || value == "002"
|
||||
}
|
||||
|
||||
private fun isValidCharset(value: String): Boolean {
|
||||
return charsetMap.containsKey(value)
|
||||
}
|
||||
|
||||
private fun isValidIdentification(value: String): Boolean {
|
||||
return value == "SCT"
|
||||
}
|
||||
|
||||
private fun isValidBic(value: String): Boolean {
|
||||
return value.matches(BIC_REGEX)
|
||||
}
|
||||
|
||||
private fun isValidIban(value: String): Boolean {
|
||||
return value.matches(IBAN_REGEX)
|
||||
}
|
||||
|
||||
private fun isValidAmount(value: String): Boolean {
|
||||
return value.matches(AMOUNT_REGEX)
|
||||
}
|
||||
|
||||
fun parse(s: String?): Map<EpcQrElement, String>? {
|
||||
if (s.isNullOrEmpty()) {
|
||||
return null
|
||||
}
|
||||
|
||||
val lines = splitLines(s) ?: return null
|
||||
if (lines.size !in 7..12) {
|
||||
return null
|
||||
}
|
||||
if (lines[0] != "BCD") {
|
||||
return null
|
||||
}
|
||||
if (!isValidVersion(lines[1])) {
|
||||
return null
|
||||
}
|
||||
if (!isValidCharset(lines[2])) {
|
||||
return null
|
||||
}
|
||||
if (!isValidIdentification(lines[3])) {
|
||||
return null
|
||||
}
|
||||
|
||||
val bic = lines.getOrElse(4) { "" }
|
||||
if ((lines[1] == "001" || bic.isNotEmpty()) && !isValidBic(bic)) {
|
||||
return null
|
||||
}
|
||||
|
||||
val recipient = lines.getOrElse(5) { "" }
|
||||
if (recipient.isBlank() || recipient.length > 70) {
|
||||
return null
|
||||
}
|
||||
|
||||
val iban = lines.getOrElse(6) { "" }
|
||||
if (!isValidIban(iban)) {
|
||||
return null
|
||||
}
|
||||
|
||||
val amount = lines.getOrElse(7) { "" }
|
||||
if (amount.isNotEmpty() && !isValidAmount(amount)) {
|
||||
return null
|
||||
}
|
||||
|
||||
val purpose = lines.getOrElse(8) { "" }
|
||||
if (purpose.isNotEmpty() && !purpose.matches(PURPOSE_REGEX)) {
|
||||
return null
|
||||
}
|
||||
|
||||
val remittanceReference = lines.getOrElse(9) { "" }
|
||||
if (remittanceReference.length > 25) {
|
||||
return null
|
||||
}
|
||||
|
||||
val remittanceText = lines.getOrElse(10) { "" }
|
||||
if (remittanceText.length > 140) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (remittanceReference.isNotEmpty() && remittanceText.isNotEmpty()) {
|
||||
return null
|
||||
}
|
||||
|
||||
val information = lines.getOrElse(11) { "" }
|
||||
if (information.length > 70) {
|
||||
return null
|
||||
}
|
||||
|
||||
return linkedMapOf(
|
||||
EpcQrElement.SERVICE_TAG to lines[0],
|
||||
EpcQrElement.VERSION to lines[1],
|
||||
EpcQrElement.CHARSET to (charsetMap[lines[2]] ?: lines[2]),
|
||||
EpcQrElement.IDENTIFICATION to lines[3],
|
||||
EpcQrElement.BIC to bic,
|
||||
EpcQrElement.NAME to recipient,
|
||||
EpcQrElement.IBAN to iban,
|
||||
EpcQrElement.AMOUNT to amount.removePrefix("EUR"),
|
||||
EpcQrElement.PURPOSE to purpose,
|
||||
EpcQrElement.REFERENCE to remittanceReference,
|
||||
EpcQrElement.TEXT to remittanceText,
|
||||
EpcQrElement.INFORMATION to information
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
package de.markusfisch.android.binaryeye.content
|
||||
|
||||
fun ByteArray.toHexString() = joinToString("") { "%02X".format(it) }
|
||||
@@ -0,0 +1,191 @@
|
||||
package de.markusfisch.android.binaryeye.content
|
||||
|
||||
import android.content.Context
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.tsenger.vdstools.DataEncoder
|
||||
import de.tsenger.vdstools.Verifier
|
||||
import de.tsenger.vdstools.idb.IdbSeal
|
||||
import org.bouncycastle.asn1.ASN1ObjectIdentifier
|
||||
import org.bouncycastle.asn1.icao.CscaMasterList
|
||||
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo
|
||||
import org.bouncycastle.asn1.x9.ECNamedCurveTable
|
||||
import org.bouncycastle.asn1.x9.X962Parameters
|
||||
import org.bouncycastle.cert.X509CertificateHolder
|
||||
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter
|
||||
import org.bouncycastle.cms.CMSSignedData
|
||||
import org.bouncycastle.jce.provider.BouncyCastleProvider
|
||||
import java.security.Provider
|
||||
import java.security.Security
|
||||
import java.security.cert.X509Certificate
|
||||
import java.util.Date
|
||||
|
||||
object IdbVerifier {
|
||||
private const val MASTER_LIST_ASSET = "DE_ML_2026-01-08-12-20-54.ml"
|
||||
|
||||
private val provider: Provider by lazy {
|
||||
BouncyCastleProvider().also { bcProvider ->
|
||||
if (Security.getProvider(bcProvider.name) == null) {
|
||||
Security.addProvider(bcProvider)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Volatile
|
||||
private var cachedMasterListCertificates: List<X509Certificate>? = null
|
||||
|
||||
fun verify(context: Context, seal: IdbSeal): List<SealField> {
|
||||
val signatureInfo = seal.signatureInfo
|
||||
?: return listOf(
|
||||
SealField(
|
||||
R.string.idb_verification,
|
||||
context.getString(R.string.idb_verification_unsigned)
|
||||
)
|
||||
)
|
||||
val headerCertificateReference = seal.signerCertReference
|
||||
?.hexToByteArrayOrNull()
|
||||
val signerCertificate = context.findCertificateByReference(
|
||||
headerCertificateReference
|
||||
)
|
||||
val curveName = signerCertificate?.let {
|
||||
curveNameFrom(it)
|
||||
}
|
||||
val signedBytes = seal.signedBytes
|
||||
val signatureResult = if (
|
||||
signerCertificate == null ||
|
||||
curveName.isNullOrBlank() ||
|
||||
signedBytes == null
|
||||
) {
|
||||
Verifier.Result.VerifyError
|
||||
} else {
|
||||
Verifier(
|
||||
signedBytes,
|
||||
signatureInfo.plainSignatureBytes,
|
||||
signerCertificate.publicKey.encoded,
|
||||
curveName
|
||||
).verify()
|
||||
}
|
||||
val referenceMatches = signerCertificate != null &&
|
||||
headerCertificateReference != null &&
|
||||
DataEncoder.buildCertificateReference(signerCertificate.encoded)
|
||||
.contentEquals(headerCertificateReference)
|
||||
val certificateValidNow = signerCertificate != null && runCatching {
|
||||
signerCertificate.checkValidity(Date())
|
||||
true
|
||||
}.getOrElse {
|
||||
false
|
||||
}
|
||||
val verification = context.getString(
|
||||
when {
|
||||
signerCertificate == null ->
|
||||
R.string.idb_verification_signer_certificate_issuer_not_found_in_master_list
|
||||
|
||||
!referenceMatches ->
|
||||
R.string.idb_verification_signature_invalid
|
||||
|
||||
!certificateValidNow ->
|
||||
R.string.idb_verification_signature_invalid
|
||||
|
||||
signatureResult == Verifier.Result.SignatureValid ->
|
||||
R.string.idb_verification_verified
|
||||
|
||||
signatureResult == Verifier.Result.SignatureInvalid ->
|
||||
R.string.idb_verification_signature_invalid
|
||||
|
||||
else -> R.string.idb_verification_error
|
||||
}
|
||||
)
|
||||
|
||||
val fields = mutableListOf(
|
||||
SealField(R.string.idb_verification, verification),
|
||||
SealField(
|
||||
R.string.idb_certificate_reference,
|
||||
signatureInfo.signerCertificateReference
|
||||
),
|
||||
SealField(
|
||||
R.string.vds_signing_date,
|
||||
signatureInfo.signingDate.toString()
|
||||
),
|
||||
SealField(
|
||||
R.string.idb_signature_verification,
|
||||
signatureResult.name
|
||||
),
|
||||
)
|
||||
if (signerCertificate != null) {
|
||||
fields.add(
|
||||
SealField(
|
||||
R.string.idb_signer_certificate_subject,
|
||||
signerCertificate.subjectX500Principal.name
|
||||
)
|
||||
)
|
||||
fields.add(
|
||||
SealField(
|
||||
R.string.idb_signer_certificate_issuer,
|
||||
signerCertificate.issuerX500Principal.name
|
||||
)
|
||||
)
|
||||
}
|
||||
return fields
|
||||
}
|
||||
|
||||
private fun String.hexToByteArrayOrNull(): ByteArray? {
|
||||
if (length % 2 != 0) {
|
||||
return null
|
||||
}
|
||||
return runCatching {
|
||||
ByteArray(length / 2) { index ->
|
||||
substring(index * 2, index * 2 + 2).toInt(16).toByte()
|
||||
}
|
||||
}.getOrNull()
|
||||
}
|
||||
|
||||
private fun Context.loadMasterListCertificates(): List<X509Certificate> {
|
||||
cachedMasterListCertificates?.let {
|
||||
return it
|
||||
}
|
||||
synchronized(this) {
|
||||
cachedMasterListCertificates?.let {
|
||||
return it
|
||||
}
|
||||
val cmsSignedData = assets.open(MASTER_LIST_ASSET).use { input ->
|
||||
CMSSignedData(input.readBytes())
|
||||
}
|
||||
val signedContent = cmsSignedData.signedContent?.content as? ByteArray
|
||||
?: throw IllegalArgumentException("Master list does not contain signed content")
|
||||
val masterList = CscaMasterList.getInstance(signedContent)
|
||||
val certificates = masterList.certStructs.map { cert ->
|
||||
JcaX509CertificateConverter()
|
||||
.setProvider(provider)
|
||||
.getCertificate(X509CertificateHolder(cert))
|
||||
}
|
||||
cachedMasterListCertificates = certificates
|
||||
return certificates
|
||||
}
|
||||
}
|
||||
|
||||
private fun Context.findCertificateByReference(
|
||||
certificateReference: ByteArray?
|
||||
): X509Certificate? {
|
||||
if (certificateReference == null || certificateReference.size != 5) {
|
||||
return null
|
||||
}
|
||||
return runCatching {
|
||||
loadMasterListCertificates().firstOrNull { certificate ->
|
||||
DataEncoder.buildCertificateReference(certificate.encoded)
|
||||
.contentEquals(certificateReference)
|
||||
}
|
||||
}.getOrNull()
|
||||
}
|
||||
|
||||
private fun curveNameFrom(certificate: X509Certificate): String? {
|
||||
return runCatching {
|
||||
val subjectPublicKeyInfo = SubjectPublicKeyInfo.getInstance(
|
||||
certificate.publicKey.encoded
|
||||
)
|
||||
val params = X962Parameters.getInstance(subjectPublicKeyInfo.algorithm.parameters)
|
||||
val oid = params.parameters as? ASN1ObjectIdentifier
|
||||
oid?.let {
|
||||
ECNamedCurveTable.getName(it)
|
||||
}
|
||||
}.getOrNull()
|
||||
}
|
||||
}
|
||||
@@ -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,138 @@
|
||||
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
|
||||
var skipSubtype = false
|
||||
|
||||
for (p in 0..len) {
|
||||
if (p < len && data[p] > '\u001f') {
|
||||
continue
|
||||
}
|
||||
|
||||
// Per spec, CR (0x0d) is the subfile boundary marker. The segment
|
||||
// immediately following a CR may begin with a 2-char subfile type
|
||||
// identifier (e.g. "ZV", "ZI") prepended to the first field.
|
||||
// Detect this by checking that the marker matches the key prefix
|
||||
// (jurisdiction fields always share their 2-char subfile prefix).
|
||||
val seg = if (skipSubtype && p - start >= 4 &&
|
||||
data[start] == data[start + 2] &&
|
||||
data[start + 1] == data[start + 3]
|
||||
) {
|
||||
start + 2
|
||||
} else {
|
||||
start
|
||||
}
|
||||
|
||||
if (p - seg > 3) {
|
||||
val vp = seg + 3
|
||||
val key = data.substring(seg, 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()
|
||||
}
|
||||
}
|
||||
skipSubtype = p < len && data[p] == '\r'
|
||||
start = p + 1
|
||||
}
|
||||
|
||||
return if (idlInfo.elements.isEmpty()) null else idlInfo
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
package de.markusfisch.android.binaryeye.content
|
||||
|
||||
import android.content.Context
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.tsenger.vdstools.asn1.DerTlv
|
||||
import de.tsenger.vdstools.generic.Message
|
||||
import de.tsenger.vdstools.generic.MessageCoding
|
||||
import de.tsenger.vdstools.generic.MessageValue
|
||||
import de.tsenger.vdstools.generic.Seal
|
||||
import de.tsenger.vdstools.idb.IdbSeal
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
import de.tsenger.vdstools.generic.SealParser as VdsSealParser
|
||||
|
||||
data class SealField(val name: Any, val value: String)
|
||||
private data class IdbChildMessageDefinition(
|
||||
val name: String,
|
||||
val coding: MessageCoding
|
||||
)
|
||||
|
||||
object SealParser {
|
||||
private val idbChildMessagesByName: Map<String, Map<Int, IdbChildMessageDefinition>> by lazy {
|
||||
loadIdbChildMessagesByName()
|
||||
}
|
||||
|
||||
fun parse(
|
||||
context: Context,
|
||||
bytes: ByteArray
|
||||
): List<SealField>? = try {
|
||||
context.buildFields(
|
||||
VdsSealParser().parse(bytes.toString(Charsets.ISO_8859_1))
|
||||
)
|
||||
} catch (_: Exception) {
|
||||
null
|
||||
}
|
||||
|
||||
private fun Context.buildFields(seal: Seal): List<SealField> = buildList {
|
||||
add(SealField(R.string.vds_document_type, seal.documentType))
|
||||
add(SealField(R.string.vds_issuing_country, seal.issuingCountry))
|
||||
if (seal is IdbSeal) {
|
||||
addAll(IdbVerifier.verify(this@buildFields, seal))
|
||||
}
|
||||
for (message in seal.messageList) {
|
||||
addAll(message.toFields())
|
||||
}
|
||||
}
|
||||
|
||||
private fun Message.toFields(): List<SealField> = buildList {
|
||||
add(
|
||||
if (value is MessageValue.BytesValue) {
|
||||
SealField(
|
||||
"$name (${value.rawBytes.size} bytes)",
|
||||
value.rawBytes.toHexString().uppercase()
|
||||
)
|
||||
} else {
|
||||
SealField(name, value.toString())
|
||||
}
|
||||
)
|
||||
for (child in childMessages()) {
|
||||
addAll(child.toFields())
|
||||
}
|
||||
}
|
||||
|
||||
private fun Message.childMessages(): List<Message> {
|
||||
val childDefinitionsByTag = idbChildMessagesByName[name] ?: return emptyList()
|
||||
return runCatching {
|
||||
DerTlv.parseAll(value.rawBytes).mapNotNull { derTlv ->
|
||||
val childTag = derTlv.tag.toInt() and 0xff
|
||||
val childDefinition = childDefinitionsByTag[childTag]
|
||||
?: return@mapNotNull null
|
||||
Message(
|
||||
childTag,
|
||||
childDefinition.name,
|
||||
childDefinition.coding,
|
||||
MessageValue.fromBytes(
|
||||
derTlv.value,
|
||||
childDefinition.coding
|
||||
)
|
||||
)
|
||||
}
|
||||
}.getOrElse {
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadIdbChildMessagesByName(): Map<String, Map<Int, IdbChildMessageDefinition>> {
|
||||
val resourceConstantsClass = runCatching {
|
||||
Class.forName("de.tsenger.vdstools.generated.ResourceConstants")
|
||||
}.getOrElse {
|
||||
return emptyMap()
|
||||
}
|
||||
val json = runCatching {
|
||||
resourceConstantsClass.getField(
|
||||
"IDB_MESSAGE_TYPES_JSON"
|
||||
).get(null) as String
|
||||
}.getOrElse {
|
||||
return emptyMap()
|
||||
}
|
||||
val childMessagesByName =
|
||||
mutableMapOf<String, Map<Int, IdbChildMessageDefinition>>()
|
||||
val messageTypes = JSONArray(json)
|
||||
for (i in 0 until messageTypes.length()) {
|
||||
val messageType = messageTypes.getJSONObject(i)
|
||||
val messages = messageType.optJSONArray("messages") ?: continue
|
||||
collectChildMessages(
|
||||
messageType.getString("name"),
|
||||
messages,
|
||||
childMessagesByName
|
||||
)
|
||||
}
|
||||
return childMessagesByName
|
||||
}
|
||||
|
||||
private fun collectChildMessages(
|
||||
parentName: String,
|
||||
messages: JSONArray,
|
||||
childMessagesByName: MutableMap<String, Map<Int, IdbChildMessageDefinition>>
|
||||
) {
|
||||
val childMessages = mutableMapOf<Int, IdbChildMessageDefinition>()
|
||||
for (i in 0 until messages.length()) {
|
||||
val message = messages.getJSONObject(i)
|
||||
val childDefinition = message.toChildMessageDefinition()
|
||||
childMessages[message.getInt("tag")] = childDefinition
|
||||
val nestedMessages = message.optJSONArray("messages") ?: continue
|
||||
collectChildMessages(
|
||||
childDefinition.name,
|
||||
nestedMessages,
|
||||
childMessagesByName
|
||||
)
|
||||
}
|
||||
if (childMessages.isNotEmpty()) {
|
||||
childMessagesByName[parentName] = childMessages
|
||||
}
|
||||
}
|
||||
|
||||
private fun JSONObject.toChildMessageDefinition(): IdbChildMessageDefinition {
|
||||
return IdbChildMessageDefinition(
|
||||
getString("name"),
|
||||
MessageCoding.valueOf(getString("coding"))
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,8 @@ import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.support.v4.content.FileProvider
|
||||
import androidx.core.content.FileProvider
|
||||
import androidx.core.net.toUri
|
||||
import de.markusfisch.android.binaryeye.BuildConfig
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.widget.toast
|
||||
@@ -33,12 +34,20 @@ fun Context.startIntent(intent: Intent): Boolean = try {
|
||||
// necessary.
|
||||
startActivity(intent)
|
||||
true
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
} catch (_: ActivityNotFoundException) {
|
||||
false
|
||||
}
|
||||
|
||||
fun Context.openUrl(url: String, silent: Boolean = false): Boolean {
|
||||
val intent = Intent(Intent.ACTION_VIEW, url.parseAndNormalizeUri())
|
||||
return openUri(url.parseAndNormalizeUri(), silent)
|
||||
}
|
||||
|
||||
fun Context.openUri(uri: Uri, silent: Boolean = false): Boolean {
|
||||
val intent = Intent(Intent.ACTION_VIEW, uri).apply {
|
||||
if (uri.scheme == "content") {
|
||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
}
|
||||
}
|
||||
return if (silent) {
|
||||
startIntent(intent)
|
||||
} else {
|
||||
@@ -46,12 +55,8 @@ fun Context.openUrl(url: String, silent: Boolean = false): Boolean {
|
||||
}
|
||||
}
|
||||
|
||||
fun String.parseAndNormalizeUri(): Uri = Uri.parse(this).let {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
|
||||
it.normalizeScheme()
|
||||
} else {
|
||||
it
|
||||
}
|
||||
fun String.parseAndNormalizeUri(): Uri = toUri().let {
|
||||
it.normalizeScheme()
|
||||
}
|
||||
|
||||
fun Context.shareText(text: String, mimeType: String = "text/plain") {
|
||||
@@ -91,7 +96,7 @@ fun <T> Context.shareAsFile(content: T, fileName: String) {
|
||||
FileOutputStream(file).use {
|
||||
it.write(bytes)
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
} catch (_: IOException) {
|
||||
toast(R.string.error_saving_file)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ package de.markusfisch.android.binaryeye.database
|
||||
|
||||
import android.content.Context
|
||||
import android.database.Cursor
|
||||
import de.markusfisch.android.binaryeye.content.toHexString
|
||||
import de.markusfisch.android.binaryeye.io.writeExternalFile
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.OutputStream
|
||||
@@ -30,10 +29,11 @@ private fun exportCsv(
|
||||
return
|
||||
}
|
||||
val columns = arrayOf(
|
||||
Database.SCANS_PINNED,
|
||||
Database.SCANS_DATETIME,
|
||||
Database.SCANS_FORMAT,
|
||||
Database.SCANS_NAME,
|
||||
Database.SCANS_CONTENT,
|
||||
Database.SCANS_TEXT,
|
||||
Database.SCANS_ERROR_CORRECTION_LEVEL,
|
||||
Database.SCANS_VERSION,
|
||||
Database.SCANS_SEQUENCE_SIZE,
|
||||
@@ -47,7 +47,7 @@ private fun exportCsv(
|
||||
val indices = columns.map {
|
||||
cursor.getColumnIndex(it)
|
||||
}
|
||||
val contentIndex = cursor.getColumnIndex(Database.SCANS_CONTENT)
|
||||
val contentIndex = cursor.getColumnIndex(Database.SCANS_TEXT)
|
||||
val rawIndex = cursor.getColumnIndex(Database.SCANS_RAW)
|
||||
outputStream.write(
|
||||
columns.joinToString(
|
||||
|
||||
@@ -1,15 +1,7 @@
|
||||
package de.markusfisch.android.binaryeye.database
|
||||
|
||||
import android.database.Cursor
|
||||
|
||||
// Overwrite Kotlin's ".use" function for Cursor because Cursor cannot
|
||||
// be cast to Closeable below API level 16. This should be removed when
|
||||
// the minSDK is increased to at least JELLY_BEAN (16).
|
||||
inline fun <R> Cursor.use(block: (Cursor) -> R): R = try {
|
||||
block.invoke(this)
|
||||
} finally {
|
||||
close()
|
||||
}
|
||||
import de.markusfisch.android.zxingcpp.ZxingCpp
|
||||
|
||||
fun Cursor.getInt(name: String): Int {
|
||||
val idx = getColumnIndex(name)
|
||||
@@ -30,3 +22,18 @@ fun Cursor.getBlob(name: String): ByteArray? {
|
||||
val idx = getColumnIndex(name)
|
||||
return if (idx < 0) null else getBlob(idx)
|
||||
}
|
||||
|
||||
fun Cursor.getBitMatrix(
|
||||
widthName: String,
|
||||
heightName: String,
|
||||
dataName: String
|
||||
): ZxingCpp.BitMatrix? {
|
||||
val width = getInt(widthName)
|
||||
val height = getInt(heightName)
|
||||
val data = getBlob(dataName)
|
||||
return if (width > 0 && height > 0 && data != null) {
|
||||
ZxingCpp.BitMatrix(width, height, data)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,8 @@ import android.database.sqlite.SQLiteDatabase
|
||||
import android.database.sqlite.SQLiteOpenHelper
|
||||
import de.markusfisch.android.binaryeye.app.prefs
|
||||
import de.markusfisch.android.binaryeye.preference.Preferences
|
||||
import de.markusfisch.android.binaryeye.zxingcpp.migrateBarcodeFormatName
|
||||
import de.markusfisch.android.binaryeye.zxingcpp.oldToNewFormatNames
|
||||
import de.markusfisch.android.zxingcpp.ZxingCpp.BarcodeFormat
|
||||
|
||||
class Database {
|
||||
@@ -16,25 +18,27 @@ 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_PINNED,
|
||||
$SCANS_DATETIME,
|
||||
$SCANS_NAME,
|
||||
$SCANS_CONTENT,
|
||||
$SCANS_TEXT,
|
||||
$SCANS_FORMAT
|
||||
FROM $SCANS
|
||||
${getWhereClause(query)}
|
||||
ORDER BY $SCANS_DATETIME DESC
|
||||
ORDER BY $SCANS_PINNED DESC, $SCANS_DATETIME DESC
|
||||
""".trimMargin(), getWhereArguments(query)
|
||||
)
|
||||
|
||||
fun getScansDetailed(query: String? = null): Cursor? = db.rawQuery(
|
||||
fun getScansDetailed(query: String? = null): Cursor = db.rawQuery(
|
||||
"""SELECT
|
||||
$SCANS_ID,
|
||||
$SCANS_PINNED,
|
||||
$SCANS_DATETIME,
|
||||
$SCANS_NAME,
|
||||
$SCANS_CONTENT,
|
||||
$SCANS_TEXT,
|
||||
$SCANS_RAW,
|
||||
$SCANS_FORMAT,
|
||||
$SCANS_ERROR_CORRECTION_LEVEL,
|
||||
@@ -48,7 +52,7 @@ class Database {
|
||||
$SCANS_GTIN_ISSUE_NUMBER
|
||||
FROM $SCANS
|
||||
${getWhereClause(query)}
|
||||
ORDER BY $SCANS_DATETIME DESC
|
||||
ORDER BY $SCANS_PINNED DESC, $SCANS_DATETIME DESC
|
||||
""".trimMargin(), getWhereArguments(query)
|
||||
)
|
||||
|
||||
@@ -56,8 +60,9 @@ class Database {
|
||||
query: String?,
|
||||
prefix: String = "WHERE"
|
||||
) = if (query?.isNotEmpty() == true) {
|
||||
"""$prefix $SCANS_CONTENT LIKE ?
|
||||
OR $SCANS_NAME LIKE ?"""
|
||||
"""$prefix $SCANS_TEXT LIKE ?
|
||||
OR $SCANS_NAME LIKE ?
|
||||
OR $SCANS_FORMAT LIKE ?"""
|
||||
} else {
|
||||
""
|
||||
}
|
||||
@@ -66,17 +71,18 @@ class Database {
|
||||
query: String?
|
||||
) = if (query?.isNotEmpty() == true) {
|
||||
val instr = "%$query%"
|
||||
arrayOf(instr, instr)
|
||||
arrayOf(instr, instr, instr)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
fun getScansDetailed(ids: LongArray): Cursor? = db.rawQuery(
|
||||
fun getScansDetailed(ids: LongArray): Cursor = db.rawQuery(
|
||||
"""SELECT
|
||||
$SCANS_ID,
|
||||
$SCANS_PINNED,
|
||||
$SCANS_DATETIME,
|
||||
$SCANS_NAME,
|
||||
$SCANS_CONTENT,
|
||||
$SCANS_TEXT,
|
||||
$SCANS_RAW,
|
||||
$SCANS_FORMAT,
|
||||
$SCANS_ERROR_CORRECTION_LEVEL,
|
||||
@@ -90,20 +96,25 @@ class Database {
|
||||
$SCANS_GTIN_ISSUE_NUMBER
|
||||
FROM $SCANS
|
||||
WHERE $SCANS_ID IN (${ids.joinToString(",")})
|
||||
ORDER BY $SCANS_DATETIME DESC
|
||||
ORDER BY $SCANS_PINNED DESC, $SCANS_DATETIME DESC
|
||||
""".trimMargin(), null
|
||||
)
|
||||
|
||||
fun getScan(id: Long): Scan? = db.rawQuery(
|
||||
"""SELECT
|
||||
$SCANS_ID,
|
||||
$SCANS_PINNED,
|
||||
$SCANS_DATETIME,
|
||||
$SCANS_NAME,
|
||||
$SCANS_CONTENT,
|
||||
$SCANS_TEXT,
|
||||
$SCANS_RAW,
|
||||
$SCANS_FORMAT,
|
||||
$SCANS_ERROR_CORRECTION_LEVEL,
|
||||
$SCANS_VERSION,
|
||||
$SCANS_DATA_MASK,
|
||||
$SCANS_SYMBOL_WIDTH,
|
||||
$SCANS_SYMBOL_HEIGHT,
|
||||
$SCANS_SYMBOL_DATA,
|
||||
$SCANS_SEQUENCE_SIZE,
|
||||
$SCANS_SEQUENCE_INDEX,
|
||||
$SCANS_SEQUENCE_ID,
|
||||
@@ -114,14 +125,22 @@ class Database {
|
||||
FROM $SCANS
|
||||
WHERE $SCANS_ID = ?
|
||||
""".trimMargin(), arrayOf("$id")
|
||||
)?.use {
|
||||
).use {
|
||||
if (it.moveToFirst()) {
|
||||
Scan(
|
||||
it.getString(SCANS_CONTENT),
|
||||
it.getString(SCANS_TEXT),
|
||||
it.getBlob(SCANS_RAW),
|
||||
BarcodeFormat.valueOf(it.getString(SCANS_FORMAT)),
|
||||
BarcodeFormat.valueOf(
|
||||
it.getString(SCANS_FORMAT).migrateBarcodeFormatName()
|
||||
),
|
||||
it.getString(SCANS_ERROR_CORRECTION_LEVEL),
|
||||
it.getString(SCANS_VERSION),
|
||||
it.getInt(SCANS_DATA_MASK),
|
||||
it.getBitMatrix(
|
||||
SCANS_SYMBOL_WIDTH,
|
||||
SCANS_SYMBOL_HEIGHT,
|
||||
SCANS_SYMBOL_DATA
|
||||
),
|
||||
it.getInt(SCANS_SEQUENCE_SIZE),
|
||||
it.getInt(SCANS_SEQUENCE_INDEX),
|
||||
it.getString(SCANS_SEQUENCE_ID),
|
||||
@@ -132,6 +151,7 @@ class Database {
|
||||
it.getString(SCANS_DATETIME),
|
||||
it.getLong(SCANS_ID),
|
||||
it.getString(SCANS_NAME),
|
||||
it.getInt(SCANS_PINNED) != 0
|
||||
)
|
||||
} else {
|
||||
null
|
||||
@@ -142,7 +162,7 @@ class Database {
|
||||
when (prefs.ignoreDuplicates()) {
|
||||
Preferences.Companion.IgnoreDuplicates.Consecutive -> {
|
||||
val id = getIdOfLastScan(
|
||||
scan.content,
|
||||
scan.text,
|
||||
scan.raw,
|
||||
scan.format
|
||||
)
|
||||
@@ -152,7 +172,7 @@ class Database {
|
||||
}
|
||||
|
||||
Preferences.Companion.IgnoreDuplicates.Any -> {
|
||||
val id = getIdOfScanByContent(scan.content, scan.format)
|
||||
val id = getIdOfScanByContent(scan.text, scan.format)
|
||||
if (id > 0L) {
|
||||
return id
|
||||
}
|
||||
@@ -165,7 +185,7 @@ class Database {
|
||||
null,
|
||||
ContentValues().apply {
|
||||
put(SCANS_DATETIME, scan.dateTime)
|
||||
put(SCANS_CONTENT, scan.content)
|
||||
put(SCANS_TEXT, scan.text)
|
||||
if (scan.raw != null) {
|
||||
put(SCANS_RAW, scan.raw)
|
||||
}
|
||||
@@ -174,6 +194,12 @@ class Database {
|
||||
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)
|
||||
@@ -181,6 +207,7 @@ class Database {
|
||||
scan.addOn?.let { put(SCANS_GTIN_ADD_ON, it) }
|
||||
scan.price?.let { put(SCANS_GTIN_PRICE, it) }
|
||||
scan.issueNumber?.let { put(SCANS_GTIN_ISSUE_NUMBER, it) }
|
||||
put(SCANS_PINNED, if (scan.pinned) 1 else 0)
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -192,17 +219,17 @@ class Database {
|
||||
): Long = db.rawQuery(
|
||||
"""SELECT
|
||||
$SCANS_ID,
|
||||
$SCANS_CONTENT,
|
||||
$SCANS_TEXT,
|
||||
$SCANS_RAW,
|
||||
$SCANS_FORMAT
|
||||
FROM $SCANS
|
||||
ORDER BY $SCANS_ID DESC
|
||||
LIMIT 1
|
||||
""".trimMargin(), null
|
||||
)?.use {
|
||||
).use {
|
||||
if (it.count > 0 &&
|
||||
it.moveToFirst() &&
|
||||
it.getString(SCANS_CONTENT) == content &&
|
||||
it.getString(SCANS_TEXT) == content &&
|
||||
(raw == null || it.getBlob(SCANS_RAW)
|
||||
?.contentEquals(raw) == true) &&
|
||||
it.getString(SCANS_FORMAT) == format.name
|
||||
@@ -211,26 +238,26 @@ class Database {
|
||||
} else {
|
||||
0L
|
||||
}
|
||||
} ?: 0L
|
||||
}
|
||||
|
||||
fun getIdOfScanByContent(
|
||||
private fun getIdOfScanByContent(
|
||||
content: String,
|
||||
format: BarcodeFormat
|
||||
): Long = db.rawQuery(
|
||||
"""SELECT
|
||||
$SCANS_ID
|
||||
FROM $SCANS
|
||||
WHERE $SCANS_CONTENT = ?
|
||||
WHERE $SCANS_TEXT = ?
|
||||
AND $SCANS_FORMAT = ?
|
||||
LIMIT 1
|
||||
""".trimMargin(), arrayOf(content, format.name)
|
||||
)?.use {
|
||||
).use {
|
||||
if (it.moveToFirst()) {
|
||||
it.getLong(SCANS_ID)
|
||||
} else {
|
||||
0L
|
||||
}
|
||||
} ?: 0L
|
||||
}
|
||||
|
||||
fun removeScan(id: Long) {
|
||||
db.delete(SCANS, "$SCANS_ID = ?", arrayOf("$id"))
|
||||
@@ -246,8 +273,14 @@ class Database {
|
||||
db.update(SCANS, cv, "$SCANS_ID = ?", arrayOf("$id"))
|
||||
}
|
||||
|
||||
fun setPinned(id: Long, pinned: Boolean) {
|
||||
val cv = ContentValues()
|
||||
cv.put(SCANS_PINNED, if (pinned) 1 else 0)
|
||||
db.update(SCANS, cv, "$SCANS_ID = ?", arrayOf("$id"))
|
||||
}
|
||||
|
||||
private class OpenHelper(context: Context) :
|
||||
SQLiteOpenHelper(context, FILE_NAME, null, 6) {
|
||||
SQLiteOpenHelper(context, FILE_NAME, null, 10) {
|
||||
override fun onCreate(db: SQLiteDatabase) {
|
||||
db.createScans()
|
||||
}
|
||||
@@ -272,6 +305,18 @@ class Database {
|
||||
if (oldVersion < 6) {
|
||||
db.migrateToVersionString()
|
||||
}
|
||||
if (oldVersion < 7) {
|
||||
db.addDataMaskColumn()
|
||||
}
|
||||
if (oldVersion < 8) {
|
||||
db.addSymbolColumns()
|
||||
}
|
||||
if (oldVersion < 9) {
|
||||
db.migrateToNativeFormatNames()
|
||||
}
|
||||
if (oldVersion < 10) {
|
||||
db.addPinnedColumn()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -281,12 +326,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"
|
||||
@@ -301,6 +350,7 @@ class Database {
|
||||
const val SCANS_GTIN_ADD_ON = "gtin_add_on"
|
||||
const val SCANS_GTIN_PRICE = "gtin_price"
|
||||
const val SCANS_GTIN_ISSUE_NUMBER = "gtin_issue_number"
|
||||
const val SCANS_PINNED = "pinned"
|
||||
|
||||
private fun SQLiteDatabase.createScans() {
|
||||
execSQL("DROP TABLE IF EXISTS $SCANS".trimMargin())
|
||||
@@ -309,18 +359,23 @@ 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,
|
||||
$SCANS_GTIN_COUNTRY TEXT,
|
||||
$SCANS_GTIN_ADD_ON TEXT,
|
||||
$SCANS_GTIN_PRICE TEXT,
|
||||
$SCANS_GTIN_ISSUE_NUMBER TEXT
|
||||
$SCANS_GTIN_ISSUE_NUMBER TEXT,
|
||||
$SCANS_PINNED INTEGER NOT NULL DEFAULT 0
|
||||
)""".trimMargin()
|
||||
)
|
||||
}
|
||||
@@ -341,7 +396,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() {
|
||||
@@ -361,9 +416,35 @@ class Database {
|
||||
execSQL("UPDATE $SCANS SET $SCANS_GTIN_ISSUE_NUMBER = $SCANS_ISSUE_NUMBER")
|
||||
}
|
||||
|
||||
private fun SQLiteDatabase.migrateToNativeFormatNames() {
|
||||
for ((oldName, newName) in oldToNewFormatNames) {
|
||||
execSQL(
|
||||
"UPDATE $SCANS SET $SCANS_FORMAT = ? WHERE $SCANS_FORMAT = ?",
|
||||
arrayOf(newName, oldName)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun SQLiteDatabase.migrateToVersionString() {
|
||||
execSQL("ALTER TABLE $SCANS ADD $SCANS_VERSION TEXT")
|
||||
execSQL("UPDATE $SCANS SET $SCANS_VERSION = $SCANS_VERSION_NUMBER")
|
||||
}
|
||||
|
||||
private fun SQLiteDatabase.addDataMaskColumn() {
|
||||
execSQL("ALTER TABLE $SCANS ADD $SCANS_DATA_MASK INTEGER")
|
||||
execSQL("UPDATE $SCANS SET $SCANS_DATA_MASK = -1")
|
||||
}
|
||||
|
||||
private fun SQLiteDatabase.addSymbolColumns() {
|
||||
execSQL("ALTER TABLE $SCANS ADD $SCANS_SYMBOL_WIDTH INTEGER")
|
||||
execSQL("ALTER TABLE $SCANS ADD $SCANS_SYMBOL_HEIGHT INTEGER")
|
||||
execSQL("ALTER TABLE $SCANS ADD $SCANS_SYMBOL_DATA BLOB")
|
||||
}
|
||||
|
||||
private fun SQLiteDatabase.addPinnedColumn() {
|
||||
execSQL(
|
||||
"ALTER TABLE $SCANS ADD COLUMN $SCANS_PINNED INTEGER NOT NULL DEFAULT 0"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ package de.markusfisch.android.binaryeye.database
|
||||
|
||||
import android.content.Context
|
||||
import android.database.Cursor
|
||||
import de.markusfisch.android.binaryeye.content.toHexString
|
||||
import de.markusfisch.android.binaryeye.io.writeExternalFile
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONException
|
||||
@@ -22,10 +21,11 @@ fun Cursor.exportJson(): String? {
|
||||
return null
|
||||
}
|
||||
val columns = arrayOf(
|
||||
Database.SCANS_PINNED,
|
||||
Database.SCANS_DATETIME,
|
||||
Database.SCANS_FORMAT,
|
||||
Database.SCANS_NAME,
|
||||
Database.SCANS_CONTENT,
|
||||
Database.SCANS_TEXT,
|
||||
Database.SCANS_ERROR_CORRECTION_LEVEL,
|
||||
Database.SCANS_VERSION,
|
||||
Database.SCANS_SEQUENCE_SIZE,
|
||||
@@ -39,7 +39,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 {
|
||||
@@ -70,7 +70,7 @@ private fun Cursor.toJsonObject(
|
||||
obj.put(it.second, value ?: "")
|
||||
}
|
||||
obj
|
||||
} catch (e: JSONException) {
|
||||
} catch (_: JSONException) {
|
||||
obj
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,21 +1,22 @@
|
||||
package de.markusfisch.android.binaryeye.database
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.os.Build
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
import android.os.Bundle
|
||||
import android.text.format.DateFormat
|
||||
import de.markusfisch.android.zxingcpp.ZxingCpp
|
||||
import de.markusfisch.android.binaryeye.zxingcpp.migrateBarcodeFormatName
|
||||
import de.markusfisch.android.zxingcpp.ZxingCpp.BarcodeFormat
|
||||
import de.markusfisch.android.zxingcpp.ZxingCpp.BitMatrix
|
||||
import de.markusfisch.android.zxingcpp.ZxingCpp.ContentType
|
||||
import de.markusfisch.android.zxingcpp.ZxingCpp.Result
|
||||
import java.util.Locale
|
||||
|
||||
data class Scan(
|
||||
val content: String,
|
||||
val text: String,
|
||||
val raw: ByteArray?,
|
||||
val format: BarcodeFormat,
|
||||
val errorCorrectionLevel: String? = null,
|
||||
val version: String? = null,
|
||||
val dataMask: Int = -1,
|
||||
val symbol: BitMatrix? = null,
|
||||
val sequenceSize: Int = -1,
|
||||
val sequenceIndex: Int = -1,
|
||||
val sequenceId: String = "",
|
||||
@@ -25,8 +26,9 @@ data class Scan(
|
||||
val issueNumber: String? = null,
|
||||
val dateTime: String = getDateTime(),
|
||||
var id: Long = 0L,
|
||||
val label: String? = null
|
||||
) : Parcelable {
|
||||
val label: String? = null,
|
||||
val pinned: Boolean = false
|
||||
) {
|
||||
// Needs to be overwritten manually, as ByteArray is an array and
|
||||
// this isn't handled well by Kotlin.
|
||||
override fun equals(other: Any?): Boolean {
|
||||
@@ -41,19 +43,23 @@ 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 &&
|
||||
country == other.country &&
|
||||
addOn == other.addOn &&
|
||||
price == other.price &&
|
||||
issueNumber == other.issueNumber
|
||||
issueNumber == other.issueNumber &&
|
||||
label == other.label &&
|
||||
pinned == other.pinned
|
||||
}
|
||||
|
||||
// Needs to be overwritten manually, as ByteArray is an array and
|
||||
@@ -61,11 +67,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()
|
||||
@@ -73,56 +80,11 @@ data class Scan(
|
||||
result = 31 * result + (addOn?.hashCode() ?: 0)
|
||||
result = 31 * result + (price?.hashCode() ?: 0)
|
||||
result = 31 * result + (issueNumber?.hashCode() ?: 0)
|
||||
result = 31 * result + (label?.hashCode() ?: 0)
|
||||
result = 31 * result + pinned.hashCode()
|
||||
return result
|
||||
}
|
||||
|
||||
private constructor(parcel: Parcel) : this(
|
||||
content = parcel.readString() ?: "",
|
||||
raw = parcel.readSizedByteArray(),
|
||||
format = BarcodeFormat.valueOf(parcel.readString() ?: ""),
|
||||
errorCorrectionLevel = parcel.readString(),
|
||||
version = parcel.readString(),
|
||||
sequenceSize = parcel.readInt(),
|
||||
sequenceIndex = parcel.readInt(),
|
||||
sequenceId = parcel.readString() ?: "",
|
||||
country = parcel.readString(),
|
||||
addOn = parcel.readString(),
|
||||
price = parcel.readString(),
|
||||
issueNumber = parcel.readString(),
|
||||
dateTime = parcel.readString() ?: "",
|
||||
id = parcel.readLong(),
|
||||
label = parcel.readString()
|
||||
)
|
||||
|
||||
override fun writeToParcel(parcel: Parcel, flags: Int) {
|
||||
parcel.apply {
|
||||
writeString(content)
|
||||
writeSizedByteArray(raw)
|
||||
writeString(format.name)
|
||||
writeString(errorCorrectionLevel)
|
||||
writeString(version)
|
||||
writeInt(sequenceSize)
|
||||
writeInt(sequenceIndex)
|
||||
writeString(sequenceId)
|
||||
writeString(country)
|
||||
writeString(addOn)
|
||||
writeString(price)
|
||||
writeString(issueNumber)
|
||||
writeString(dateTime)
|
||||
writeLong(id)
|
||||
writeString(label)
|
||||
}
|
||||
}
|
||||
|
||||
override fun describeContents() = 0
|
||||
|
||||
companion object {
|
||||
@JvmField
|
||||
val CREATOR = object : Parcelable.Creator<Scan> {
|
||||
override fun createFromParcel(parcel: Parcel) = Scan(parcel)
|
||||
override fun newArray(size: Int) = arrayOfNulls<Scan>(size)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun Result.toScan(): Scan {
|
||||
@@ -132,12 +94,14 @@ fun Result.toScan(): Scan {
|
||||
ContentType.ISO15434,
|
||||
ContentType.TEXT -> text
|
||||
|
||||
else -> ""
|
||||
else -> "" // Show as binary.
|
||||
},
|
||||
rawBytes,
|
||||
format,
|
||||
ecLevel,
|
||||
version,
|
||||
dataMask,
|
||||
symbol,
|
||||
sequenceSize,
|
||||
sequenceIndex,
|
||||
sequenceId,
|
||||
@@ -148,65 +112,98 @@ fun Result.toScan(): Scan {
|
||||
)
|
||||
}
|
||||
|
||||
data class Recreation(
|
||||
val format: BarcodeFormat,
|
||||
val ecLevel: Int,
|
||||
val size: Int,
|
||||
val margin: Int
|
||||
) {
|
||||
fun <T> encode(content: T): Bitmap? = try {
|
||||
ZxingCpp.encodeAsBitmap(
|
||||
content, format, size, size, margin, ecLevel
|
||||
fun Scan.toBundle() = Bundle().apply {
|
||||
putString(SCAN_TEXT, text)
|
||||
putByteArray(SCAN_RAW, raw)
|
||||
putString(SCAN_FORMAT, format.name)
|
||||
putString(SCAN_ERROR_CORRECTION_LEVEL, errorCorrectionLevel)
|
||||
putString(SCAN_VERSION, version)
|
||||
putInt(SCAN_DATA_MASK, dataMask)
|
||||
putInt(SCAN_SYMBOL_WIDTH, symbol?.width ?: 0)
|
||||
putInt(SCAN_SYMBOL_HEIGHT, symbol?.height ?: 0)
|
||||
putByteArray(SCAN_SYMBOL_DATA, symbol?.data)
|
||||
putInt(SCAN_SEQUENCE_SIZE, sequenceSize)
|
||||
putInt(SCAN_SEQUENCE_INDEX, sequenceIndex)
|
||||
putString(SCAN_SEQUENCE_ID, sequenceId)
|
||||
putString(SCAN_COUNTRY, country)
|
||||
putString(SCAN_ADD_ON, addOn)
|
||||
putString(SCAN_PRICE, price)
|
||||
putString(SCAN_ISSUE_NUMBER, issueNumber)
|
||||
putString(SCAN_DATE_TIME, dateTime)
|
||||
putLong(SCAN_ID, id)
|
||||
putString(SCAN_LABEL, label)
|
||||
putBoolean(SCAN_PINNED, pinned)
|
||||
}
|
||||
|
||||
fun Bundle.toScan(): Scan? {
|
||||
val formatName = getString(SCAN_FORMAT)?.migrateBarcodeFormatName()
|
||||
if (formatName.isNullOrEmpty()) {
|
||||
return null
|
||||
}
|
||||
return try {
|
||||
Scan(
|
||||
text = getString(SCAN_TEXT) ?: "",
|
||||
raw = getByteArray(SCAN_RAW),
|
||||
format = BarcodeFormat.valueOf(formatName),
|
||||
errorCorrectionLevel = getString(SCAN_ERROR_CORRECTION_LEVEL),
|
||||
version = getString(SCAN_VERSION),
|
||||
dataMask = getInt(SCAN_DATA_MASK, -1),
|
||||
symbol = getBitMatrix(),
|
||||
sequenceSize = getInt(SCAN_SEQUENCE_SIZE, -1),
|
||||
sequenceIndex = getInt(SCAN_SEQUENCE_INDEX, -1),
|
||||
sequenceId = getString(SCAN_SEQUENCE_ID) ?: "",
|
||||
country = getString(SCAN_COUNTRY),
|
||||
addOn = getString(SCAN_ADD_ON),
|
||||
price = getString(SCAN_PRICE),
|
||||
issueNumber = getString(SCAN_ISSUE_NUMBER),
|
||||
dateTime = getString(SCAN_DATE_TIME) ?: getDateTime(),
|
||||
id = getLong(SCAN_ID, 0L),
|
||||
label = getString(SCAN_LABEL),
|
||||
pinned = getBoolean(SCAN_PINNED, false)
|
||||
)
|
||||
} catch (_: RuntimeException) {
|
||||
} catch (_: IllegalArgumentException) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun Scan.toRecreation(
|
||||
size: Int = 128,
|
||||
margin: Int = -1
|
||||
) = Recreation(
|
||||
format,
|
||||
when (errorCorrectionLevel) {
|
||||
"L" -> 0
|
||||
"M" -> 4
|
||||
"Q" -> 6
|
||||
"H" -> 8
|
||||
else -> -1
|
||||
},
|
||||
size,
|
||||
margin
|
||||
)
|
||||
|
||||
private fun getDateTime(
|
||||
time: Long = System.currentTimeMillis()
|
||||
) = DateFormat.format(
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2) {
|
||||
// Up to API level 17, 'k' was interpreted as 'H', which wasn't
|
||||
// implemented until API level 18.
|
||||
"yyyy-MM-dd kk:mm:ss"
|
||||
} else {
|
||||
"yyyy-MM-dd HH:mm:ss"
|
||||
},
|
||||
"yyyy-MM-dd HH:mm:ss",
|
||||
time
|
||||
).toString()
|
||||
).toString() + String.format(
|
||||
Locale.getDefault(),
|
||||
":%03d",
|
||||
time % 1000
|
||||
)
|
||||
|
||||
private fun Parcel.writeSizedByteArray(array: ByteArray?) {
|
||||
val size = array?.size ?: 0
|
||||
writeInt(size)
|
||||
if (size > 0) {
|
||||
writeByteArray(array)
|
||||
private fun Bundle.getBitMatrix(): BitMatrix? {
|
||||
val width = getInt(SCAN_SYMBOL_WIDTH, 0)
|
||||
val height = getInt(SCAN_SYMBOL_HEIGHT, 0)
|
||||
val data = getByteArray(SCAN_SYMBOL_DATA)
|
||||
if (width < 1 || height < 1 || data == null) {
|
||||
return null
|
||||
}
|
||||
return BitMatrix(width, height, data)
|
||||
}
|
||||
|
||||
private fun Parcel.readSizedByteArray(): ByteArray? {
|
||||
val size = readInt()
|
||||
return if (size > 0) {
|
||||
val array = ByteArray(size)
|
||||
readByteArray(array)
|
||||
array
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
private const val SCAN_TEXT = "text"
|
||||
private const val SCAN_RAW = "raw"
|
||||
private const val SCAN_FORMAT = "format"
|
||||
private const val SCAN_ERROR_CORRECTION_LEVEL = "error_correction_level"
|
||||
private const val SCAN_VERSION = "version"
|
||||
private const val SCAN_DATA_MASK = "data_mask"
|
||||
private const val SCAN_SYMBOL_WIDTH = "symbol_width"
|
||||
private const val SCAN_SYMBOL_HEIGHT = "symbol_height"
|
||||
private const val SCAN_SYMBOL_DATA = "symbol_data"
|
||||
private const val SCAN_SEQUENCE_SIZE = "sequence_size"
|
||||
private const val SCAN_SEQUENCE_INDEX = "sequence_index"
|
||||
private const val SCAN_SEQUENCE_ID = "sequence_id"
|
||||
private const val SCAN_COUNTRY = "country"
|
||||
private const val SCAN_ADD_ON = "add_on"
|
||||
private const val SCAN_PRICE = "price"
|
||||
private const val SCAN_ISSUE_NUMBER = "issue_number"
|
||||
private const val SCAN_DATE_TIME = "date_time"
|
||||
private const val SCAN_ID = "id"
|
||||
private const val SCAN_LABEL = "label"
|
||||
private const val SCAN_PINNED = "pinned"
|
||||
|
||||
@@ -1,441 +0,0 @@
|
||||
package de.markusfisch.android.binaryeye.fragment
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.AlertDialog
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.support.v4.app.Fragment
|
||||
import android.view.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 de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.app.db
|
||||
import de.markusfisch.android.binaryeye.app.hasWritePermission
|
||||
import de.markusfisch.android.binaryeye.content.copyToClipboard
|
||||
import de.markusfisch.android.binaryeye.content.shareFile
|
||||
import de.markusfisch.android.binaryeye.content.shareText
|
||||
import de.markusfisch.android.binaryeye.database.toScan
|
||||
import de.markusfisch.android.binaryeye.graphics.COLOR_BLACK
|
||||
import de.markusfisch.android.binaryeye.graphics.COLOR_WHITE
|
||||
import de.markusfisch.android.binaryeye.io.addSuffixIfNotGiven
|
||||
import de.markusfisch.android.binaryeye.io.toSaveResult
|
||||
import de.markusfisch.android.binaryeye.io.writeExternalFile
|
||||
import de.markusfisch.android.binaryeye.view.doOnApplyWindowInsets
|
||||
import de.markusfisch.android.binaryeye.view.setPaddingFromWindowInsets
|
||||
import de.markusfisch.android.binaryeye.widget.ConfinedScalingImageView
|
||||
import de.markusfisch.android.binaryeye.widget.toast
|
||||
import de.markusfisch.android.zxingcpp.ZxingCpp
|
||||
import de.markusfisch.android.zxingcpp.ZxingCpp.BarcodeFormat
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
import java.io.OutputStream
|
||||
import java.util.Locale
|
||||
import kotlin.math.min
|
||||
|
||||
class BarcodeFragment : Fragment() {
|
||||
private enum class FileType {
|
||||
PNG, JPG, SVG, TXT
|
||||
}
|
||||
|
||||
private val parentJob = Job()
|
||||
private val scope = CoroutineScope(Dispatchers.IO + parentJob)
|
||||
|
||||
private lateinit var barcode: Barcode<*>
|
||||
private lateinit var addToHistoryItem: MenuItem
|
||||
|
||||
override fun onCreate(state: Bundle?) {
|
||||
super.onCreate(state)
|
||||
setHasOptionsMenu(true)
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
state: Bundle?
|
||||
): View? {
|
||||
val ac = activity ?: return null
|
||||
ac.setTitle(R.string.view_barcode)
|
||||
|
||||
val view = inflater.inflate(
|
||||
R.layout.fragment_barcode,
|
||||
container,
|
||||
false
|
||||
)
|
||||
|
||||
val bitmap: Bitmap
|
||||
try {
|
||||
barcode = arguments?.toBarcode() ?: throw IllegalArgumentException(
|
||||
"Illegal arguments"
|
||||
)
|
||||
// Catch exceptions from encoding.
|
||||
bitmap = barcode.bitmap()
|
||||
} catch (e: Exception) {
|
||||
var message = e.message
|
||||
if (message.isNullOrEmpty()) {
|
||||
message = getString(R.string.error_encoding_barcode)
|
||||
}
|
||||
message?.let {
|
||||
ac.toast(message)
|
||||
}
|
||||
fragmentManager.popBackStack()
|
||||
return null
|
||||
}
|
||||
|
||||
val imageView = view.findViewById<ConfinedScalingImageView>(
|
||||
R.id.barcode
|
||||
)
|
||||
imageView.setImageBitmap(bitmap)
|
||||
imageView.post {
|
||||
// Make sure to invoke this after ScalingImageView.onLayout().
|
||||
imageView.minWidth = min(
|
||||
imageView.minWidth / 2f,
|
||||
barcode.size.toFloat()
|
||||
)
|
||||
}
|
||||
|
||||
view.findViewById<View>(R.id.share).setOnClickListener {
|
||||
it.context?.apply {
|
||||
pickFileType(R.string.share_as) { fileType ->
|
||||
shareAs(fileType)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
(view.findViewById(R.id.inset_layout) as View).setPaddingFromWindowInsets()
|
||||
imageView.doOnApplyWindowInsets { v, insets ->
|
||||
(v as ConfinedScalingImageView).insets.set(insets)
|
||||
}
|
||||
|
||||
return view
|
||||
}
|
||||
|
||||
private fun Bundle.toBarcode() = Barcode(
|
||||
getString(CONTENT_TEXT) ?: getByteArray(CONTENT_RAW) ?: throw IllegalArgumentException(
|
||||
"content cannot be null"
|
||||
),
|
||||
BarcodeFormat.valueOf(
|
||||
getString(FORMAT) ?: throw IllegalArgumentException(
|
||||
"format cannot be null"
|
||||
)
|
||||
),
|
||||
getInt(SIZE),
|
||||
getInt(MARGIN),
|
||||
getInt(EC_LEVEL),
|
||||
Colors.entries[getInt(COLORS)]
|
||||
)
|
||||
|
||||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
parentJob.cancel()
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
inflater.inflate(R.menu.fragment_barcode, menu)
|
||||
addToHistoryItem = menu.findItem(R.id.add_to_history)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
R.id.export_to_file -> {
|
||||
context.pickFileType(R.string.export_as) {
|
||||
askForFileNameAndSave(it)
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
}
|
||||
|
||||
private fun Context.pickFileType(
|
||||
title: Int,
|
||||
action: (FileType) -> Unit
|
||||
) {
|
||||
val fileTypes = FileType.entries.toTypedArray()
|
||||
AlertDialog.Builder(this)
|
||||
.setTitle(title)
|
||||
.setItems(fileTypes.map { it.name }.toTypedArray()) { _, which ->
|
||||
action(fileTypes[which])
|
||||
}
|
||||
.show()
|
||||
}
|
||||
|
||||
// Dialogs do not have a parent view.
|
||||
@SuppressLint("InflateParams")
|
||||
private fun askForFileNameAndSave(fileType: FileType) {
|
||||
val ac = activity ?: return
|
||||
// Write permission is only required before Android Q.
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q &&
|
||||
!ac.hasWritePermission { askForFileNameAndSave(fileType) }
|
||||
) {
|
||||
return
|
||||
}
|
||||
val view = ac.layoutInflater.inflate(R.layout.dialog_save_file, null)
|
||||
val editText = view.findViewById<EditText>(R.id.file_name)
|
||||
editText.setText(
|
||||
encodeFileName("${barcode.format}_${barcode.content}")
|
||||
)
|
||||
AlertDialog.Builder(ac)
|
||||
.setView(view)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
val fileName = editText.text.toString()
|
||||
when (fileType) {
|
||||
FileType.PNG -> saveAs(
|
||||
addSuffixIfNotGiven(fileName, ".png"),
|
||||
MIME_PNG
|
||||
) {
|
||||
barcode.bitmap().saveAsPng(it)
|
||||
}
|
||||
|
||||
FileType.JPG -> saveAs(
|
||||
addSuffixIfNotGiven(fileName, ".jpg"),
|
||||
MIME_JPG
|
||||
) {
|
||||
barcode.bitmap().saveAsJpg(it)
|
||||
}
|
||||
|
||||
FileType.SVG -> saveAs(
|
||||
addSuffixIfNotGiven(fileName, ".svg"),
|
||||
MIME_SVG
|
||||
) { outputStream ->
|
||||
outputStream.write(barcode.svg().toByteArray())
|
||||
}
|
||||
|
||||
FileType.TXT -> saveAs(
|
||||
addSuffixIfNotGiven(fileName, ".txt"),
|
||||
MIME_TXT
|
||||
) { outputStream ->
|
||||
outputStream.write(barcode.text().toByteArray())
|
||||
}
|
||||
}
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||
}
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun saveAs(
|
||||
fileName: String,
|
||||
mimeType: String,
|
||||
write: (outputStream: OutputStream) -> Unit
|
||||
) {
|
||||
val ac = activity ?: return
|
||||
scope.launch(Dispatchers.IO) {
|
||||
val message = ac.writeExternalFile(fileName, mimeType, write).toSaveResult()
|
||||
withContext(Dispatchers.Main) {
|
||||
ac.toast(message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun Context.shareAs(fileType: FileType) {
|
||||
when (fileType) {
|
||||
FileType.PNG -> share(barcode.bitmap(), MIME_PNG, "png")
|
||||
FileType.JPG -> share(barcode.bitmap(), MIME_JPG, "jpg")
|
||||
FileType.SVG -> shareText(barcode.svg(), MIME_SVG)
|
||||
FileType.TXT -> shareText(barcode.text())
|
||||
}
|
||||
}
|
||||
|
||||
private fun Context.share(bitmap: Bitmap, mimeType: String, ext: String) {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
val file = File(
|
||||
externalCacheDir,
|
||||
"shared_barcode.$ext"
|
||||
)
|
||||
val success = try {
|
||||
FileOutputStream(file).use {
|
||||
when (mimeType) {
|
||||
MIME_PNG -> bitmap.saveAsPng(it)
|
||||
MIME_JPG -> bitmap.saveAsJpg(it)
|
||||
else -> throw IllegalArgumentException(
|
||||
"Invalid mime type"
|
||||
)
|
||||
}
|
||||
}
|
||||
true
|
||||
} catch (e: IOException) {
|
||||
false
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
if (success) {
|
||||
shareFile(file, mimeType)
|
||||
} else {
|
||||
toast(R.string.error_saving_file)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val CONTENT_TEXT = "content_text"
|
||||
private const val CONTENT_RAW = "content_raw"
|
||||
private const val FORMAT = "format"
|
||||
private const val SIZE = "size"
|
||||
private const val MARGIN = "margin"
|
||||
private const val EC_LEVEL = "ec_level"
|
||||
private const val COLORS = "colors"
|
||||
private const val MIME_PNG = "image/png"
|
||||
private const val MIME_JPG = "image/jpeg"
|
||||
private const val MIME_SVG = "image/svg+xmg"
|
||||
private const val MIME_TXT = "text/plain"
|
||||
|
||||
fun <T> newInstance(
|
||||
content: T,
|
||||
format: BarcodeFormat,
|
||||
size: Int,
|
||||
margin: Int,
|
||||
ecLevel: Int = -1,
|
||||
colors: Int = 0
|
||||
): Fragment {
|
||||
val args = Bundle()
|
||||
when (content) {
|
||||
is String -> {
|
||||
args.putString(CONTENT_TEXT, content)
|
||||
}
|
||||
|
||||
is ByteArray -> {
|
||||
args.putByteArray(CONTENT_RAW, content)
|
||||
}
|
||||
|
||||
else -> {
|
||||
throw IllegalArgumentException(
|
||||
"content must be a String of a ByteArray"
|
||||
)
|
||||
}
|
||||
}
|
||||
args.putString(FORMAT, format.name)
|
||||
args.putInt(SIZE, size)
|
||||
args.putInt(MARGIN, margin)
|
||||
args.putInt(EC_LEVEL, ecLevel)
|
||||
args.putInt(COLORS, colors)
|
||||
val fragment = BarcodeFragment()
|
||||
fragment.arguments = args
|
||||
return fragment
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private data class Barcode<T>(
|
||||
val content: T,
|
||||
val format: BarcodeFormat,
|
||||
val size: Int,
|
||||
val margin: Int,
|
||||
val ecLevel: Int,
|
||||
val colors: Colors
|
||||
) {
|
||||
private var _bitmap: Bitmap? = null
|
||||
fun bitmap(): Bitmap {
|
||||
val b = _bitmap ?: ZxingCpp.encodeAsBitmap(
|
||||
content, format, size, size, margin, ecLevel,
|
||||
setColor = colors.foregroundColor(),
|
||||
unsetColor = colors.backgroundColor()
|
||||
)
|
||||
_bitmap = b
|
||||
return b
|
||||
}
|
||||
|
||||
private var _svg: String? = null
|
||||
fun svg(): String {
|
||||
val s = _svg ?: ZxingCpp.encodeAsSvg(
|
||||
content, format, margin, ecLevel
|
||||
)
|
||||
_svg = s
|
||||
return s
|
||||
}
|
||||
|
||||
private var _text: String? = null
|
||||
fun text(): String {
|
||||
val t = _text ?: ZxingCpp.encodeAsText(
|
||||
content, format, margin, ecLevel,
|
||||
inverted = colors == Colors.BLACK_ON_WHITE
|
||||
)
|
||||
_text = t
|
||||
return t
|
||||
}
|
||||
}
|
||||
|
||||
private enum class Colors {
|
||||
BLACK_ON_WHITE,
|
||||
WHITE_ON_BLACK,
|
||||
BLACK_ON_TRANSPARENT,
|
||||
WHITE_ON_TRANSPARENT;
|
||||
|
||||
fun foregroundColor(): Int = when (this) {
|
||||
BLACK_ON_WHITE,
|
||||
BLACK_ON_TRANSPARENT -> COLOR_BLACK
|
||||
|
||||
WHITE_ON_BLACK,
|
||||
WHITE_ON_TRANSPARENT -> COLOR_WHITE
|
||||
}
|
||||
|
||||
fun backgroundColor(): Int = when (this) {
|
||||
BLACK_ON_WHITE -> COLOR_WHITE
|
||||
WHITE_ON_BLACK -> COLOR_BLACK
|
||||
BLACK_ON_TRANSPARENT,
|
||||
WHITE_ON_TRANSPARENT -> 0
|
||||
}
|
||||
}
|
||||
|
||||
private fun Bitmap.saveAsPng(outputStream: OutputStream, quality: Int = 90) =
|
||||
compress(Bitmap.CompressFormat.PNG, quality, outputStream)
|
||||
|
||||
private 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 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,688 +0,0 @@
|
||||
package de.markusfisch.android.binaryeye.fragment
|
||||
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.support.design.widget.FloatingActionButton
|
||||
import android.support.v4.app.Fragment
|
||||
import android.text.Editable
|
||||
import android.text.Html
|
||||
import android.text.Spannable
|
||||
import android.text.SpannableString
|
||||
import android.text.TextWatcher
|
||||
import android.text.method.LinkMovementMethod
|
||||
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.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.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.copyToClipboard
|
||||
import de.markusfisch.android.binaryeye.content.shareAsFile
|
||||
import de.markusfisch.android.binaryeye.content.shareText
|
||||
import de.markusfisch.android.binaryeye.content.toHexString
|
||||
import de.markusfisch.android.binaryeye.content.wipeLastShareFile
|
||||
import de.markusfisch.android.binaryeye.database.Recreation
|
||||
import de.markusfisch.android.binaryeye.database.Scan
|
||||
import de.markusfisch.android.binaryeye.database.toRecreation
|
||||
import de.markusfisch.android.binaryeye.io.askForFileName
|
||||
import de.markusfisch.android.binaryeye.io.toSaveResult
|
||||
import de.markusfisch.android.binaryeye.io.writeExternalFile
|
||||
import de.markusfisch.android.binaryeye.view.setPaddingFromWindowInsets
|
||||
import de.markusfisch.android.binaryeye.widget.toast
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.security.MessageDigest
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
class DecodeFragment : Fragment() {
|
||||
private lateinit var contentView: EditText
|
||||
private lateinit var formatView: TextView
|
||||
private lateinit var dataView: TableLayout
|
||||
private lateinit var metaView: TableLayout
|
||||
private lateinit var hexView: TextView
|
||||
private lateinit var stampView: TextView
|
||||
private lateinit var recreationView: ImageView
|
||||
private lateinit var labelView: EditText
|
||||
private lateinit var fab: FloatingActionButton
|
||||
private lateinit var format: String
|
||||
|
||||
private val parentJob = Job()
|
||||
private val scope: CoroutineScope = CoroutineScope(Dispatchers.Main + parentJob)
|
||||
private val dp: Float
|
||||
get() = resources.displayMetrics.density
|
||||
private val content: String
|
||||
get() = contentView.text.toString()
|
||||
|
||||
private var closeAutomatically = false
|
||||
private var action = ActionRegistry.DEFAULT_ACTION
|
||||
private var isBinary = false
|
||||
private var originalBytes: ByteArray = ByteArray(0)
|
||||
private var id = 0L
|
||||
private var recreation: Recreation? = null
|
||||
private var label: String? = null
|
||||
|
||||
override fun onCreate(state: Bundle?) {
|
||||
super.onCreate(state)
|
||||
setHasOptionsMenu(true)
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
state: Bundle?
|
||||
): View {
|
||||
activity?.setTitle(R.string.content)
|
||||
|
||||
val view = inflater.inflate(
|
||||
R.layout.fragment_decode,
|
||||
container,
|
||||
false
|
||||
)
|
||||
|
||||
closeAutomatically = prefs.closeAutomatically &&
|
||||
activity?.intent?.hasExtra(MainActivity.DECODED) == true
|
||||
|
||||
val scan = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
arguments?.getParcelable(SCAN, Scan::class.java)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
arguments?.getParcelable(SCAN) as Scan?
|
||||
} ?: throw IllegalArgumentException("DecodeFragment needs a Scan")
|
||||
|
||||
val originalContent = scan.content
|
||||
isBinary = scan.content.isEmpty()
|
||||
originalBytes = scan.raw ?: originalContent.toByteArray()
|
||||
format = scan.format.name
|
||||
id = scan.id
|
||||
|
||||
contentView = view.findViewById(R.id.content)
|
||||
formatView = view.findViewById(R.id.format)
|
||||
dataView = view.findViewById(R.id.data)
|
||||
metaView = view.findViewById(R.id.meta)
|
||||
hexView = view.findViewById(R.id.hex)
|
||||
stampView = view.findViewById(R.id.stamp)
|
||||
recreationView = view.findViewById(R.id.recreation)
|
||||
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 (id > 0) {
|
||||
scan.label?.let {
|
||||
labelView.setText(it)
|
||||
label = it
|
||||
}
|
||||
} else {
|
||||
labelView.visibility = View.GONE
|
||||
}
|
||||
|
||||
updateViewsAndFab(originalContent, originalBytes)
|
||||
|
||||
(view.findViewById(R.id.inset_layout) as View).setPaddingFromWindowInsets()
|
||||
(view.findViewById(R.id.scroll_view) as View).setPaddingFromWindowInsets()
|
||||
|
||||
return view
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
if (id > 0) {
|
||||
val newLabel = labelView.text.toString().trim()
|
||||
if (newLabel != (label ?: "")) {
|
||||
db.renameScan(id, newLabel)
|
||||
label = newLabel
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
parentJob.cancel()
|
||||
wipeLastShareFile()
|
||||
}
|
||||
|
||||
private fun initContentAndFab(originalContent: String) {
|
||||
if (isBinary) {
|
||||
contentView.setText(String(originalBytes).foldNonAlNum())
|
||||
contentView.isEnabled = false
|
||||
fab.setImageResource(R.drawable.ic_action_save)
|
||||
fab.setOnClickListener {
|
||||
askForFileNameAndSave(originalBytes)
|
||||
}
|
||||
} else {
|
||||
contentView.setText(originalContent)
|
||||
contentView.addTextChangedListener(object : TextWatcher {
|
||||
override fun afterTextChanged(s: Editable?) {
|
||||
updateViewsAndFab(s?.toString() ?: "")
|
||||
}
|
||||
|
||||
override fun beforeTextChanged(
|
||||
s: CharSequence?,
|
||||
start: Int,
|
||||
count: Int,
|
||||
after: Int
|
||||
) = Unit
|
||||
|
||||
override fun onTextChanged(
|
||||
s: CharSequence?,
|
||||
start: Int,
|
||||
before: Int,
|
||||
count: Int
|
||||
) = Unit
|
||||
})
|
||||
fab.setOnClickListener {
|
||||
executeAction(content)
|
||||
}
|
||||
if (prefs.openImmediately) {
|
||||
executeAction(content)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateViewsAndFab(text: String, bytes: ByteArray? = null) {
|
||||
val b = bytes ?: text.toByteArray()
|
||||
resolveActionAndUpdateFab(b)
|
||||
updateViews(text, b)
|
||||
}
|
||||
|
||||
private fun resolveActionAndUpdateFab(bytes: ByteArray) {
|
||||
val prevAction = action
|
||||
if (!prevAction.canExecuteOn(bytes)) {
|
||||
action = ActionRegistry.getAction(bytes)
|
||||
}
|
||||
if (prevAction !== action) {
|
||||
updateFab(action)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateFab(action: IAction) {
|
||||
fab.setImageResource(action.iconResId)
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
||||
fab.setOnLongClickListener { v ->
|
||||
v.context.toast(action.titleResId)
|
||||
true
|
||||
}
|
||||
} else {
|
||||
fab.tooltipText = getString(action.titleResId)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateViews(text: String, bytes: ByteArray) {
|
||||
formatView.text = resources.getQuantityString(
|
||||
R.plurals.barcode_info,
|
||||
bytes.size,
|
||||
prettifyFormatName(format),
|
||||
bytes.size
|
||||
)
|
||||
hexView.showIf(prefs.showHexDump) { v ->
|
||||
v.text = hexDump(bytes)
|
||||
}
|
||||
recreationView.showIf(prefs.showRecreation) { v ->
|
||||
val r = recreation ?: return@showIf
|
||||
val content = if (isBinary) bytes else text
|
||||
v.setImageBitmap(r.encode(content))
|
||||
v.setOnClickListener {
|
||||
fragmentManager?.addFragment(
|
||||
BarcodeFragment.newInstance(
|
||||
content,
|
||||
r.format,
|
||||
r.size,
|
||||
r.margin,
|
||||
r.ecLevel
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
dataView.fillDataView(text, bytes)
|
||||
stampView.setTrackingLink(bytes, format)
|
||||
}
|
||||
|
||||
private fun TableLayout.fillDataView(text: String, bytes: ByteArray) {
|
||||
val items = LinkedHashMap<Int, 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
|
||||
}
|
||||
if (action is WifiAction) {
|
||||
WifiConnector.parseMap(text)?.let { wifiData ->
|
||||
items.putAll(
|
||||
linkedMapOf(
|
||||
R.string.entry_type to getString(R.string.wifi_network),
|
||||
R.string.wifi_ssid to wifiData["S"],
|
||||
R.string.wifi_password to wifiData["P"],
|
||||
R.string.wifi_type to wifiData["T"],
|
||||
R.string.wifi_hidden to wifiData["H"],
|
||||
R.string.wifi_eap to wifiData["E"],
|
||||
R.string.wifi_identity to wifiData["I"],
|
||||
R.string.wifi_anonymous_identity to wifiData["A"],
|
||||
R.string.wifi_phase2 to wifiData["PH2"]
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
fill(items)
|
||||
}
|
||||
|
||||
private fun TableLayout.fillMetaView(scan: Scan) {
|
||||
val items = linkedMapOf<Int, CharSequence?>(
|
||||
R.string.error_correction_level to scan.errorCorrectionLevel,
|
||||
R.string.sequence_size to scan.sequenceSize.positiveToString(),
|
||||
R.string.sequence_index to scan.sequenceIndex.positiveToString(),
|
||||
R.string.sequence_id to scan.sequenceId,
|
||||
R.string.gtin_country to scan.country,
|
||||
R.string.gtin_add_on to scan.addOn,
|
||||
R.string.gtin_price to scan.price,
|
||||
R.string.gtin_issue_number to scan.issueNumber,
|
||||
)
|
||||
if (!scan.version.isNullOrBlank()) {
|
||||
val versionString = if (scan.format.name == "QR_CODE") {
|
||||
getString(
|
||||
R.string.qr_version_and_modules,
|
||||
scan.version,
|
||||
(scan.version.toIntOrNull() ?: 0) * 4 + 17
|
||||
)
|
||||
} else {
|
||||
scan.version
|
||||
}
|
||||
items.putAll(
|
||||
linkedMapOf(
|
||||
R.string.barcode_version_number to versionString
|
||||
)
|
||||
)
|
||||
}
|
||||
fill(items)
|
||||
}
|
||||
|
||||
private fun TableLayout.fill(
|
||||
items: LinkedHashMap<Int, CharSequence?>
|
||||
) {
|
||||
removeAllViews()
|
||||
visibility = if (items.isEmpty()) View.GONE else {
|
||||
val ctx = context
|
||||
val spaceBetween = (16f * dp).roundToInt()
|
||||
items.forEach { item ->
|
||||
val text = item.value
|
||||
if (!text.isNullOrBlank()) {
|
||||
val tr = TableRow(ctx)
|
||||
val keyView = TextView(ctx)
|
||||
keyView.setText(item.key)
|
||||
val valueView = TextView(ctx)
|
||||
valueView.setPadding(spaceBetween, 0, 0, 0)
|
||||
valueView.text = text
|
||||
tr.addView(keyView)
|
||||
tr.addView(valueView)
|
||||
addView(tr)
|
||||
}
|
||||
}
|
||||
View.VISIBLE
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
inflater.inflate(R.menu.fragment_decode, menu)
|
||||
if (id > 0L) {
|
||||
menu.findItem(R.id.remove).isVisible = true
|
||||
}
|
||||
if (action is WifiAction) {
|
||||
menu.findItem(R.id.copy_password).isVisible = true
|
||||
}
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
return when (item.itemId) {
|
||||
R.id.copy_password -> {
|
||||
copyPasswordToClipboard()
|
||||
maybeBackOrFinish()
|
||||
true
|
||||
}
|
||||
|
||||
R.id.copy_to_clipboard -> {
|
||||
copyToClipboard(textOrHex())
|
||||
maybeBackOrFinish()
|
||||
true
|
||||
}
|
||||
|
||||
R.id.share -> {
|
||||
context?.apply {
|
||||
shareText(textOrHex())
|
||||
maybeBackOrFinish()
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
R.id.share_file -> {
|
||||
context?.apply {
|
||||
val (data, name) = if (isBinary) {
|
||||
Pair(originalBytes, "barcode_content.bin")
|
||||
} else {
|
||||
Pair(content, "barcode_content.txt")
|
||||
}
|
||||
shareAsFile(data, name)
|
||||
maybeBackOrFinish()
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
R.id.create -> {
|
||||
fragmentManager?.addFragment(
|
||||
EncodeFragment.newInstance(
|
||||
if (isBinary) originalBytes else content,
|
||||
format
|
||||
)
|
||||
)
|
||||
true
|
||||
}
|
||||
|
||||
R.id.remove -> {
|
||||
db.removeScan(id)
|
||||
backOrFinish()
|
||||
true
|
||||
}
|
||||
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
}
|
||||
|
||||
private fun textOrHex() = if (isBinary) {
|
||||
originalBytes.toHexString()
|
||||
} else {
|
||||
content
|
||||
}
|
||||
|
||||
private fun copyPasswordToClipboard() {
|
||||
val ac = action
|
||||
if (ac is WifiAction) {
|
||||
ac.password?.let { password ->
|
||||
activity?.apply {
|
||||
copyToClipboard(password, isSensitive = true)
|
||||
toast(R.string.copied_password_to_clipboard)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun copyToClipboard(text: String, isSensitive: Boolean = false) {
|
||||
activity?.apply {
|
||||
copyToClipboard(text, isSensitive)
|
||||
toast(R.string.copied_to_clipboard)
|
||||
}
|
||||
}
|
||||
|
||||
private fun maybeBackOrFinish() {
|
||||
if (closeAutomatically) {
|
||||
backOrFinish()
|
||||
}
|
||||
}
|
||||
|
||||
private fun backOrFinish() {
|
||||
val fm = fragmentManager
|
||||
if (fm != null && fm.backStackEntryCount > 0) {
|
||||
fm.popBackStack()
|
||||
} else {
|
||||
activity?.finish()
|
||||
}
|
||||
}
|
||||
|
||||
private fun executeAction(content: String) {
|
||||
val ac = activity ?: return
|
||||
if (content.isNotEmpty()) {
|
||||
if (action is WifiAction &&
|
||||
Build.VERSION.SDK_INT < Build.VERSION_CODES.Q &&
|
||||
!ac.hasLocationPermission { executeAction(content) }
|
||||
) {
|
||||
return
|
||||
}
|
||||
scope.launch {
|
||||
action.execute(ac, content.toByteArray())
|
||||
maybeBackOrFinish()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun askForFileNameAndSave(raw: ByteArray) {
|
||||
val ac = activity ?: return
|
||||
// Write permission is only required before Android Q.
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q &&
|
||||
!ac.hasWritePermission { askForFileNameAndSave(raw) }
|
||||
) {
|
||||
return
|
||||
}
|
||||
scope.launch(Dispatchers.Main) {
|
||||
val name = ac.askForFileName() ?: return@launch
|
||||
withContext(Dispatchers.IO) {
|
||||
val message = ac.writeExternalFile(
|
||||
name,
|
||||
"application/octet-stream"
|
||||
) {
|
||||
it.write(raw)
|
||||
}.toSaveResult()
|
||||
withContext(Dispatchers.Main) {
|
||||
ac.toast(message)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val SCAN = "scan"
|
||||
|
||||
fun newInstance(scan: Scan): Fragment {
|
||||
val args = Bundle()
|
||||
args.putParcelable(SCAN, scan)
|
||||
val fragment = DecodeFragment()
|
||||
fragment.arguments = args
|
||||
return fragment
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private inline fun <T : View> T.showIf(
|
||||
visible: Boolean,
|
||||
block: (T) -> Unit
|
||||
) {
|
||||
visibility = if (visible) {
|
||||
block.invoke(this)
|
||||
View.VISIBLE
|
||||
} else {
|
||||
View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
private val nonAlNum = "[^a-zA-Z0-9]".toRegex()
|
||||
private val multipleDots = "[…]+".toRegex()
|
||||
private fun String.foldNonAlNum() = replace(nonAlNum, "…")
|
||||
.replace(multipleDots, "…")
|
||||
|
||||
private fun hexDump(bytes: ByteArray, charsPerLine: Int = 33): String {
|
||||
if (charsPerLine < 4 || bytes.isEmpty()) {
|
||||
return ""
|
||||
}
|
||||
val dump = StringBuilder()
|
||||
val hex = StringBuilder()
|
||||
val ascii = StringBuilder()
|
||||
val itemsPerLine = (charsPerLine - 1) / 4
|
||||
val len = bytes.size
|
||||
var i = 0
|
||||
while (true) {
|
||||
val ord = bytes[i]
|
||||
hex.append(String.format("%02X ", ord))
|
||||
ascii.append(if (ord > 31) ord.toInt().toChar() else " ")
|
||||
++i
|
||||
val posInLine = i % itemsPerLine
|
||||
val atEnd = i >= len
|
||||
var atLineEnd = posInLine == 0
|
||||
if (atEnd && !atLineEnd) {
|
||||
for (j in posInLine until itemsPerLine) {
|
||||
hex.append(" ")
|
||||
}
|
||||
atLineEnd = true
|
||||
}
|
||||
if (atLineEnd) {
|
||||
dump.append(hex.toString())
|
||||
dump.append(" ")
|
||||
dump.append(ascii.toString())
|
||||
dump.append("\n")
|
||||
hex.setLength(0)
|
||||
ascii.setLength(0)
|
||||
}
|
||||
if (atEnd) {
|
||||
break
|
||||
}
|
||||
}
|
||||
return dump.toString()
|
||||
}
|
||||
|
||||
private fun Int.positiveToString() = if (this > -1) this.toString() else ""
|
||||
|
||||
private fun TextView.setTrackingLink(bytes: ByteArray, format: String) {
|
||||
val trackingLink = generateDpTrackingLink(bytes, format)
|
||||
if (trackingLink != null) {
|
||||
text = trackingLink.fromHtml()
|
||||
isClickable = true
|
||||
movementMethod = LinkMovementMethod.getInstance()
|
||||
} else {
|
||||
visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
private fun generateDpTrackingLink(bytes: ByteArray, format: String): String? {
|
||||
// Check for Deutsche Post Matrixcode stamp.
|
||||
var isStamp = false
|
||||
var rawData = bytes
|
||||
if (format == "DATA_MATRIX" &&
|
||||
bytes.toString(Charsets.ISO_8859_1).startsWith("DEA5")
|
||||
) {
|
||||
if (bytes.size == 47) {
|
||||
isStamp = true
|
||||
} else if (bytes.size > 47) {
|
||||
// Transform back to original data.
|
||||
rawData = bytes.toString(Charsets.UTF_8).toByteArray(
|
||||
Charsets.ISO_8859_1
|
||||
)
|
||||
if (rawData.size == 47) {
|
||||
isStamp = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!isStamp) {
|
||||
return null
|
||||
}
|
||||
|
||||
val hex = StringBuilder()
|
||||
hex.append(String.format("%02X", rawData[9]))
|
||||
hex.append(String.format("%02X", rawData[10]))
|
||||
hex.append(String.format("%02X", rawData[11]))
|
||||
hex.append(String.format("%02X", rawData[12]))
|
||||
hex.append(String.format("%02X", rawData[13]))
|
||||
hex.append(String.format("%X", (rawData[4].toInt() and 0x0f).toByte()))
|
||||
hex.append(String.format("%02X", rawData[5]))
|
||||
hex.append(String.format("%02X", rawData[6]))
|
||||
hex.append(String.format("%02X", rawData[7]))
|
||||
hex.append(String.format("%02X", rawData[8]))
|
||||
val hexString = hex.toString()
|
||||
val trackingNumber = hexString + String.format(
|
||||
"%X",
|
||||
crc4(hexString.toByteArray(Charsets.ISO_8859_1))
|
||||
)
|
||||
return "<a href=\"https://www.deutschepost.de/de/s/sendungsverfolgung/verfolgen.html?piececode=$trackingNumber\">Deutsche Post: $trackingNumber</a>"
|
||||
}
|
||||
|
||||
// CRC-4 with polynomial x^4 + x + 1.
|
||||
private fun crc4(input: ByteArray): Int {
|
||||
var crc = 0
|
||||
var i = 0
|
||||
while (i < input.size) {
|
||||
val c = input[i].toInt()
|
||||
var j = 0x80
|
||||
while (j != 0) {
|
||||
var bit = crc and 0x8
|
||||
crc = crc shl 1
|
||||
if (c and j != 0) {
|
||||
bit = bit xor 0x8
|
||||
}
|
||||
if (bit != 0) {
|
||||
crc = crc xor 0x3
|
||||
}
|
||||
j = j ushr 1
|
||||
}
|
||||
++i
|
||||
}
|
||||
crc = crc and 0xF
|
||||
return crc
|
||||
}
|
||||
|
||||
private fun String.fromHtml() = if (
|
||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.N
|
||||
) {
|
||||
Html.fromHtml(this, Html.FROM_HTML_MODE_LEGACY)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
Html.fromHtml(this)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
+224
-64
@@ -9,15 +9,19 @@ import android.content.SharedPreferences.OnSharedPreferenceChangeListener
|
||||
import android.net.wifi.WifiManager
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.support.annotation.RequiresApi
|
||||
import android.support.v14.preference.MultiSelectListPreference
|
||||
import android.support.v7.preference.ListPreference
|
||||
import android.support.v7.preference.Preference
|
||||
import android.support.v7.preference.PreferenceFragmentCompat
|
||||
import android.support.v7.preference.PreferenceGroup
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.preference.ListPreference
|
||||
import androidx.preference.MultiSelectListPreference
|
||||
import androidx.preference.Preference
|
||||
import androidx.preference.PreferenceFragmentCompat
|
||||
import androidx.preference.PreferenceGroup
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.activity.AutomatedActionsActivity
|
||||
import de.markusfisch.android.binaryeye.activity.IgnoreCodesActivity
|
||||
import de.markusfisch.android.binaryeye.activity.NetworkSuggestionsActivity
|
||||
import de.markusfisch.android.binaryeye.activity.ProfilesActivity
|
||||
import de.markusfisch.android.binaryeye.activity.SplashActivity
|
||||
import de.markusfisch.android.binaryeye.app.addFragment
|
||||
import de.markusfisch.android.binaryeye.adapter.prettifyFormatName
|
||||
import de.markusfisch.android.binaryeye.app.hasBluetoothPermission
|
||||
import de.markusfisch.android.binaryeye.app.prefs
|
||||
import de.markusfisch.android.binaryeye.bluetooth.setBluetoothHosts
|
||||
@@ -28,58 +32,119 @@ import de.markusfisch.android.binaryeye.view.systemBarRecyclerViewScrollListener
|
||||
import de.markusfisch.android.binaryeye.widget.toast
|
||||
|
||||
class PreferencesFragment : PreferenceFragmentCompat() {
|
||||
private var lastProfile: String? = null
|
||||
|
||||
private val changeListener = object : OnSharedPreferenceChangeListener {
|
||||
override fun onSharedPreferenceChanged(
|
||||
sharedPreferences: SharedPreferences?,
|
||||
key: String?
|
||||
) {
|
||||
key ?: return
|
||||
val preference = findPreference(key) ?: return
|
||||
prefs.update()
|
||||
when (preference.key) {
|
||||
"custom_locale" -> activity?.restartApp()
|
||||
"beep_tone_name" -> {
|
||||
beepConfirm()
|
||||
setSummary(preference)
|
||||
}
|
||||
|
||||
"send_scan_bluetooth" -> {
|
||||
if (prefs.sendScanBluetooth &&
|
||||
activity?.hasBluetoothPermission() == false
|
||||
) {
|
||||
prefs.sendScanBluetooth = false
|
||||
}
|
||||
setSummary(preference)
|
||||
}
|
||||
|
||||
else -> setSummary(preference)
|
||||
val preference = findPreference<Preference>(key) ?: return
|
||||
if (preference.key != PROFILE) {
|
||||
prefs.update()
|
||||
}
|
||||
when (preference.key) {
|
||||
"custom_locale" -> {
|
||||
activity?.restartApp()
|
||||
return
|
||||
}
|
||||
|
||||
"beep_tone_name",
|
||||
"beep_stream_name" -> beepConfirm()
|
||||
|
||||
"send_scan_bluetooth" -> if (
|
||||
prefs.sendScanBluetooth &&
|
||||
activity?.hasBluetoothPermission() == false
|
||||
) {
|
||||
prefs.sendScanBluetooth = false
|
||||
}
|
||||
}
|
||||
setSummary(preference)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreatePreferences(state: Bundle?, rootKey: String?) {
|
||||
addPreferencesFromResource(R.xml.preferences)
|
||||
loadPreferences(rootKey)
|
||||
}
|
||||
|
||||
private fun loadPreferences(rootKey: String? = null) {
|
||||
preferenceScreen?.sharedPreferences
|
||||
?.unregisterOnSharedPreferenceChangeListener(changeListener)
|
||||
|
||||
preferenceManager.apply {
|
||||
sharedPreferencesName = prefs.profile ?: "${context.packageName}_preferences"
|
||||
sharedPreferencesMode = Context.MODE_PRIVATE
|
||||
}
|
||||
|
||||
// Refresh the preferences.
|
||||
preferenceScreen = null
|
||||
setPreferencesFromResource(R.xml.preferences, rootKey)
|
||||
preferenceScreen.sharedPreferences
|
||||
?.registerOnSharedPreferenceChangeListener(changeListener)
|
||||
setSummaries(preferenceScreen)
|
||||
setIcons()
|
||||
|
||||
wireProfiles()
|
||||
wireAutomatedActions()
|
||||
wireIgnoreCodes()
|
||||
setBluetoothResources()
|
||||
wireClearNetworkPreferences()
|
||||
}
|
||||
|
||||
private fun wireProfiles() {
|
||||
findPreference<Preference>(PROFILE)?.apply {
|
||||
updateProfileSummary(this)
|
||||
onPreferenceClickListener = Preference.OnPreferenceClickListener {
|
||||
startActivity(Intent(activity, ProfilesActivity::class.java))
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun wireAutomatedActions() {
|
||||
findPreference<Preference>(AUTOMATED_ACTIONS)?.apply {
|
||||
updateAutomatedActionsSummary(this)
|
||||
onPreferenceClickListener = Preference.OnPreferenceClickListener {
|
||||
startActivity(
|
||||
Intent(activity, AutomatedActionsActivity::class.java)
|
||||
)
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun wireIgnoreCodes() {
|
||||
findPreference<Preference>(IGNORE_CODES)?.apply {
|
||||
updateIgnoreCodesSummary(this)
|
||||
onPreferenceClickListener = Preference.OnPreferenceClickListener {
|
||||
startActivity(
|
||||
Intent(activity, IgnoreCodesActivity::class.java)
|
||||
)
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setBluetoothResources() {
|
||||
if (prefs.sendScanBluetooth &&
|
||||
activity?.hasBluetoothPermission() == true
|
||||
) {
|
||||
setBluetoothHosts(
|
||||
findPreference("send_scan_bluetooth_host") as ListPreference
|
||||
)
|
||||
findPreference<ListPreference>("send_scan_bluetooth_host")?.let {
|
||||
setBluetoothHosts(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun wireClearNetworkPreferences() {
|
||||
findPreference("clear_network_suggestions").apply {
|
||||
findPreference<Preference>("clear_network_suggestions")?.apply {
|
||||
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.Q) {
|
||||
// From R+ we can query past network suggestions and
|
||||
// make them editable.
|
||||
setOnPreferenceClickListener {
|
||||
fragmentManager.addFragment(NetworkSuggestionsFragment())
|
||||
startActivity(
|
||||
Intent(activity, NetworkSuggestionsActivity::class.java)
|
||||
)
|
||||
true
|
||||
}
|
||||
} else if (Build.VERSION.SDK_INT == Build.VERSION_CODES.Q) {
|
||||
@@ -88,7 +153,7 @@ class PreferencesFragment : PreferenceFragmentCompat() {
|
||||
// adding network suggestions on Q as well, so we
|
||||
// need to keep this option.
|
||||
setOnPreferenceClickListener {
|
||||
context.askToClearNetworkSuggestions()
|
||||
context?.askToClearNetworkSuggestions()
|
||||
true
|
||||
}
|
||||
} else {
|
||||
@@ -124,24 +189,33 @@ class PreferencesFragment : PreferenceFragmentCompat() {
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
if (lastProfile != prefs.profile) {
|
||||
loadPreferences()
|
||||
}
|
||||
activity?.setTitle(R.string.preferences)
|
||||
findPreference<Preference>(AUTOMATED_ACTIONS)?.let {
|
||||
updateAutomatedActionsSummary(it)
|
||||
}
|
||||
findPreference<Preference>(IGNORE_CODES)?.let {
|
||||
updateIgnoreCodesSummary(it)
|
||||
}
|
||||
findPreference<Preference>(PROFILE)?.let {
|
||||
updateProfileSummary(it)
|
||||
}
|
||||
listView.setPaddingFromWindowInsets()
|
||||
listView.removeOnScrollListener(systemBarRecyclerViewScrollListener)
|
||||
listView.addOnScrollListener(systemBarRecyclerViewScrollListener)
|
||||
preferenceScreen.sharedPreferences
|
||||
.registerOnSharedPreferenceChangeListener(changeListener)
|
||||
setSummaries(preferenceScreen)
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
preferenceScreen.sharedPreferences
|
||||
.unregisterOnSharedPreferenceChangeListener(changeListener)
|
||||
?.unregisterOnSharedPreferenceChangeListener(changeListener)
|
||||
}
|
||||
|
||||
override fun onDisplayPreferenceDialog(preference: Preference) {
|
||||
if (preference is UrlPreference) {
|
||||
val fm = fragmentManager
|
||||
val fm = parentFragmentManager
|
||||
UrlDialogFragment.newInstance(preference.key).apply {
|
||||
setTargetFragment(this@PreferencesFragment, 0)
|
||||
show(fm, null)
|
||||
@@ -165,38 +239,124 @@ class PreferencesFragment : PreferenceFragmentCompat() {
|
||||
}
|
||||
|
||||
private fun setSummary(preference: Preference) {
|
||||
when (preference) {
|
||||
is UrlPreference -> {
|
||||
preference.setSummary(preference.getUrl())
|
||||
}
|
||||
|
||||
is ListPreference -> {
|
||||
preference.setSummary(preference.entry)
|
||||
}
|
||||
|
||||
is MultiSelectListPreference -> {
|
||||
preference.setSummary(
|
||||
preference.values.joinToString(", ") {
|
||||
it.replace(Regex("_"), " ")
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
is PreferenceGroup -> {
|
||||
setSummaries(preference)
|
||||
}
|
||||
when (preference.key) {
|
||||
AUTOMATED_ACTIONS -> return updateAutomatedActionsSummary(preference)
|
||||
IGNORE_CODES -> return updateIgnoreCodesSummary(preference)
|
||||
}
|
||||
when (preference) {
|
||||
is UrlPreference -> preference.summary = preference.getUrl()
|
||||
is ListPreference -> preference.setSummary(preference.entry)
|
||||
is MultiSelectListPreference -> preference.summary =
|
||||
getSortedSummary(preference)
|
||||
|
||||
is PreferenceGroup -> setSummaries(preference)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getSortedSummary(preference: MultiSelectListPreference): String {
|
||||
val labelsByValue = mutableMapOf<String, String>()
|
||||
val entries = preference.entries
|
||||
val entryValues = preference.entryValues
|
||||
val count = minOf(entries.size, entryValues.size)
|
||||
|
||||
for (i in 0 until count) {
|
||||
labelsByValue[entryValues[i].toString()] = entries[i].toString()
|
||||
}
|
||||
|
||||
return preference.values
|
||||
.map { value ->
|
||||
labelsByValue[value] ?: value.prettifyFormatName()
|
||||
}
|
||||
.sortedBy { it.lowercase() }
|
||||
.joinToString(", ")
|
||||
}
|
||||
|
||||
private fun setIcons() {
|
||||
setIcon(PROFILE, R.drawable.ic_action_profile)
|
||||
setIcon("formats", R.drawable.ic_action_scan)
|
||||
setIcon("show_crop_handle", R.drawable.ic_action_crop)
|
||||
setIcon("show_crosshairs", R.drawable.ic_action_search)
|
||||
setIcon("zoom_by_swiping", R.drawable.ic_action_search)
|
||||
setIcon("auto_rotate", R.drawable.ic_action_down)
|
||||
setIcon("try_harder", R.drawable.ic_action_search)
|
||||
setIcon("bulk_mode", R.drawable.ic_action_bulk_mode)
|
||||
setIcon("bulk_mode_delay", R.drawable.ic_action_delay)
|
||||
setIcon("show_toast_in_bulk_mode", R.drawable.ic_action_toast)
|
||||
setIcon("vibrate", R.drawable.ic_action_vibrate)
|
||||
setIcon("beep", R.drawable.ic_action_beep)
|
||||
setIcon("beep_tone_name", R.drawable.ic_action_notification_sound)
|
||||
setIcon("beep_stream_name", R.drawable.ic_action_notification_sound)
|
||||
setIcon("use_history", R.drawable.ic_action_history)
|
||||
setIcon("ignore_duplicates_name", R.drawable.ic_action_remove)
|
||||
setIcon(IGNORE_CODES, R.drawable.ic_action_remove)
|
||||
setIcon("copy_immediately", R.drawable.ic_action_copy)
|
||||
setIcon("send_scan_active", R.drawable.ic_action_forward)
|
||||
setIcon("send_scan_url", R.drawable.ic_action_link)
|
||||
setIcon("send_scan_type", R.drawable.ic_action_link)
|
||||
setIcon("send_scan_bluetooth", R.drawable.ic_action_bluetooth)
|
||||
setIcon("send_scan_bluetooth_host", R.drawable.ic_action_bluetooth)
|
||||
setIcon("open_immediately", R.drawable.ic_action_open)
|
||||
setIcon("show_meta_data", R.drawable.ic_action_info)
|
||||
setIcon("show_hex_dump", R.drawable.ic_action_info)
|
||||
setIcon("show_checksum", R.drawable.ic_action_info)
|
||||
setIcon("show_recreation", R.drawable.ic_action_scan)
|
||||
setIcon("close_automatically", R.drawable.ic_action_back)
|
||||
setIcon("default_search_url", R.drawable.ic_action_search)
|
||||
setIcon("open_with_url", R.drawable.ic_action_link)
|
||||
setIcon(AUTOMATED_ACTIONS, R.drawable.ic_action_create)
|
||||
setIcon("clear_network_suggestions", R.drawable.ic_action_wifi)
|
||||
setIcon("brighten_screen", R.drawable.ic_action_bright)
|
||||
setIcon("custom_locale", R.drawable.ic_action_preferences)
|
||||
}
|
||||
|
||||
private fun setIcon(key: String, iconResId: Int) {
|
||||
findPreference<Preference>(key)?.setIcon(iconResId)
|
||||
}
|
||||
|
||||
private fun updateAutomatedActionsSummary(preference: Preference) {
|
||||
val count = prefs.automatedActions.size
|
||||
preference.summary = if (count == 0) {
|
||||
getString(R.string.automated_actions_none)
|
||||
} else {
|
||||
resources.getQuantityString(
|
||||
R.plurals.automated_actions_count,
|
||||
count,
|
||||
count
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateIgnoreCodesSummary(preference: Preference) {
|
||||
val count = prefs.ignoreCodes.size
|
||||
preference.summary = if (count == 0) {
|
||||
getString(R.string.ignore_codes_none)
|
||||
} else {
|
||||
resources.getQuantityString(
|
||||
R.plurals.ignore_codes_count,
|
||||
count,
|
||||
count
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateProfileSummary(preference: Preference) {
|
||||
preference.summary = prefs.profile ?: getString(R.string.profile_default)
|
||||
lastProfile = prefs.profile
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val PROFILE = "profile"
|
||||
private const val AUTOMATED_ACTIONS = "automated_actions"
|
||||
private const val IGNORE_CODES = "ignore_codes"
|
||||
}
|
||||
}
|
||||
|
||||
private fun Activity.restartApp() {
|
||||
val intent = Intent(this, SplashActivity::class.java)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
|
||||
intent.addFlags(
|
||||
Intent.FLAG_ACTIVITY_NEW_TASK or
|
||||
Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||
)
|
||||
}
|
||||
intent.addFlags(
|
||||
Intent.FLAG_ACTIVITY_NEW_TASK or
|
||||
Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||
)
|
||||
startActivity(intent)
|
||||
finish()
|
||||
// Restart to begin with an unmodified Locale to follow system settings.
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
package de.markusfisch.android.binaryeye.fragment
|
||||
|
||||
import android.os.Bundle
|
||||
import android.support.v7.preference.PreferenceDialogFragmentCompat
|
||||
import android.view.View
|
||||
import android.widget.TextView
|
||||
import androidx.preference.PreferenceDialogFragmentCompat
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.app.prefs
|
||||
import de.markusfisch.android.binaryeye.database.Scan
|
||||
@@ -15,10 +15,10 @@ class UrlDialogFragment : PreferenceDialogFragmentCompat() {
|
||||
private var urlView: TextView? = null
|
||||
private var testButton: TextView? = null
|
||||
|
||||
override fun onBindDialogView(view: View?) {
|
||||
override fun onBindDialogView(view: View) {
|
||||
super.onBindDialogView(view)
|
||||
urlView = view?.findViewById(R.id.url)
|
||||
testButton = view?.findViewById(R.id.test_url)
|
||||
urlView = view.findViewById(R.id.url)
|
||||
testButton = view.findViewById(R.id.test_url)
|
||||
testButton?.setOnClickListener {
|
||||
testUrl(testButton)
|
||||
}
|
||||
@@ -51,7 +51,7 @@ class UrlDialogFragment : PreferenceDialogFragmentCompat() {
|
||||
Scan(
|
||||
"test",
|
||||
null,
|
||||
BarcodeFormat.NONE
|
||||
BarcodeFormat.None
|
||||
).sendAsync(url, prefs.sendScanType) { code, body ->
|
||||
textView.text = when {
|
||||
code != null -> "$code"
|
||||
@@ -80,10 +80,5 @@ private fun completeUrl(template: String): String {
|
||||
if (!s.startsWith("http")) {
|
||||
s = "http://${s}"
|
||||
}
|
||||
if (prefs.sendScanType == "0" &&
|
||||
!s.matches(".*/[a-zA-Z._-]*\\?[a-zA-Z0-9&=_-]+=$".toRegex())
|
||||
) {
|
||||
s = "${s}?content="
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ fun ContentResolver.loadImageUri(uri: Uri): Bitmap? = try {
|
||||
openInputStream(uri)?.use {
|
||||
BitmapFactory.decodeStream(it, null, options)
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
} catch (_: IOException) {
|
||||
null
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ package de.markusfisch.android.binaryeye.graphics
|
||||
import android.content.Context
|
||||
import android.graphics.DashPathEffect
|
||||
import android.graphics.Paint
|
||||
import android.support.v4.content.ContextCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
|
||||
fun Context.getDashedBorderPaint() = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
|
||||
@@ -5,26 +5,22 @@ import android.graphics.Bitmap
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.drawable.BitmapDrawable
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.os.Build
|
||||
import androidx.core.content.res.ResourcesCompat
|
||||
import androidx.core.graphics.createBitmap
|
||||
|
||||
fun Resources.getBitmapFromDrawable(
|
||||
resId: Int
|
||||
): Bitmap = getBitmapFromDrawable(getDrawableCompat(resId))
|
||||
|
||||
fun Resources.getDrawableCompat(
|
||||
resId: Int
|
||||
): Drawable = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
getDrawable(resId, null)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
getDrawable(resId)
|
||||
}
|
||||
): Bitmap = getBitmapFromDrawable(
|
||||
ResourcesCompat.getDrawable(this, resId, null) ?: throw Resources.NotFoundException(
|
||||
"Drawable resource ID #0x${resId.toString(16)}"
|
||||
)
|
||||
)
|
||||
|
||||
private fun getBitmapFromDrawable(drawable: Drawable): Bitmap {
|
||||
if (drawable is BitmapDrawable) {
|
||||
return drawable.bitmap
|
||||
}
|
||||
val bitmap = Bitmap.createBitmap(
|
||||
val bitmap = createBitmap(
|
||||
drawable.intrinsicWidth,
|
||||
drawable.intrinsicHeight,
|
||||
Bitmap.Config.ARGB_8888
|
||||
|
||||
@@ -10,7 +10,9 @@ data class FrameMetrics(
|
||||
var width: Int = 0,
|
||||
var height: Int = 0,
|
||||
var orientation: Int = 0
|
||||
)
|
||||
) {
|
||||
fun isValid() = width > 0 && height > 0
|
||||
}
|
||||
|
||||
fun Rect.setFrameRoi(
|
||||
frameMetrics: FrameMetrics,
|
||||
@@ -38,6 +40,15 @@ fun Rect.setFrameRoi(
|
||||
frameRoiF.bottom.roundToInt()
|
||||
)
|
||||
}
|
||||
val clampedLeft = left.coerceIn(0, frameMetrics.width)
|
||||
val clampedTop = top.coerceIn(0, frameMetrics.height)
|
||||
val clampedRight = right.coerceIn(0, frameMetrics.width)
|
||||
val clampedBottom = bottom.coerceIn(0, frameMetrics.height)
|
||||
if (clampedLeft >= clampedRight || clampedTop >= clampedBottom) {
|
||||
setEmpty()
|
||||
} else {
|
||||
set(clampedLeft, clampedTop, clampedRight, clampedBottom)
|
||||
}
|
||||
}
|
||||
|
||||
fun Matrix.setFrameToView(
|
||||
@@ -86,3 +97,14 @@ fun Matrix.mapPosition(
|
||||
mapPoints(coords)
|
||||
return i
|
||||
}
|
||||
|
||||
fun Matrix.mapViewYToFrame(viewY: Float): Float {
|
||||
val values = FloatArray(9)
|
||||
getValues(values)
|
||||
val scaleY = values[Matrix.MSCALE_Y]
|
||||
return if (scaleY != 0f) {
|
||||
(viewY - values[Matrix.MTRANS_Y]) / scaleY
|
||||
} else {
|
||||
viewY
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,14 +2,16 @@ package de.markusfisch.android.binaryeye.graphics
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Canvas
|
||||
import androidx.core.graphics.createBitmap
|
||||
|
||||
fun Bitmap.fixTransparency(): Bitmap {
|
||||
val result = analyzeTransparency()
|
||||
return if (result.hasTransparentPixels) {
|
||||
val copy = Bitmap.createBitmap(
|
||||
val cfg = config
|
||||
return if (cfg != null && result.hasTransparentPixels) {
|
||||
val copy = createBitmap(
|
||||
width,
|
||||
height,
|
||||
config
|
||||
cfg
|
||||
)
|
||||
Canvas(copy).apply {
|
||||
drawColor(result.backgroundColor)
|
||||
|
||||
@@ -6,8 +6,8 @@ import android.content.Context
|
||||
import android.os.Build
|
||||
import android.os.Environment
|
||||
import android.provider.MediaStore
|
||||
import android.support.annotation.MainThread
|
||||
import android.widget.EditText
|
||||
import androidx.annotation.MainThread
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.app.alertDialog
|
||||
import java.io.File
|
||||
@@ -53,7 +53,7 @@ fun Context.writeExternalFile(
|
||||
): Boolean = try {
|
||||
openExternalOutputStream(fileName, mimeType).use { write(it) }
|
||||
true
|
||||
} catch (e: IOException) {
|
||||
} catch (_: IOException) {
|
||||
false
|
||||
}
|
||||
|
||||
|
||||
@@ -5,13 +5,20 @@ import android.media.ToneGenerator
|
||||
import de.markusfisch.android.binaryeye.app.prefs
|
||||
|
||||
private var confirmToneGenerator: ToneGenerator? = null
|
||||
private var confirmToneStream = AudioManager.STREAM_MUSIC
|
||||
private var errorToneGenerator: ToneGenerator? = null
|
||||
|
||||
fun beepConfirm() {
|
||||
val tg = confirmToneGenerator ?: ToneGenerator(
|
||||
AudioManager.STREAM_NOTIFICATION,
|
||||
ToneGenerator.MAX_VOLUME
|
||||
)
|
||||
val stream = prefs.beepStream()
|
||||
val tg = if (confirmToneGenerator == null || confirmToneStream != stream) {
|
||||
confirmToneGenerator?.release()
|
||||
ToneGenerator(stream, ToneGenerator.MAX_VOLUME).also {
|
||||
confirmToneGenerator = it
|
||||
confirmToneStream = stream
|
||||
}
|
||||
} else {
|
||||
confirmToneGenerator!!
|
||||
}
|
||||
confirmToneGenerator = tg
|
||||
tg.startTone(prefs.beepTone())
|
||||
}
|
||||
@@ -28,6 +35,7 @@ fun beepError() {
|
||||
fun releaseToneGenerators() {
|
||||
confirmToneGenerator?.release()
|
||||
confirmToneGenerator = null
|
||||
confirmToneStream = AudioManager.STREAM_MUSIC
|
||||
errorToneGenerator?.release()
|
||||
errorToneGenerator = null
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
package de.markusfisch.android.binaryeye.net
|
||||
|
||||
import android.net.Uri
|
||||
|
||||
private const val SCHEME = "binaryeye"
|
||||
private const val HOSTNAME = "markusfisch.de"
|
||||
private const val ENCODE_PATH = "encode"
|
||||
|
||||
fun isScanDeeplink(text: String) = listOf(
|
||||
"$SCHEME://scan",
|
||||
"http://$HOSTNAME/BinaryEye",
|
||||
"https://$HOSTNAME/BinaryEye"
|
||||
).any { text.startsWith(it) }
|
||||
|
||||
fun isEncodeDeeplink(text: String) = listOf(
|
||||
"$SCHEME://$ENCODE_PATH",
|
||||
"http://$HOSTNAME/$ENCODE_PATH",
|
||||
"https://$HOSTNAME/$ENCODE_PATH"
|
||||
).any { text.startsWith(it) }
|
||||
|
||||
fun createEncodeDeeplink(format: String, content: String): String {
|
||||
return Uri.Builder()
|
||||
.scheme("https")
|
||||
.authority(HOSTNAME)
|
||||
.appendPath(ENCODE_PATH)
|
||||
.appendQueryParameter("format", format)
|
||||
.appendQueryParameter("content", content)
|
||||
.build()
|
||||
.toString()
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
package de.markusfisch.android.binaryeye.net
|
||||
|
||||
import de.markusfisch.android.binaryeye.content.toHexString
|
||||
import de.markusfisch.android.binaryeye.database.Scan
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -36,7 +35,10 @@ private fun Scan.send(url: String, type: String): Response {
|
||||
|
||||
"2" -> request(url) { con ->
|
||||
con.requestMethod = "POST"
|
||||
con.setRequestProperty("Content-Type", "application/x-www-form-urlencoded")
|
||||
con.setRequestProperty(
|
||||
"Content-Type",
|
||||
"application/x-www-form-urlencoded"
|
||||
)
|
||||
con.outputStream.apply {
|
||||
write(asUrlArguments().toByteArray())
|
||||
close()
|
||||
@@ -53,7 +55,7 @@ private fun Scan.send(url: String, type: String): Response {
|
||||
}
|
||||
|
||||
else -> request(
|
||||
url + content.urlEncode()
|
||||
url + text.urlEncode()
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -67,7 +69,7 @@ private fun Scan.asJson() = JSONObject().apply {
|
||||
}
|
||||
|
||||
private fun Scan.getMap(): Map<String, String> = mapOf(
|
||||
"content" to content,
|
||||
"content" to text,
|
||||
"raw" to raw?.toHexString(),
|
||||
"format" to format.name,
|
||||
"errorCorrectionLevel" to errorCorrectionLevel,
|
||||
|
||||
@@ -5,6 +5,6 @@ import java.net.URLEncoder
|
||||
|
||||
fun String.urlEncode(): String = try {
|
||||
URLEncoder.encode(this, "UTF-8")
|
||||
} catch (e: UnsupportedEncodingException) {
|
||||
} catch (_: UnsupportedEncodingException) {
|
||||
this
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
package de.markusfisch.android.binaryeye.os
|
||||
|
||||
import android.app.Activity
|
||||
import android.view.WindowManager
|
||||
|
||||
fun Activity.getScreenBrightness(): Float {
|
||||
val layoutParams = window.attributes
|
||||
return if (layoutParams.screenBrightness ==
|
||||
WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_NONE
|
||||
) {
|
||||
android.provider.Settings.System.getInt(
|
||||
contentResolver,
|
||||
android.provider.Settings.System.SCREEN_BRIGHTNESS
|
||||
) / 255.0f
|
||||
} else {
|
||||
layoutParams.screenBrightness
|
||||
}
|
||||
}
|
||||
|
||||
fun Activity.setScreenBrightness(brightness: Float) {
|
||||
val layoutParams = window.attributes
|
||||
layoutParams.screenBrightness = brightness
|
||||
window.attributes = layoutParams
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package de.markusfisch.android.binaryeye.preference
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import android.graphics.Point
|
||||
import androidx.core.content.edit
|
||||
import org.json.JSONObject
|
||||
|
||||
fun SharedPreferences.storeCropHandle(
|
||||
name: String,
|
||||
viewWidth: Int,
|
||||
viewHeight: Int,
|
||||
cropHandle: Point
|
||||
) {
|
||||
edit {
|
||||
putString(
|
||||
cropHandleKey(name, viewWidth, viewHeight),
|
||||
JSONObject().apply {
|
||||
put("x", cropHandle.x)
|
||||
put("y", cropHandle.y)
|
||||
}.toString()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun SharedPreferences.restoreCropHandle(
|
||||
name: String,
|
||||
viewWidth: Int,
|
||||
viewHeight: Int,
|
||||
default: Point = Point(-2, -2) // -2 means set default roi.
|
||||
): Point {
|
||||
val s = getString(
|
||||
cropHandleKey(name, viewWidth, viewHeight), null
|
||||
) ?: return default
|
||||
return try {
|
||||
val json = JSONObject(s)
|
||||
Point(
|
||||
json.getInt("x"),
|
||||
json.getInt("y")
|
||||
)
|
||||
} catch (_: Exception) {
|
||||
return default
|
||||
}
|
||||
}
|
||||
|
||||
private fun cropHandleKey(
|
||||
name: String,
|
||||
viewWidth: Int,
|
||||
viewHeight: Int
|
||||
) = "${name}:${viewWidth}:${viewHeight}"
|
||||
@@ -0,0 +1,75 @@
|
||||
package de.markusfisch.android.binaryeye.preference
|
||||
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
|
||||
data class IgnoreCode(
|
||||
val pattern: String,
|
||||
val format: String? = null
|
||||
) {
|
||||
private val compiledRegex: Regex? by lazy {
|
||||
try {
|
||||
Regex(pattern)
|
||||
} catch (_: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun matches(content: String, format: String): Boolean =
|
||||
(this.format == null || this.format == format) &&
|
||||
compiledRegex?.containsMatchIn(content) == true
|
||||
|
||||
fun toJson(): JSONObject = JSONObject().apply {
|
||||
put(KEY_PATTERN, pattern)
|
||||
put(KEY_FORMAT, format ?: "")
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val KEY_PATTERN = "pattern"
|
||||
private const val KEY_FORMAT = "format"
|
||||
|
||||
fun fromJson(obj: JSONObject): IgnoreCode? {
|
||||
val pattern = obj.optString(KEY_PATTERN, "").trim()
|
||||
if (pattern.isEmpty()) {
|
||||
return null
|
||||
}
|
||||
val format = obj.optString(KEY_FORMAT, "").trim()
|
||||
return IgnoreCode(
|
||||
pattern = pattern,
|
||||
format = format.ifEmpty { null }
|
||||
)
|
||||
}
|
||||
|
||||
fun fromJsonArray(json: String): MutableList<IgnoreCode> {
|
||||
val items = ArrayList<IgnoreCode>()
|
||||
val array = try {
|
||||
JSONArray(json)
|
||||
} catch (_: Exception) {
|
||||
JSONArray()
|
||||
}
|
||||
for (i in 0 until array.length()) {
|
||||
when (val item = array.opt(i)) {
|
||||
is String -> {
|
||||
val pattern = item.trim()
|
||||
if (pattern.isNotEmpty()) {
|
||||
items.add(IgnoreCode(pattern))
|
||||
}
|
||||
}
|
||||
|
||||
is JSONObject -> {
|
||||
fromJson(item)?.let { items.add(it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
fun toJsonArray(items: List<IgnoreCode>): String {
|
||||
val array = JSONArray()
|
||||
items.forEach { item ->
|
||||
array.put(item.toJson())
|
||||
}
|
||||
return array.toString()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,61 +3,73 @@ package de.markusfisch.android.binaryeye.preference
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import android.graphics.Point
|
||||
import android.media.ToneGenerator
|
||||
import android.os.Build
|
||||
import android.preference.PreferenceManager
|
||||
import android.support.annotation.RequiresApi
|
||||
import androidx.core.content.edit
|
||||
import de.markusfisch.android.binaryeye.automation.AutomatedAction
|
||||
import de.markusfisch.android.binaryeye.automation.AutomatedAction.Companion.fromJsonArray
|
||||
import de.markusfisch.android.binaryeye.automation.AutomatedAction.Companion.toJsonArray
|
||||
import de.markusfisch.android.binaryeye.zxingcpp.migrateBarcodeFormatName
|
||||
import de.markusfisch.android.zxingcpp.ZxingCpp.BarcodeFormat
|
||||
import de.markusfisch.android.binaryeye.preference.IgnoreCode.Companion.fromJsonArray as ignoreCodesFromJsonArray
|
||||
import de.markusfisch.android.binaryeye.preference.IgnoreCode.Companion.toJsonArray as ignoreCodesToJsonArray
|
||||
|
||||
class Preferences {
|
||||
lateinit var defaultPreferences: SharedPreferences
|
||||
lateinit var preferences: SharedPreferences
|
||||
|
||||
val profiles = ArrayList<String>()
|
||||
|
||||
var profile: String? = null
|
||||
set(value) {
|
||||
defaultPreferences.edit {
|
||||
putString(PROFILE, value)
|
||||
}
|
||||
field = value
|
||||
}
|
||||
var barcodeFormats = setOf(
|
||||
BarcodeFormat.AZTEC.name,
|
||||
BarcodeFormat.CODABAR.name,
|
||||
BarcodeFormat.CODE_39.name,
|
||||
BarcodeFormat.CODE_93.name,
|
||||
BarcodeFormat.CODE_128.name,
|
||||
BarcodeFormat.DATA_BAR.name,
|
||||
BarcodeFormat.DATA_BAR_EXPANDED.name,
|
||||
BarcodeFormat.DATA_MATRIX.name,
|
||||
BarcodeFormat.DX_FILM_EDGE.name,
|
||||
BarcodeFormat.EAN_8.name,
|
||||
BarcodeFormat.EAN_13.name,
|
||||
BarcodeFormat.Aztec.name,
|
||||
BarcodeFormat.Codabar.name,
|
||||
BarcodeFormat.Code39Std.name,
|
||||
BarcodeFormat.Code39Ext.name,
|
||||
BarcodeFormat.Code32.name,
|
||||
BarcodeFormat.PZN.name,
|
||||
BarcodeFormat.Code93.name,
|
||||
BarcodeFormat.Code128.name,
|
||||
BarcodeFormat.DataBar.name,
|
||||
BarcodeFormat.DataBarOmni.name,
|
||||
BarcodeFormat.DataBarStk.name,
|
||||
BarcodeFormat.DataBarLtd.name,
|
||||
BarcodeFormat.DataBarExp.name,
|
||||
BarcodeFormat.DataBarExpStk.name,
|
||||
BarcodeFormat.DataMatrix.name,
|
||||
BarcodeFormat.DXFilmEdge.name,
|
||||
BarcodeFormat.EAN8.name,
|
||||
BarcodeFormat.EAN13.name,
|
||||
BarcodeFormat.ITF.name,
|
||||
BarcodeFormat.MAXICODE.name,
|
||||
BarcodeFormat.PDF_417.name,
|
||||
BarcodeFormat.QR_CODE.name,
|
||||
BarcodeFormat.MICRO_QR_CODE.name,
|
||||
BarcodeFormat.RMQR_CODE.name,
|
||||
BarcodeFormat.UPC_A.name,
|
||||
BarcodeFormat.UPC_E.name,
|
||||
BarcodeFormat.MaxiCode.name,
|
||||
BarcodeFormat.PDF417.name,
|
||||
BarcodeFormat.QRCode.name,
|
||||
BarcodeFormat.MicroQRCode.name,
|
||||
BarcodeFormat.RMQRCode.name,
|
||||
BarcodeFormat.UPCA.name,
|
||||
BarcodeFormat.UPCE.name,
|
||||
)
|
||||
@RequiresApi(Build.VERSION_CODES.HONEYCOMB)
|
||||
set(value) {
|
||||
apply(BARCODE_FORMATS, value)
|
||||
field = value
|
||||
}
|
||||
var cropHandleX = -2 // -2 means set default roi.
|
||||
set(value) {
|
||||
apply(CROP_HANDLE_X, value)
|
||||
field = value
|
||||
}
|
||||
var cropHandleY = -2
|
||||
set(value) {
|
||||
apply(CROP_HANDLE_Y, value)
|
||||
field = value
|
||||
}
|
||||
var cropHandleOrientation = 0
|
||||
set(value) {
|
||||
apply(CROP_HANDLE_ORIENTATION, value)
|
||||
field = value
|
||||
}
|
||||
var showCropHandle = true
|
||||
set(value) {
|
||||
apply(SHOW_CROP_HANDLE, value)
|
||||
field = value
|
||||
}
|
||||
var showCrosshairs = false
|
||||
set(value) {
|
||||
apply(SHOW_CROSSHAIRS, value)
|
||||
field = value
|
||||
}
|
||||
var zoomBySwiping = true
|
||||
set(value) {
|
||||
apply(ZOOM_BY_SWIPING, value)
|
||||
@@ -103,6 +115,11 @@ class Preferences {
|
||||
apply(BEEP_TONE_NAME, value)
|
||||
field = value
|
||||
}
|
||||
var beepStreamName = "stream_music"
|
||||
set(value) {
|
||||
apply(BEEP_STREAM_NAME, value)
|
||||
field = value
|
||||
}
|
||||
var useHistory = false
|
||||
set(value) {
|
||||
apply(USE_HISTORY, value)
|
||||
@@ -113,6 +130,8 @@ class Preferences {
|
||||
apply(IGNORE_DUPLICATES_NAME, value)
|
||||
field = value
|
||||
}
|
||||
var ignoreCodes = mutableListOf(IgnoreCode(DEFAULT_IGNORE_CODE_PATTERN))
|
||||
private set
|
||||
var copyImmediately = false
|
||||
set(value) {
|
||||
apply(COPY_IMMEDIATELY, value)
|
||||
@@ -200,11 +219,6 @@ 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 = false
|
||||
set(value) {
|
||||
apply(FREE_ROTATION, value)
|
||||
@@ -215,33 +229,112 @@ 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, org.json.JSONArray(profiles).toString())
|
||||
}
|
||||
}
|
||||
|
||||
fun loadProfiles() {
|
||||
profiles.clear()
|
||||
org.json.JSONArray(
|
||||
defaultPreferences.getString(PROFILES, "[]")
|
||||
).let {
|
||||
profiles.addAll(Array(it.length()) { i -> it.getString(i) })
|
||||
}
|
||||
}
|
||||
|
||||
fun update() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
|
||||
preferences.getStringSet(BARCODE_FORMATS, barcodeFormats)?.let {
|
||||
barcodeFormats = addFormatsOnUpdate(
|
||||
it,
|
||||
BarcodeFormat.RMQR_CODE,
|
||||
BarcodeFormat.DX_FILM_EDGE
|
||||
)
|
||||
preferences.getStringSet(BARCODE_FORMATS, barcodeFormats)?.let {
|
||||
val migrated = migrateCode39ToCode39Std(migrateBarcodeFormats(it))
|
||||
if (migrated != it) {
|
||||
apply(BARCODE_FORMATS, migrated)
|
||||
}
|
||||
barcodeFormats = addFormatsOnUpdate(
|
||||
migrated,
|
||||
BarcodeFormat.Code39Std,
|
||||
BarcodeFormat.Code39Ext,
|
||||
BarcodeFormat.Code32,
|
||||
BarcodeFormat.PZN,
|
||||
BarcodeFormat.DataBarOmni,
|
||||
BarcodeFormat.DataBarStk,
|
||||
BarcodeFormat.DataBarLtd,
|
||||
BarcodeFormat.DataBarExp,
|
||||
BarcodeFormat.DataBarExpStk,
|
||||
BarcodeFormat.RMQRCode,
|
||||
BarcodeFormat.DXFilmEdge
|
||||
)
|
||||
}
|
||||
|
||||
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
|
||||
)
|
||||
showCrosshairs = preferences.getBoolean(
|
||||
SHOW_CROSSHAIRS,
|
||||
showCrosshairs
|
||||
)
|
||||
zoomBySwiping = preferences.getBoolean(ZOOM_BY_SWIPING, zoomBySwiping)
|
||||
autoRotate = preferences.getBoolean(AUTO_ROTATE, autoRotate)
|
||||
tryHarder = preferences.getBoolean(TRY_HARDER, tryHarder)
|
||||
@@ -259,18 +352,34 @@ class Preferences {
|
||||
preferences.getString(BEEP_TONE_NAME, beepToneName)?.also {
|
||||
beepToneName = it
|
||||
}
|
||||
preferences.getString(BEEP_STREAM_NAME, beepStreamName)?.also {
|
||||
beepStreamName = it
|
||||
}
|
||||
useHistory = preferences.getBoolean(USE_HISTORY, useHistory)
|
||||
|
||||
// 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()
|
||||
preferences.edit {
|
||||
remove(ignoreConsecutiveDuplicates)
|
||||
}
|
||||
ignoreDuplicatesName = "accept_duplicates"
|
||||
}
|
||||
|
||||
preferences.getString(IGNORE_DUPLICATES_NAME, ignoreDuplicatesName)?.also {
|
||||
preferences.getString(
|
||||
IGNORE_DUPLICATES_NAME,
|
||||
ignoreDuplicatesName
|
||||
)?.also {
|
||||
ignoreDuplicatesName = it
|
||||
}
|
||||
ignoreCodes = ignoreCodesFromJsonArray(
|
||||
preferences.getString(
|
||||
IGNORE_CODES,
|
||||
ignoreCodesToJsonArray(
|
||||
listOf(IgnoreCode(DEFAULT_IGNORE_CODE_PATTERN))
|
||||
)
|
||||
) ?: "[]"
|
||||
)
|
||||
copyImmediately = preferences.getBoolean(
|
||||
COPY_IMMEDIATELY,
|
||||
copyImmediately
|
||||
@@ -326,17 +435,38 @@ 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))
|
||||
}
|
||||
|
||||
fun setIgnoreCodes(patterns: List<IgnoreCode>) {
|
||||
ignoreCodes = patterns.toMutableList()
|
||||
apply(IGNORE_CODES, ignoreCodesToJsonArray(patterns))
|
||||
}
|
||||
|
||||
fun shouldIgnoreHistoryContent(content: String, format: String): Boolean =
|
||||
ignoreCodes.any { it.matches(content, format) }
|
||||
|
||||
private fun addFormatsOnUpdate(
|
||||
restored: Set<String>,
|
||||
vararg formats: BarcodeFormat
|
||||
@@ -344,12 +474,45 @@ class Preferences {
|
||||
for (format in formats) {
|
||||
val name = "${format.name}_added"
|
||||
if (!preferences.getBoolean(name, false)) {
|
||||
preferences.edit().putBoolean(name, true).apply()
|
||||
preferences.edit {
|
||||
putBoolean(name, true)
|
||||
}
|
||||
add(format.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun migrateCode39ToCode39Std(
|
||||
restored: Set<String>
|
||||
) = restored.toMutableSet().apply {
|
||||
// Migrate Code39 to the more specific Code39Std.
|
||||
if (contains(BarcodeFormat.Code39.name)) {
|
||||
remove(BarcodeFormat.Code39.name)
|
||||
if (!contains(BarcodeFormat.Code39Std.name)) {
|
||||
add(BarcodeFormat.Code39Std.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun migrateBarcodeFormats(formats: Set<String>): Set<String> {
|
||||
return formats.mapTo(mutableSetOf()) { format ->
|
||||
format.migrateBarcodeFormatName()
|
||||
}
|
||||
}
|
||||
|
||||
fun restoreCropHandle(
|
||||
name: String,
|
||||
viewWidth: Int,
|
||||
viewHeight: Int
|
||||
) = preferences.restoreCropHandle(name, viewWidth, viewHeight)
|
||||
|
||||
fun storeCropHandle(
|
||||
name: String,
|
||||
viewWidth: Int,
|
||||
viewHeight: Int,
|
||||
cropHandle: Point
|
||||
) = preferences.storeCropHandle(name, viewWidth, viewHeight, cropHandle)
|
||||
|
||||
fun beepTone() = when (beepToneName) {
|
||||
"tone_cdma_confirm" -> ToneGenerator.TONE_CDMA_CONFIRM
|
||||
"tone_sup_radio_ack" -> ToneGenerator.TONE_SUP_RADIO_ACK
|
||||
@@ -359,38 +522,52 @@ class Preferences {
|
||||
else -> ToneGenerator.TONE_PROP_BEEP
|
||||
}
|
||||
|
||||
fun beepStream() = when (beepStreamName) {
|
||||
"stream_notification" -> android.media.AudioManager.STREAM_NOTIFICATION
|
||||
else -> android.media.AudioManager.STREAM_MUSIC
|
||||
}
|
||||
|
||||
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()
|
||||
preferences.edit {
|
||||
putBoolean(label, value)
|
||||
}
|
||||
}
|
||||
|
||||
private fun apply(label: String, value: String) {
|
||||
preferences.edit().putString(label, value).apply()
|
||||
preferences.edit {
|
||||
putString(label, value)
|
||||
}
|
||||
}
|
||||
|
||||
private fun apply(label: String, value: Int) {
|
||||
preferences.edit().putInt(label, value).apply()
|
||||
preferences.edit {
|
||||
putInt(label, value)
|
||||
}
|
||||
}
|
||||
|
||||
private fun apply(label: String, value: Float) {
|
||||
preferences.edit {
|
||||
putFloat(label, value)
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("ApplySharedPref")
|
||||
private fun commit(label: String, value: String) {
|
||||
preferences.edit().putString(label, value).commit()
|
||||
preferences.edit(true) {
|
||||
putString(label, value)
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.HONEYCOMB)
|
||||
private fun apply(label: String, value: Set<String>) {
|
||||
preferences.edit().putStringSet(label, value).apply()
|
||||
preferences.edit {
|
||||
putStringSet(label, value)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
@@ -398,11 +575,11 @@ class Preferences {
|
||||
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 SHOW_CROSSHAIRS = "show_crosshairs"
|
||||
private const val ZOOM_BY_SWIPING = "zoom_by_swiping"
|
||||
private const val AUTO_ROTATE = "auto_rotate"
|
||||
private const val TRY_HARDER = "try_harder"
|
||||
@@ -412,8 +589,10 @@ class Preferences {
|
||||
private const val VIBRATE = "vibrate"
|
||||
private const val BEEP = "beep"
|
||||
private const val BEEP_TONE_NAME = "beep_tone_name"
|
||||
private const val BEEP_STREAM_NAME = "beep_stream_name"
|
||||
private const val USE_HISTORY = "use_history"
|
||||
private const val IGNORE_DUPLICATES_NAME = "ignore_duplicates_name"
|
||||
private const val IGNORE_CODES = "ignore_codes"
|
||||
private const val OPEN_IMMEDIATELY = "open_immediately"
|
||||
private const val COPY_IMMEDIATELY = "copy_immediately"
|
||||
private const val SHOW_META_DATA = "show_meta_data"
|
||||
@@ -431,8 +610,12 @@ 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"
|
||||
private const val DEFAULT_IGNORE_CODE_PATTERN = "^FIDO://.*"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
package de.markusfisch.android.binaryeye.preference
|
||||
|
||||
import android.content.Context
|
||||
import android.support.v7.preference.DialogPreference
|
||||
import android.util.AttributeSet
|
||||
import androidx.preference.DialogPreference
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
|
||||
class UrlPreference(
|
||||
context: Context?,
|
||||
context: Context,
|
||||
attrs: AttributeSet?
|
||||
) : DialogPreference(context, attrs) {
|
||||
private var url: String? = null
|
||||
|
||||
@@ -5,7 +5,7 @@ import android.app.PendingIntent
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.service.quicksettings.TileService
|
||||
import android.support.annotation.RequiresApi
|
||||
import androidx.annotation.RequiresApi
|
||||
import de.markusfisch.android.binaryeye.activity.CameraActivity
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.N)
|
||||
|
||||
@@ -70,7 +70,13 @@ fun String.unescape(): String {
|
||||
val sb = StringBuilder()
|
||||
while (i < len) {
|
||||
val ch = get(i++)
|
||||
sb.append(if (ch == '\\' && i < len) expand() else ch)
|
||||
sb.append(
|
||||
if (ch == '\\' && i < len) {
|
||||
expand()
|
||||
} else {
|
||||
ch
|
||||
}
|
||||
)
|
||||
}
|
||||
return sb.toString()
|
||||
}
|
||||
|
||||
@@ -2,15 +2,16 @@ package de.markusfisch.android.binaryeye.view
|
||||
|
||||
import android.content.Context
|
||||
import android.content.ContextWrapper
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
import android.os.Build
|
||||
import android.support.v4.content.ContextCompat
|
||||
import android.support.v7.app.AppCompatActivity
|
||||
import android.support.v7.widget.LinearLayoutManager
|
||||
import android.support.v7.widget.RecyclerView
|
||||
import android.view.View
|
||||
import android.view.WindowManager
|
||||
import android.widget.AbsListView
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.graphics.drawable.toDrawable
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
|
||||
val systemBarListViewScrollListener = object : AbsListView.OnScrollListener {
|
||||
@@ -25,7 +26,8 @@ val systemBarListViewScrollListener = object : AbsListView.OnScrollListener {
|
||||
view.post {
|
||||
val scrolled = firstVisibleItem > 0 ||
|
||||
(totalItemCount > 0 && firstChildScrolled(view))
|
||||
val scrollable = scrolled || totalItemCount > 0 && lastChildOutOfView(view)
|
||||
val scrollable = scrolled ||
|
||||
totalItemCount > 0 && lastChildOutOfView(view)
|
||||
colorSystemAndToolBars(view.context, scrolled, scrollable)
|
||||
}
|
||||
}
|
||||
@@ -37,9 +39,11 @@ val systemBarListViewScrollListener = object : AbsListView.OnScrollListener {
|
||||
val systemBarRecyclerViewScrollListener = object : RecyclerView.OnScrollListener() {
|
||||
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
||||
val layoutManager = recyclerView.layoutManager as LinearLayoutManager
|
||||
val scrolled = layoutManager.findFirstCompletelyVisibleItemPosition() != 0
|
||||
val scrollable = scrolled || layoutManager.findLastVisibleItemPosition() <
|
||||
recyclerView.adapter.itemCount - 1
|
||||
val scrolled =
|
||||
layoutManager.findFirstCompletelyVisibleItemPosition() != 0
|
||||
val scrollable = scrolled ||
|
||||
layoutManager.findLastVisibleItemPosition() <
|
||||
(recyclerView.adapter?.itemCount ?: 0) - 1
|
||||
colorSystemAndToolBars(recyclerView.context, scrolled, scrollable)
|
||||
}
|
||||
|
||||
@@ -57,19 +61,25 @@ private fun lastChildOutOfView(listView: AbsListView): Boolean {
|
||||
return child != null && child.bottom >= listView.height
|
||||
}
|
||||
|
||||
fun initSystemBars(activity: AppCompatActivity) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
// Keeps the soft keyboard from repositioning the layout.
|
||||
val window = activity.window
|
||||
window.setSoftInputMode(
|
||||
WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE
|
||||
)
|
||||
window.decorView.systemUiVisibility =
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_STABLE or
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
|
||||
}
|
||||
colorSystemAndToolBars(activity)
|
||||
fun AppCompatActivity.initBars() {
|
||||
// Keeps the soft keyboard from repositioning the layout.
|
||||
window.setSoftInputMode(
|
||||
WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE
|
||||
)
|
||||
window.decorView.systemUiVisibility =
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_STABLE or
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
|
||||
colorSystemAndToolBars(this)
|
||||
setPaddingFromWindowInsets(
|
||||
findViewById(R.id.main),
|
||||
(findViewById(R.id.toolbar) as Toolbar).apply {
|
||||
setSupportActionBar(this)
|
||||
},
|
||||
findViewById<View>(R.id.navbar)?.apply {
|
||||
setBackgroundColor(translucentPrimaryColor)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private var statusBarColorLocked = false
|
||||
@@ -82,7 +92,6 @@ fun unlockStatusBarColor() {
|
||||
}
|
||||
|
||||
private var translucentPrimaryColor = 0
|
||||
private val actionBarBackground = ColorDrawable()
|
||||
fun colorSystemAndToolBars(
|
||||
context: Context,
|
||||
scrolled: Boolean = false,
|
||||
@@ -96,27 +105,29 @@ fun colorSystemAndToolBars(
|
||||
}
|
||||
val topColor = if (scrolled) translucentPrimaryColor else 0
|
||||
val activity = getAppCompatActivity(context) ?: return
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
// System bars no longer have a background from SDK35+.
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.VANILLA_ICE_CREAM) {
|
||||
val window = activity.window
|
||||
if (!statusBarColorLocked) {
|
||||
@Suppress("DEPRECATION")
|
||||
window.statusBarColor = topColor
|
||||
}
|
||||
@Suppress("DEPRECATION")
|
||||
window.navigationBarColor = if (scrolled || scrollable) {
|
||||
translucentPrimaryColor
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
activity.supportActionBar?.setBackgroundDrawable(
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
|
||||
// Avoid allocation on Honeycomb and better.
|
||||
actionBarBackground.color = topColor
|
||||
actionBarBackground
|
||||
} else {
|
||||
// ColorDrawable.setColor() doesn't exist pre Honeycomb.
|
||||
ColorDrawable(topColor)
|
||||
} else {
|
||||
activity.findViewById<View>(R.id.navbar)?.apply {
|
||||
visibility = if (scrolled || scrollable) {
|
||||
View.VISIBLE
|
||||
} else {
|
||||
View.GONE
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
activity.supportActionBar?.setBackgroundDrawable(topColor.toDrawable())
|
||||
}
|
||||
|
||||
private fun getAppCompatActivity(context: Context): AppCompatActivity? {
|
||||
|
||||
@@ -2,15 +2,45 @@ package de.markusfisch.android.binaryeye.view
|
||||
|
||||
import android.graphics.Rect
|
||||
import android.os.Build
|
||||
import android.support.annotation.RequiresApi
|
||||
import android.support.v4.view.ViewCompat
|
||||
import android.support.v4.view.WindowInsetsCompat
|
||||
import android.support.v7.widget.Toolbar
|
||||
import android.view.View
|
||||
import android.view.WindowInsets
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
|
||||
private var toolbarHeight = 0
|
||||
fun recordToolbarHeight(toolbar: Toolbar) {
|
||||
fun setPaddingFromWindowInsets(
|
||||
mainLayout: View,
|
||||
toolbar: Toolbar,
|
||||
navbar: View?
|
||||
) {
|
||||
toolbarHeight = toolbar.layoutParams.height
|
||||
// SDK 35+ is edge-to-edge by default, so the toolbar needs to extend
|
||||
// below the status bar.
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.VANILLA_ICE_CREAM) {
|
||||
return
|
||||
}
|
||||
mainLayout.setOnApplyWindowInsetsListener { _, insets ->
|
||||
val systemBarInsets = insets.getInsets(
|
||||
WindowInsets.Type.systemBars()
|
||||
)
|
||||
|
||||
val statusBarTop = systemBarInsets.top
|
||||
if (toolbar.paddingTop == 0 && statusBarTop > 0) {
|
||||
toolbar.setPadding(0, statusBarTop, 0, 0)
|
||||
toolbar.layoutParams.height += statusBarTop
|
||||
}
|
||||
|
||||
val navBarBottom = systemBarInsets.bottom
|
||||
if (navBarBottom > 0) {
|
||||
mainLayout.setPadding(0, 0, 0, navBarBottom);
|
||||
navbar?.apply {
|
||||
layoutParams.height = navBarBottom
|
||||
}
|
||||
}
|
||||
|
||||
insets
|
||||
}
|
||||
}
|
||||
|
||||
fun View.setPaddingFromWindowInsets() {
|
||||
@@ -22,19 +52,15 @@ fun View.setPaddingFromWindowInsets() {
|
||||
// For the original post see here:
|
||||
// https://medium.com/androiddevelopers/windowinsets-listeners-to-layouts-8f9ccc8fa4d1
|
||||
fun View.doOnApplyWindowInsets(f: (View, Rect) -> Unit) {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
|
||||
f(this, insetsWithToolbar())
|
||||
} else {
|
||||
ViewCompat.setOnApplyWindowInsetsListener(this) { v, insets ->
|
||||
f(v, insetsWithToolbar(insets))
|
||||
insets
|
||||
}
|
||||
// It's important to explicitly request the insets (again) in
|
||||
// case the view was created in Fragment.onCreateView() because
|
||||
// setOnApplyWindowInsetsListener() won't fire when the view
|
||||
// isn't attached.
|
||||
requestApplyInsetsWhenAttached()
|
||||
ViewCompat.setOnApplyWindowInsetsListener(this) { v, insets ->
|
||||
f(v, insetsWithToolbar(insets))
|
||||
insets
|
||||
}
|
||||
// It's important to explicitly request the insets (again) in
|
||||
// case the view was created in Fragment.onCreateView() because
|
||||
// setOnApplyWindowInsetsListener() won't fire when the view
|
||||
// isn't attached.
|
||||
requestApplyInsetsWhenAttached()
|
||||
}
|
||||
|
||||
private fun insetsWithToolbar(insets: WindowInsetsCompat? = null) = Rect(
|
||||
@@ -44,7 +70,6 @@ private fun insetsWithToolbar(insets: WindowInsetsCompat? = null) = Rect(
|
||||
insets?.systemWindowInsetBottom ?: 0
|
||||
)
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.KITKAT)
|
||||
private fun View.requestApplyInsetsWhenAttached() {
|
||||
if (isAttachedToWindow) {
|
||||
ViewCompat.requestApplyInsets(this)
|
||||
|
||||
+5
@@ -10,6 +10,8 @@ import de.markusfisch.android.scalingimageview.widget.ScalingImageView
|
||||
open class ConfinedScalingImageView : ScalingImageView {
|
||||
val insets = Rect()
|
||||
|
||||
var runAfterLayout: (() -> Unit)? = null
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet, defStyle: Int) :
|
||||
super(context, attrs, defStyle)
|
||||
|
||||
@@ -37,5 +39,8 @@ open class ConfinedScalingImageView : ScalingImageView {
|
||||
)
|
||||
}
|
||||
centerRemap()
|
||||
runAfterLayout?.invoke()
|
||||
}
|
||||
|
||||
fun isInBounds(): Boolean = inBounds()
|
||||
}
|
||||
|
||||
@@ -27,7 +27,8 @@ class ConfinedScrollView : ScrollView {
|
||||
// not putting it on the queue makes it only work sometimes.
|
||||
post {
|
||||
getChildAt(0)?.also { child ->
|
||||
scrollable = height < child.height + paddingTop + paddingBottom
|
||||
scrollable = height <
|
||||
child.height + paddingTop + paddingBottom
|
||||
colorSystemAndToolBars(
|
||||
context,
|
||||
scrollY > 0,
|
||||
|
||||
@@ -9,16 +9,14 @@ import android.graphics.Point
|
||||
import android.graphics.Rect
|
||||
import android.graphics.Region
|
||||
import android.os.Build
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
import android.support.v4.content.ContextCompat
|
||||
import android.util.AttributeSet
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.graphics.withSave
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.app.prefs
|
||||
import de.markusfisch.android.binaryeye.graphics.getBitmapFromDrawable
|
||||
import de.markusfisch.android.binaryeye.graphics.getDashedBorderPaint
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
@@ -28,26 +26,31 @@ class DetectorView : View {
|
||||
val coordinates = FloatArray(32)
|
||||
val roi = Rect()
|
||||
|
||||
var cropHandleName: String? = null
|
||||
var onRoiChange: (() -> Unit)? = null
|
||||
var onRoiChanged: (() -> Unit)? = null
|
||||
|
||||
private val currentOrientation = resources.configuration.orientation
|
||||
private val invalidateRunnable: Runnable = Runnable {
|
||||
coordinatesLast = 0
|
||||
invalidate()
|
||||
}
|
||||
private val roiPaint = context.getDashedBorderPaint()
|
||||
private val dotPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
color = ContextCompat.getColor(context, R.color.dot)
|
||||
style = Paint.Style.FILL
|
||||
}
|
||||
private val crosshairPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
color = ContextCompat.getColor(context, R.color.dot)
|
||||
style = Paint.Style.STROKE
|
||||
strokeWidth = context.resources.displayMetrics.density
|
||||
}
|
||||
private val dotRadius: Float
|
||||
private val handleBitmap = resources.getBitmapFromDrawable(
|
||||
R.drawable.button_crop
|
||||
)
|
||||
private val handleXRadius = handleBitmap.width / 2
|
||||
private val handleYRadius = handleBitmap.height / 2
|
||||
private val handlePos = Point(-1, -1)
|
||||
private val inactiveHandlePos = Point(-1, -1)
|
||||
private val handlePos = Point(inactiveHandlePos)
|
||||
private val handleHome = Point()
|
||||
private val center = Point()
|
||||
private val touchDown = Point()
|
||||
@@ -57,6 +60,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
|
||||
@@ -83,37 +88,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) {
|
||||
@@ -123,31 +129,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)
|
||||
@@ -244,40 +225,48 @@ 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) {
|
||||
if (handleActive) {
|
||||
canvas.drawClip()
|
||||
}
|
||||
if (prefs.showCrosshairs) {
|
||||
canvas.drawCrosshairs()
|
||||
}
|
||||
canvas.drawDots()
|
||||
if (prefs.showCropHandle) {
|
||||
canvas.drawHandle()
|
||||
}
|
||||
}
|
||||
|
||||
fun getHorizontalCrosshairY() = if (handleActive) {
|
||||
(roi.top + roi.bottom) * .5f
|
||||
} else {
|
||||
viewHeight * .5f
|
||||
}
|
||||
|
||||
private fun Canvas.drawDots() {
|
||||
var i = 0
|
||||
while (i < coordinatesLast) {
|
||||
@@ -294,10 +283,8 @@ class DetectorView : View {
|
||||
if (minDist < 1) {
|
||||
return
|
||||
}
|
||||
// canvas.clipRect() doesn't work reliably below KITKAT.
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||
val radius = min(minDist / 2, cornerRadius).toFloat()
|
||||
save()
|
||||
val radius = min(minDist / 2, cornerRadius).toFloat()
|
||||
withSave {
|
||||
clipOutPathCompat(
|
||||
calculateRoundedRectPath(
|
||||
roi.left.toFloat(),
|
||||
@@ -309,9 +296,6 @@ class DetectorView : View {
|
||||
)
|
||||
)
|
||||
drawColor(shadeColor)
|
||||
restore()
|
||||
} else {
|
||||
drawRect(roi, roiPaint)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -324,6 +308,28 @@ class DetectorView : View {
|
||||
)
|
||||
}
|
||||
|
||||
private fun Canvas.drawCrosshairs() {
|
||||
val left: Float
|
||||
val top: Float
|
||||
val right: Float
|
||||
val bottom: Float
|
||||
if (handleActive) {
|
||||
left = roi.left.toFloat()
|
||||
top = roi.top.toFloat()
|
||||
right = roi.right.toFloat()
|
||||
bottom = roi.bottom.toFloat()
|
||||
} else {
|
||||
left = 0f
|
||||
top = 0f
|
||||
right = viewWidth.toFloat()
|
||||
bottom = viewHeight.toFloat()
|
||||
}
|
||||
val cx = (left + right) * .5f
|
||||
val cy = (top + bottom) * .5f
|
||||
drawLine(cx, top, cx, bottom, crosshairPaint)
|
||||
drawLine(left, cy, right, cy, crosshairPaint)
|
||||
}
|
||||
|
||||
private fun updateClipRect() {
|
||||
clampHandlePos()
|
||||
val dx = abs(handlePos.x - center.x)
|
||||
@@ -349,37 +355,6 @@ class DetectorView : View {
|
||||
private fun clampX(x: Int) = min(center.x * 2, max(0, x))
|
||||
|
||||
private fun clampY(y: Int) = min(maxY, max(minY, y))
|
||||
|
||||
internal class SavedState : BaseSavedState {
|
||||
val savedHandlePos = Point()
|
||||
|
||||
var savedOrientation = 0
|
||||
|
||||
constructor(superState: Parcelable?) : super(superState)
|
||||
|
||||
private constructor(parcel: Parcel) : super(parcel) {
|
||||
savedHandlePos.set(
|
||||
parcel.readInt(),
|
||||
parcel.readInt()
|
||||
)
|
||||
savedOrientation = parcel.readInt()
|
||||
}
|
||||
|
||||
override fun writeToParcel(out: Parcel, flags: Int) {
|
||||
super.writeToParcel(out, flags)
|
||||
out.writeInt(savedHandlePos.x)
|
||||
out.writeInt(savedHandlePos.y)
|
||||
out.writeInt(savedOrientation)
|
||||
}
|
||||
|
||||
companion object {
|
||||
@JvmField
|
||||
val CREATOR = object : Parcelable.Creator<SavedState> {
|
||||
override fun createFromParcel(source: Parcel) = SavedState(source)
|
||||
override fun newArray(size: Int): Array<SavedState?> = arrayOfNulls(size)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun distSq(a: Point, b: Point): Int {
|
||||
|
||||
@@ -3,19 +3,29 @@ package de.markusfisch.android.binaryeye.widget
|
||||
import android.content.Context
|
||||
import android.widget.Toast
|
||||
|
||||
fun Context.toast(message: Int) = Toast.makeText(
|
||||
applicationContext,
|
||||
message,
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
private var lastToast: Toast? = null
|
||||
|
||||
fun Context.toast(message: String) = Toast.makeText(
|
||||
applicationContext,
|
||||
message.ellipsize(128),
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
fun <T> Context.toast(message: T) {
|
||||
lastToast?.cancel()
|
||||
lastToast = when (message) {
|
||||
is String -> Toast.makeText(
|
||||
applicationContext,
|
||||
message.ellipsize(),
|
||||
Toast.LENGTH_LONG
|
||||
)
|
||||
|
||||
private fun String.ellipsize(max: Int) = if (length < max) {
|
||||
is Int -> Toast.makeText(
|
||||
applicationContext,
|
||||
message,
|
||||
Toast.LENGTH_LONG
|
||||
)
|
||||
|
||||
else -> return
|
||||
}
|
||||
lastToast?.show()
|
||||
}
|
||||
|
||||
private fun String.ellipsize(max: Int = 128) = if (length < max) {
|
||||
this
|
||||
} else {
|
||||
"${take(max)}…"
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
package de.markusfisch.android.binaryeye.zxingcpp
|
||||
|
||||
val oldToNewFormatNames = mapOf(
|
||||
"NONE" to "None",
|
||||
"AZTEC" to "Aztec",
|
||||
"CODABAR" to "Codabar",
|
||||
"CODE_39" to "Code39",
|
||||
"CODE_39_EXT" to "Code39Ext",
|
||||
"CODE_32" to "Code32",
|
||||
"PZN" to "PZN",
|
||||
"CODE_93" to "Code93",
|
||||
"CODE_128" to "Code128",
|
||||
"DATA_BAR_OMNI" to "DataBarOmni",
|
||||
"DATA_BAR_STK" to "DataBarStk",
|
||||
"DATA_BAR_LTD" to "DataBarLtd",
|
||||
"DATA_BAR_EXPANDED" to "DataBarExp",
|
||||
"DATA_BAR_EXP_STK" to "DataBarExpStk",
|
||||
"DATA_MATRIX" to "DataMatrix",
|
||||
"DX_FILM_EDGE" to "DXFilmEdge",
|
||||
"EAN_8" to "EAN8",
|
||||
"EAN_13" to "EAN13",
|
||||
"ITF" to "ITF",
|
||||
"MAXICODE" to "MaxiCode",
|
||||
"PDF_417" to "PDF417",
|
||||
"QR_CODE" to "QRCode",
|
||||
"MICRO_QR_CODE" to "MicroQRCode",
|
||||
"RMQR_CODE" to "RMQRCode",
|
||||
"UPC_A" to "UPCA",
|
||||
"UPC_E" to "UPCE"
|
||||
)
|
||||
|
||||
/**
|
||||
* Translate legacy barcode format names from ZXingC++ before 3.x
|
||||
* to current BarcodeFormat enum names.
|
||||
*/
|
||||
fun String.migrateBarcodeFormatName() = oldToNewFormatNames[this] ?: this
|
||||
@@ -0,0 +1,16 @@
|
||||
<selector
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:constantSize="true">
|
||||
<item android:state_pressed="false">
|
||||
<shape android:shape="rectangle">
|
||||
<corners android:radius="2dp"/>
|
||||
<solid android:color="@color/selected_row"/>
|
||||
</shape>
|
||||
</item>
|
||||
<item android:state_pressed="true">
|
||||
<shape android:shape="rectangle">
|
||||
<corners android:radius="2dp"/>
|
||||
<solid android:color="@color/pressed_row"/>
|
||||
</shape>
|
||||
</item>
|
||||
</selector>
|
||||
@@ -0,0 +1,3 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:autoMirrored="true" android:height="24dp" android:tint="#FFFFFF" android:viewportHeight="960" android:viewportWidth="960" android:width="24dp">
|
||||
<path android:fillColor="@android:color/white" android:pathData="M313 520l224 224-57 56-320-320 320-320 57 56-224 224h487v80H313z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,3 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#FFFFFF" android:viewportHeight="960" android:viewportWidth="960" android:width="24dp">
|
||||
<path android:fillColor="@android:color/white" android:pathData="M480 340q13 0 21.5-9t8.5-21q0-13-8.5-21.5T480 280q-12 0-21 8.5t-9 21.5q0 12 9 21t21 9zM172 595q-15-26-23.5-54.5T140 480q0-32 8.5-60.5T172 365l64 50q-8 15-12 31t-4 34q0 18 4 34t12 31l-64 50zM62 681q-29-44-45.5-94.5T0 480q0-56 16.5-106.5T62 279l63 50q-21 33-33 71t-12 80q0 42 12 80.5t33 71.5l-63 49zm298 119q-33 0-56.5-23.5T280 720V240q0-33 23.5-56.5T360 160h240q33 0 56.5 23.5T680 240v480q0 33-23.5 56.5T600 800H360zm0-80h240V240H360v480zm427-125l-64-50q8-15 12.5-31t4.5-34q0-18-4.5-34T723 415l64-50q15 26 24 54.5t9 60.5q0 32-9 60.5T787 595zm111 85l-63-49q21-33 33-71t12-80q0-42-12-80.5T835 328l63-49q29 43 45.5 93.5T960 479q0 56-16.5 106.5T898 680zm-538 40V240v480z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,3 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#FFFFFF" android:viewportHeight="960" android:viewportWidth="960" android:width="24dp">
|
||||
<path android:fillColor="@android:color/white" android:pathData="M440 880V576L256 760l-56-56 224-224-224-224 56-56 184 184V80h40l228 228-172 172 172 172-228 228h-40zm80-496l76-76-76-74v150zm0 342l76-74-76-76v150z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,3 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#FFFFFF" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
|
||||
<path android:fillColor="#fff" android:pathData="M20 8.69V4h-4.69L12 0.69 8.69 4H4v4.69L0.69 12 4 15.31V20h4.69L12 23.31 15.31 20H20v-4.69L23.31 12 20 8.69zM12 18c-3.31 0-6-2.69-6-6s2.69-6 6-6 6 2.69 6 6-2.69 6-6 6zm0-10c-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4-1.79-4-4-4z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,3 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#FFFFFF" android:viewportHeight="960" android:viewportWidth="960" android:width="24dp">
|
||||
<path android:fillColor="@android:color/white" android:pathData="M680 920V760H280q-33 0-56.5-23.5T200 680V280H40v-80h160V40h80v640h640v80H760v160h-80zm0-320V280H360v-80h320q33 0 56.5 23.5T760 280v320h-80z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,3 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#FFFFFF" android:viewportHeight="960" android:viewportWidth="960" android:width="24dp">
|
||||
<path android:fillColor="@android:color/white" android:pathData="M340.5 811.5Q275 783 226 734t-77.5-114.5Q120 554 120 480q0-44 10-85.5t29-78q19-36.5 45.5-68T264 192l272 272-56 56-216-216q-30 36-47 80.5T200 480q0 116 82 198t198 82q116 0 198-82t82-198q0-107-68.5-184.5T520 204v76h-80V120h40q74 0 139.5 28.5T734 226q49 49 77.5 114.5T840 480q0 74-28.5 139.5T734 734q-49 49-114.5 77.5T480 840q-74 0-139.5-28.5zm-89-303Q240 497 240 480t11.5-28.5Q263 440 280 440t28.5 11.5Q320 463 320 480t-11.5 28.5Q297 520 280 520t-28.5-11.5zm200 200Q440 697 440 680t11.5-28.5Q463 640 480 640t28.5 11.5Q520 663 520 680t-11.5 28.5Q497 720 480 720t-28.5-11.5zm200-200Q640 497 640 480t11.5-28.5Q663 440 680 440t28.5 11.5Q720 463 720 480t-11.5 28.5Q697 520 680 520t-28.5-11.5z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,3 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#FFFFFF" android:viewportHeight="960" android:viewportWidth="960" android:width="24dp">
|
||||
<path android:fillColor="@android:color/white" android:pathData="M440 160v487L216 423l-56 57 320 320 320-320-56-57-224 224V160h-80z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,3 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:autoMirrored="true" android:height="24dp" android:tint="#FFFFFF" android:viewportHeight="960" android:viewportWidth="960" android:width="24dp">
|
||||
<path android:fillColor="@android:color/white" android:pathData="M640 680l-57-56 184-184-184-184 57-56 240 240-240 240zM80 760V600q0-83 58.5-141.5T280 400h247L383 256l57-56 240 240-240 240-57-56 144-144H280q-50 0-85 35t-35 85v160H80z"/>
|
||||
</vector>
|
||||
@@ -1,3 +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.49L14.54,16.51L17.51,16.51L17.51,13.54L19.49,13.54L19.49,16.51L22.46,16.51L22.46,18.49L19.508,18.508L19.49,21.46L17.51,21.46L17.492,18.508L14.54,18.49ZM13.35,14.75L11,12.4L11,7L13,7L13,11.6L14.4,13L13.35,14.75ZM3,10L3,4L5,4L5,6.35C5.85,5.283 6.888,4.458 8.113,3.875C9.338,3.292 10.633,3 12,3C14.5,3 16.625,3.875 18.375,5.625C20.125,7.375 21,9.5 21,12L19,12C19,10.05 18.321,8.396 16.963,7.037C15.604,5.679 13.95,5 12,5C10.85,5 9.775,5.267 8.775,5.8C7.775,6.333 6.933,7.067 6.25,8L9,8L9,10L3,10ZM12,21C9.7,21 7.696,20.237 5.988,18.712C4.279,17.187 3.3,15.283 3.05,13L5.1,13C5.333,14.733 6.104,16.167 7.413,17.3C8.721,18.433 10.25,19 12,19C12.183,19 12.354,18.996 12.513,18.987C12.671,18.979 12.833,18.958 13,18.925L13,20.95C12.833,20.967 12.671,20.979 12.513,20.987C12.354,20.996 12.183,21 12,21Z"/>
|
||||
<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.1h4V7H7c-2.76 0-5 2.24-5 5s2.24 5 5 5h4v-1.9H7c-1.71 0-3.1-1.39-3.1-3.1zM8 13h8v-2H8v2zm9-6h-4v1.9h4c1.71 0 3.1 1.39 3.1 3.1s-1.39 3.1-3.1 3.1h-4V17h4c2.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="@android:color/white" android:pathData="M501 460zm-21 420q-33 0-56.5-23.5T400 800h160q0 33-23.5 56.5T480 880zM160 760v-80h80V400q0-83 50-147.5T420 168v-28q0-25 17.5-42.5T480 80q25 0 42.5 17.5T540 140v28q29 7 53.5 20.5T639 220l-57 57q-21-17-47-27t-55-10q-66 0-113 47t-47 113v280h480v80H160zm496-219l-56-57q10-10 15-24t5-32q0-18-5-32.5T600 371l56-56q21 21 32.5 50t11.5 63q0 34-11.5 63T656 541zm43 43q29-29 45-70t16-86q0-45-16-86t-45-70l57-56q41 41 62.5 95.5T840 428q0 62-21.5 116.5T756 640l-57-56z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,3 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24">
|
||||
<path android:fillColor="#ffffffff" android:pathData="M10 4h4c1.1 0 2 0.9 2 2v3l3 3v3h-6v7l-1 1-1-1v-7H5v-3l3-3V6c0-1.1 0.9-2 2-2z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,3 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24">
|
||||
<path android:fillColor="#ffffffff" android:pathData="M10 6h4v3.83l3 3.34V13H7v0.17l3-3.34V6zm0-2C8.9 4 8 4.9 8 6v3l-3 3v3h6v7l1 1 1-1v-7h6v-3l-3-3V6c0-1.1-0.9-2-2-2h-4z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,3 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#FFFFFF" android:viewportHeight="960" android:viewportWidth="960" android:width="24dp">
|
||||
<path android:fillColor="#fff" android:pathData="M480 480q-66 0-113-47t-47-113q0-66 47-113t113-47q66 0 113 47t47 113q0 66-47 113t-113 47zM160 800V688q0-34 17.5-62.5T224 582q62-31 126-46.5T480 520q66 0 130 15.5T736 582q29 15 46.5 43.5T800 688v112H160zm80-80h480v-32q0-11-5.5-20T700 654q-54-27-109-40.5T480 600q-56 0-111 13.5T260 654q-9 5-14.5 14t-5.5 20v32zm240-320q33 0 56.5-23.5T560 320q0-33-23.5-56.5T480 240q-33 0-56.5 23.5T400 320q0 33 23.5 56.5T480 400zm0-80zm0 400z"/>
|
||||
</vector>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user