Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
565643f19e | ||
|
|
4c92045563 | ||
|
|
7116aa7278 | ||
|
|
860e867ad4 | ||
|
|
3b509cd09c | ||
|
|
51ec228fd8 | ||
|
|
9d1dec146d | ||
|
|
7bd9fe36fe | ||
|
|
85dba0604c | ||
|
|
56c70f5974 | ||
|
|
098b7740a0 | ||
|
|
7d92481fa6 | ||
|
|
2e285a5b1d | ||
|
|
ad25c4681f | ||
|
|
c00c87ceca | ||
|
|
e001a92870 | ||
|
|
7a32b00e1f | ||
|
|
50cedf782d | ||
|
|
fb0d22198f | ||
|
|
af528df448 | ||
|
|
a9b5771d57 | ||
|
|
a161e8fc0f | ||
|
|
ec17d07695 | ||
|
|
be2fc2f559 | ||
|
|
de3aa752ff | ||
|
|
411cbb243f | ||
|
|
f76c1521fe | ||
|
|
9b9224b100 | ||
|
|
62d3fd7e94 | ||
|
|
acdac4ca30 | ||
|
|
221cf11576 | ||
|
|
d34fceb48f | ||
|
|
4cc4cef4ca | ||
|
|
969ffd3d0a | ||
|
|
81a505c752 | ||
|
|
eafcf0dba6 | ||
|
|
0539468d3d | ||
|
|
5e9361ac77 | ||
|
|
54fb31d6a5 | ||
|
|
0f7da91e77 | ||
|
|
13bbb3e435 | ||
|
|
e318385032 | ||
|
|
9b1445efb5 | ||
|
|
9a74098f11 | ||
|
|
e22d242248 | ||
|
|
77d419aa7b | ||
|
|
b2fbdceee4 | ||
|
|
8a625dc5d0 | ||
|
|
6d9c6aa53f | ||
|
|
6b03935b9a | ||
|
|
fcac0add8b | ||
|
|
f63b78cc28 | ||
|
|
f34194c5b5 | ||
|
|
f2a22d2312 | ||
|
|
746bcc0706 | ||
|
|
2525b693c7 | ||
|
|
ba9970eb4d | ||
|
|
0495985518 | ||
|
|
b662ba9307 | ||
|
|
c4fe713b6d | ||
|
|
3609c377d2 | ||
|
|
8043ab7eff | ||
|
|
4f7b392611 | ||
|
|
27fb611f4e | ||
|
|
3c693c7bd5 | ||
|
|
b2cc1c3c34 | ||
|
|
e2effb613f | ||
|
|
ae422d9a02 | ||
|
|
ace841814a | ||
|
|
21c3206464 | ||
|
|
f392baca8b | ||
|
|
4cd0fb6418 | ||
|
|
5b948a4c8f | ||
|
|
a75aff2e64 | ||
|
|
a49a49f1d2 | ||
|
|
6db46f1d0d | ||
|
|
1c0062df8b | ||
|
|
d776688d26 | ||
|
|
9856aa9efa | ||
|
|
27ce6cb12b | ||
|
|
9d3a2b8479 | ||
|
|
74f0c050c0 | ||
|
|
af92d1db17 | ||
|
|
307401087e | ||
|
|
a36fd668a9 | ||
|
|
7e022249aa | ||
|
|
d464cc26b0 | ||
|
|
45eb24036f | ||
|
|
ea0212c58e | ||
|
|
170a9944ef | ||
|
|
ccfd356c67 | ||
|
|
91aa1d343e | ||
|
|
b64b3c5c01 | ||
|
|
dfb6c07930 | ||
|
|
6ca93f2502 | ||
|
|
e867651acd | ||
|
|
607e1324ed | ||
|
|
256183b4aa | ||
|
|
733b6bb123 | ||
|
|
27a382f2cb | ||
|
|
b0eb601e5a | ||
|
|
3a3e743575 | ||
|
|
f08efb8238 | ||
|
|
723362dd67 | ||
|
|
dde039a299 | ||
|
|
f2a056c1d0 | ||
|
|
0defe263ad | ||
|
|
9d8d33de06 | ||
|
|
738b4d0f7b | ||
|
|
80378afa84 | ||
|
|
ec07e10208 | ||
|
|
2a6cd169aa | ||
|
|
051454988a | ||
|
|
a388d7cbec | ||
|
|
fa45d9e088 | ||
|
|
41e538fa97 | ||
|
|
1753438288 | ||
|
|
4135dfbd3f | ||
|
|
4de7d5d5d7 | ||
|
|
c4856f97ef | ||
|
|
aa885b8379 | ||
|
|
eab9639242 | ||
|
|
a199d82539 | ||
|
|
469fc2ff5f | ||
|
|
b4261d8571 | ||
|
|
725817151d | ||
|
|
e434300eda | ||
|
|
6b08c422cb | ||
|
|
4bfa7b3582 | ||
|
|
d6f902ae02 | ||
|
|
94023b0671 | ||
|
|
16a9428bc1 | ||
|
|
3db5891e0f | ||
|
|
d03fd72b95 | ||
|
|
97640c2a1f | ||
|
|
3bba6cf10d | ||
|
|
aca97afc78 | ||
|
|
3b46da5571 | ||
|
|
df62881ee6 | ||
|
|
966eea4f3f | ||
|
|
eb2ed05b6d | ||
|
|
f31c1eee0e | ||
|
|
c61db34d27 | ||
|
|
3d85cef8a0 | ||
|
|
276fcccc46 | ||
|
|
caa075f422 | ||
|
|
cc629314ca | ||
|
|
c9242c3475 | ||
|
|
3a87416cdb | ||
|
|
4c4f60bddc | ||
|
|
557652b18e | ||
|
|
a1094626c4 | ||
|
|
e45dd7ff18 | ||
|
|
1c1625b6fd | ||
|
|
9c4cb0daad | ||
|
|
024dd379e8 | ||
|
|
16c4957c79 | ||
|
|
01741b83cd | ||
|
|
4200b3274a | ||
|
|
eabd14eb60 | ||
|
|
778c087930 | ||
|
|
cd646a2e90 | ||
|
|
be6576ac74 | ||
|
|
6460500a4d | ||
|
|
02474dfad2 | ||
|
|
d78fd109ff | ||
|
|
85e3f49270 | ||
|
|
59d40b944b | ||
|
|
1e82e8bd53 | ||
|
|
ab5e9b6e9b | ||
|
|
7336d8d12b | ||
|
|
1c9dd73d9a | ||
|
|
5892002a89 | ||
|
|
540290e97c | ||
|
|
f315916fb8 | ||
|
|
e75bb48360 | ||
|
|
2b1d711168 | ||
|
|
2af5a89430 | ||
|
|
b640fa1372 | ||
|
|
8e24673d08 | ||
|
|
ae54e62ec3 | ||
|
|
420a4cbad3 | ||
|
|
05ec77da27 | ||
|
|
c03abf1148 | ||
|
|
b929df4848 | ||
|
|
ec729b6624 | ||
|
|
67471ee141 | ||
|
|
6999f0a863 | ||
|
|
6574a5d651 | ||
|
|
1639e73738 | ||
|
|
68fa67f54f | ||
|
|
326c34b6c1 | ||
|
|
a43dca3644 | ||
|
|
168ccf7d10 | ||
|
|
a9630b4d53 | ||
|
|
4a138bd425 | ||
|
|
339ff59270 | ||
|
|
d208ec12bc | ||
|
|
893ddefa71 | ||
|
|
5d56365e79 | ||
|
|
3237467503 | ||
|
|
346529469c | ||
|
|
9f4e420dbb | ||
|
|
cf4020c613 | ||
|
|
93edde7ed1 | ||
|
|
362e462c2a | ||
|
|
a7241bc0fb | ||
|
|
f26046f34b | ||
|
|
a70813ea5c | ||
|
|
3f251fdb66 | ||
|
|
b76cf329dd | ||
|
|
e0ad177a62 | ||
|
|
1223bd3679 | ||
|
|
b4e24fa328 | ||
|
|
0368ab5f68 | ||
|
|
c44ee8f063 | ||
|
|
3bfa6345a0 | ||
|
|
77d87ba995 | ||
|
|
a2ac68028f | ||
|
|
aeb945f145 | ||
|
|
aadde397ea | ||
|
|
063fea913e | ||
|
|
9d57889b81 | ||
|
|
233b69623f | ||
|
|
c7d6e60efb | ||
|
|
63688a9994 | ||
|
|
dc74e33b8b | ||
|
|
81f2620a69 | ||
|
|
1c95550f5d | ||
|
|
37a821818d | ||
|
|
29e0bdfbc3 | ||
|
|
d4bbcef011 | ||
|
|
81fc41ea81 | ||
|
|
8cadf42c86 | ||
|
|
e29ec2cc40 | ||
|
|
05952e7f7f | ||
|
|
0e8abb4607 | ||
|
|
cede9186a2 | ||
|
|
c1c9fa050b | ||
|
|
76d86288cc | ||
|
|
c0902f8830 | ||
|
|
1d3546d676 | ||
|
|
58baab6ffd | ||
|
|
3bafa60d59 | ||
|
|
825062ba78 | ||
|
|
d128e3cc24 | ||
|
|
83619834a8 | ||
|
|
cb16220ebb | ||
|
|
7f3af23773 | ||
|
|
9898a14dd3 | ||
|
|
85fc475118 | ||
|
|
987e8531e6 | ||
|
|
4d9a537bef | ||
|
|
05de15488e | ||
|
|
721a3ee801 | ||
|
|
bca000c21b | ||
|
|
80afd0c5b7 | ||
|
|
7416037762 | ||
|
|
325aa00bf8 | ||
|
|
40c2cc9b60 | ||
|
|
1bf16a2fb7 | ||
|
|
2687ce031a | ||
|
|
3213169ea1 | ||
|
|
5c3d6c4b31 | ||
|
|
336f596d08 | ||
|
|
930656fbe5 | ||
|
|
3b45061974 | ||
|
|
983c3e3c38 | ||
|
|
691fe05322 | ||
|
|
569389bb12 | ||
|
|
da886291cf | ||
|
|
05b219b3ea | ||
|
|
7cfefe7791 | ||
|
|
a2fe94285d | ||
|
|
6429a1ba52 | ||
|
|
477ae25fc7 | ||
|
|
a2646ffdb3 | ||
|
|
7a93172876 | ||
|
|
327cbe55d7 | ||
|
|
35ec26868c | ||
|
|
0c7317f09e | ||
|
|
a049db588b | ||
|
|
840691451a | ||
|
|
b50c956a21 | ||
|
|
3820d647e0 | ||
|
|
ce234381c8 | ||
|
|
2abeccdd1b | ||
|
|
ae4d6f8e2a | ||
|
|
78c3253979 | ||
|
|
8026478052 | ||
|
|
0d2ac7751e | ||
|
|
47cb298317 | ||
|
|
c1d0b98990 | ||
|
|
798c10d420 | ||
|
|
0d65f41bb5 | ||
|
|
eb32af9cda | ||
|
|
cb82374712 | ||
|
|
c4f7189435 | ||
|
|
2c037a6322 | ||
|
|
6b9e6ee5b4 | ||
|
|
adecdf7e27 | ||
|
|
d925e136ef | ||
|
|
3ae7e561c4 | ||
|
|
ca619efa14 | ||
|
|
47d5923a04 | ||
|
|
8c22a8ab00 | ||
|
|
8152cb6a24 | ||
|
|
ffed4fc5cf | ||
|
|
d8bd3c1176 | ||
|
|
3b12aa4b7b | ||
|
|
099b522d35 | ||
|
|
166d181469 | ||
|
|
a4a2a67177 | ||
|
|
7fe3de78af |
@@ -1,8 +1,8 @@
|
||||
*.DS_Store
|
||||
*.swp
|
||||
.gradle
|
||||
.idea
|
||||
build
|
||||
local.properties
|
||||
gradle.properties
|
||||
infer-out
|
||||
*.iml
|
||||
|
||||
@@ -1,19 +1,184 @@
|
||||
# Change Log
|
||||
|
||||
# 1.12.0
|
||||
## 1.32.0
|
||||
* Add bulk mode
|
||||
* Remember region of interest permanently
|
||||
* Export generated barcode as SVG
|
||||
* Run detection on images in background
|
||||
* Remove a scan from result view
|
||||
* Update Indonesian translation
|
||||
|
||||
## 1.31.0
|
||||
* Stop detection while region of interest is modified
|
||||
* Fix resetting region of interest
|
||||
* Add Polish translation
|
||||
|
||||
## 1.30.1
|
||||
* Improve usability of cropping limiter
|
||||
* Draw round corners around region of interest
|
||||
* Update Indonesian translation
|
||||
|
||||
## 1.30.0
|
||||
* Add a handle to define a region of interest
|
||||
* Add a setting to show/hide cropping limiter
|
||||
* Add support for VCALENDAR types
|
||||
* Add spanish translation
|
||||
* Add copy to clipboard button to context menu in history listing
|
||||
* Make history actions work on current listing only
|
||||
* Keep camera selection over orientation changes
|
||||
|
||||
## 1.29.0
|
||||
* Add a setting to enable reading of vertical 1D barcodes
|
||||
|
||||
## 1.28.0
|
||||
* Share history as CSV and JSON too
|
||||
* Export history in JSON format
|
||||
* Update Indonesian translation
|
||||
* Fix initializing of RenderScript automatically
|
||||
* Fix highlighting after landscape to landscape rotations
|
||||
|
||||
## 1.27.1
|
||||
* Update Russian translation
|
||||
* Force compat mode if native RenderScript crashed
|
||||
|
||||
## 1.27.0
|
||||
* Export SQLite database
|
||||
* Improve order of preferences
|
||||
* Update Italian translation
|
||||
|
||||
## 1.26.0
|
||||
* Zoom in & out by swiping vertically in camera view
|
||||
* Add a setting for swipe to zoom feature
|
||||
* Optimize vector drawables
|
||||
* Update Indonesian translation
|
||||
|
||||
## 1.25.1
|
||||
* Fix Proguard configuration
|
||||
|
||||
## 1.25.0
|
||||
* Add giving scans individual names in history listing
|
||||
* Add searching the history
|
||||
* Add a setting to search harder for barcodes
|
||||
* Add a setting to control vibration on detection
|
||||
* Add app shortcuts for launchers that support them
|
||||
* Remove saving barcodes for intents from other apps
|
||||
* Show latest possible result point in camera view
|
||||
* Suggest a WiFi network on Android Q+
|
||||
* Always put parsed WiFi password into clipboard
|
||||
* Use native image processing where available
|
||||
* Ignore consecutive duplicates by default
|
||||
* Fix saving external files on Android Q+
|
||||
* Fix order of metadata items
|
||||
|
||||
## 1.24.0
|
||||
* Show barcode metadata
|
||||
* Add a setting to show/hide metadata
|
||||
* Include metadata in CSV export
|
||||
* Update italian translation
|
||||
* Fix resetting flash button
|
||||
* Fix CSV export for Android Q
|
||||
|
||||
## 1.23.0
|
||||
* Add a setting to not save consecutive double scans
|
||||
* Add a setting to show/hide hex dump
|
||||
* Limit history items to just one line
|
||||
* Fix window insets for consecutive screens
|
||||
|
||||
## 1.22.3
|
||||
* Fix broken RenderScript built
|
||||
|
||||
## 1.22.2
|
||||
* Fix getting window insets for Android Lollipop and above
|
||||
|
||||
## 1.22.1
|
||||
* Ensure non-printable content is saved as raw data
|
||||
|
||||
## 1.22.0
|
||||
* Highlight detected barcodes in camera view
|
||||
* Add bing and yandex search engines
|
||||
* Update Chinese Translation
|
||||
* Update Italian translation
|
||||
* Update Indonesian translation
|
||||
|
||||
## 1.21.0
|
||||
* Save and restore history list state
|
||||
* Improve scanning from large images
|
||||
* Fix opening image files by "Open with"
|
||||
|
||||
## 1.20.1
|
||||
* Fix window insets below Android Lollipop
|
||||
|
||||
## 1.20.0
|
||||
* Scroll content below tool and system bars
|
||||
* Preset file name to save with format and content
|
||||
* Add encode menu item to make encoding easier when keyboard is visible
|
||||
* Increase font size of barcode contents in history
|
||||
* Update Hungarian translation
|
||||
* Update Indonesian translation
|
||||
* Fix encoding for old Androids
|
||||
* Fix returning result for ZXing's SCAN intent again
|
||||
* Fix transfering barcode format from history
|
||||
|
||||
## 1.19.0
|
||||
* Allow scaling a new barcode to half its size
|
||||
* Remember last selected barcode format
|
||||
|
||||
## 1.18.0
|
||||
* Make open URL default action after decoding a barcode
|
||||
* Add a setting to open contents without inspection
|
||||
* Highlight codes for picking in shared/loaded images
|
||||
* Improve visability of crop border
|
||||
* Fix returning result for ZXing's SCAN intent
|
||||
|
||||
## 1.17.1
|
||||
* Fix navigating back without choosing an image
|
||||
|
||||
## 1.17.0
|
||||
* Pinch/Zoom shared/loaded images to scan just a section
|
||||
* Add a load file menu item to load an image file
|
||||
* Add a save file menu item to the barcode view
|
||||
* Use discrete style for size seek bar
|
||||
* Fix bar transparency when zooming/pinching
|
||||
|
||||
## 1.16.0
|
||||
* Improve displaying scanned barcodes
|
||||
* Show a message if a barcode cannot be shared
|
||||
* Fix setting system bar transparency when double taping a barcode
|
||||
* Update Dutch translation
|
||||
* Update Hungarian translation
|
||||
* Update Indonesian translation
|
||||
* Update Italian translation
|
||||
|
||||
## 1.15.0
|
||||
* Additionally export history as CSV file
|
||||
* Color tool and system bars when a view is scrolled
|
||||
* Add an intent filter for opening images
|
||||
* Add Russian translation
|
||||
|
||||
## 1.14.0
|
||||
* Add OTP support
|
||||
* Update Indonesian translation
|
||||
|
||||
## 1.13.1
|
||||
* Fix crash caused by missing support for Regex on Android < 6
|
||||
|
||||
## 1.13.0
|
||||
* Add missing extras for ZXing result intents
|
||||
|
||||
## 1.12.0
|
||||
* Add preferences dialog and the option to use a custom URL with read contents
|
||||
* Add Brazilian Portuguese translation
|
||||
* Add Chinese translation
|
||||
* Encode text using UTF-8 encoding when creating a barcode
|
||||
|
||||
# 1.11.2
|
||||
## 1.11.2
|
||||
* Downgrade ZXing version
|
||||
* Improve getting system UI metrics
|
||||
|
||||
# 1.11.1
|
||||
## 1.11.1
|
||||
* Fix tap to focus on some devices
|
||||
|
||||
# 1.11.0
|
||||
## 1.11.0
|
||||
* Add support for VCard and VEvent barcodes contents
|
||||
* Generated barcodes are now zoomable
|
||||
* Fix creating PDF417 barcodes
|
||||
|
||||
@@ -13,7 +13,7 @@ to squash when merging into master:
|
||||
|
||||
$ git merge cool_feature --squash
|
||||
|
||||
Then write a [good commit messages][commit_messages] to keep the history
|
||||
Then write a [good commit message][commit_messages] to keep the history
|
||||
meaningful and useful. One feature, one commit.
|
||||
|
||||
[android_best_practices]: https://developer.android.com/distribute/best-practices/develop/
|
||||
|
||||
@@ -14,13 +14,13 @@ bundle: lint
|
||||
lint:
|
||||
./gradlew lintDebug
|
||||
|
||||
sonarqube:
|
||||
./gradlew sonarqube
|
||||
|
||||
infer: clean
|
||||
infer -- ./gradlew assembleDebug
|
||||
|
||||
test:
|
||||
./gradlew test
|
||||
|
||||
cat:
|
||||
./gradlew test cAT
|
||||
|
||||
install:
|
||||
@@ -33,8 +33,14 @@ start:
|
||||
uninstall:
|
||||
adb $(TARGET) uninstall $(PACKAGE).debug
|
||||
|
||||
meminfo:
|
||||
adb shell dumpsys meminfo $(PACKAGE).debug
|
||||
|
||||
images:
|
||||
svg/update.sh
|
||||
|
||||
avocado:
|
||||
avocado $(shell fgrep -rl '<vector' app/src/main/res)
|
||||
|
||||
clean:
|
||||
./gradlew clean
|
||||
|
||||
@@ -2,4 +2,5 @@
|
||||
|
||||
Binary Eye does not collect any personal information.
|
||||
|
||||
If you have any questions or suggestions about my Privacy Policy, do not hesitate to contact me.
|
||||
If you have any questions or suggestions about my Privacy Policy, do not
|
||||
hesitate to contact me.
|
||||
|
||||
@@ -2,18 +2,133 @@
|
||||
|
||||
Yet another barcode scanner for Android. As if there weren't [enough][play].
|
||||
|
||||
This one is totally free, open source and does use the [ZXing][zxing] barcode
|
||||
scanning library.
|
||||
This one is free, without any ads and open source.
|
||||
|
||||
Unlike many others, it works in portrait and landscape orientation, can read
|
||||
inverted codes, comes in Material Design and doesn't do much else but decoding
|
||||
a barcode.
|
||||
Works in portrait and landscape orientation, can read inverted codes,
|
||||
comes in Material Design and can also generate barcodes.
|
||||
|
||||
Binary Eye uses the [ZXing][zxing] ("Zebra Crossing") barcode scanning
|
||||
library.
|
||||
|
||||
## Screenshots
|
||||
|
||||

|
||||

|
||||

|
||||
|
||||
## Download
|
||||
|
||||
<a href="https://f-droid.org/en/packages/de.markusfisch.android.binaryeye/"><img alt="Get it on F-Droid" src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png" height="80"/></a> <a href="https://play.google.com/store/apps/details?id=de.markusfisch.android.binaryeye"><img alt="Get it on Google Play" src="https://play.google.com/intl/en_us/badges/images/generic/en_badge_web_generic.png" height="80"/></a>
|
||||
|
||||
And it's written in [Kotlin][kotlin] because I wanted to explore that a
|
||||
bit more.
|
||||
## Supported Barcode Formats
|
||||
|
||||
### Read
|
||||
|
||||
ZXing can read the following barcode formats:
|
||||
* [AZTEC][aztec]
|
||||
* [CODABAR][codabar]
|
||||
* [CODE 39][code_39]
|
||||
* [CODE 93][code_93]
|
||||
* [CODE 128][code_128]
|
||||
* [DATA MATRIX][data_matrix]
|
||||
* [EAN 8][ean_8]
|
||||
* [EAN 13][ean_13]
|
||||
* [ITF][itf]
|
||||
* [MAXICODE][maxicode] (only when unrotated and unskewed, see [77][77],
|
||||
because of which Binary Eye *cannot* read this barcode)
|
||||
* [PDF417][pdf417]
|
||||
* [QR CODE][qr_code]
|
||||
* [RSS 14][rss]
|
||||
* [RSS EXPANDED][rss]
|
||||
* [UPC A][upc_a]
|
||||
* [UPC E][upc_e]
|
||||
* [UPC EAN EXTENSION][upc_ean]
|
||||
|
||||
### Generate
|
||||
|
||||
ZXing can generate the following barcode formats:
|
||||
* [AZTEC][aztec]
|
||||
* [CODABAR][codabar]
|
||||
* [CODE 39][code_39]
|
||||
* [CODE 128][code_128]
|
||||
* [DATA MATRIX][data_matrix]
|
||||
* [EAN 8][ean_8]
|
||||
* [EAN 13][ean_13]
|
||||
* [ITF][itf]
|
||||
* [PDF 417][pdf417]
|
||||
* [QR CODE][qr_code]
|
||||
* [UPC A][upc_a]
|
||||
|
||||
## RenderScript
|
||||
|
||||
This app uses [RenderScript][rs] to resize and rotate the camera image.
|
||||
Unfortunately, RenderScript has some nasty gotchas.
|
||||
|
||||
### RenderScript.forceCompat()
|
||||
|
||||
It's necessary to call `RenderScript.forceCompat()` on some devices/roms.
|
||||
|
||||
`RenderScript.forceCompat()` needs to be run before any other RenderScript
|
||||
function and unfortunately there is no way to know if invoking `forceCompat()`
|
||||
is necessary or not.
|
||||
|
||||
If `RenderScript.forceCompat()` is necessary, a `RSRuntimeException` will
|
||||
be thrown and the only option is to restart the app, this time with calling
|
||||
`forceCompat()` first.
|
||||
|
||||
Calling `RenderScript.forceCompat()` means the processing is done in
|
||||
software so you probably don't want to enable it by default.
|
||||
|
||||
### macOS Catalina
|
||||
|
||||
At the time of writing, calling `RenderScript.forceCompat()` in a build made
|
||||
on macOS Catalina with build tools 29.0.3 will crash the app. This is a bug
|
||||
in the build tools for Catalina.
|
||||
|
||||
So *do not* build release builts on macOS Catalina with build tools 29.0.3.
|
||||
|
||||
Building on Linux with 29.0.3 works fine.
|
||||
|
||||
### Side by Side NDK
|
||||
|
||||
At the time of writing, it's important to _not_ have the NDK available when
|
||||
compiling the custom rotation kernel. This will produce a broken build for
|
||||
some ARMv7 devices running Android 6 (e.g. One Plus X, Yotaphone 2, Moto E)
|
||||
while it works for newer Android versions.
|
||||
|
||||
### App Bundle
|
||||
|
||||
At the time of writing, `./gradlew bundleRelease` will produce a broken
|
||||
App Bundle without the necessary RenderScript libraries. This is another
|
||||
bug in the build tools.
|
||||
|
||||
### 2D barcodes
|
||||
|
||||
If you want to fork this and are only interested in reading 2D barcodes
|
||||
(like QR or Aztec), you may want to remove the custom rotation kernel
|
||||
altogether as ZXing can read 2D barcodes in any orientation.
|
||||
|
||||
This will make your app a bit simpler and saves you from compiling a
|
||||
custom RenderScript kernel for each architecture you want to support.
|
||||
|
||||
[play]: https://play.google.com/store/search?q=barcode%20scanner&c=apps
|
||||
[zxing]: https://github.com/zxing/zxing
|
||||
[kotlin]: http://kotlinlang.org/
|
||||
[aztec]: https://en.wikipedia.org/wiki/Aztec_Code
|
||||
[codabar]: https://en.wikipedia.org/wiki/Codabar
|
||||
[code_39]: https://en.wikipedia.org/wiki/Code_39
|
||||
[code_93]: https://en.wikipedia.org/wiki/Code_93
|
||||
[code_128]: https://en.wikipedia.org/wiki/Code_128
|
||||
[data_matrix]: https://en.wikipedia.org/wiki/Data_Matrix
|
||||
[ean_8]: https://en.wikipedia.org/wiki/EAN-8
|
||||
[ean_13]: https://en.wikipedia.org/wiki/International_Article_Number
|
||||
[itf]: https://en.wikipedia.org/wiki/Interleaved_2_of_5
|
||||
[maxicode]: https://en.wikipedia.org/wiki/MaxiCode
|
||||
[pdf417]: https://en.wikipedia.org/wiki/PDF417
|
||||
[qr_code]: https://en.wikipedia.org/wiki/QR_code
|
||||
[rss]: https://en.wikipedia.org/wiki/GS1_DataBar
|
||||
[upc_a]: https://en.wikipedia.org/wiki/Universal_Product_Code
|
||||
[upc_e]: https://en.wikipedia.org/wiki/Universal_Product_Code#UPC-E
|
||||
[upc_ean]: https://en.wikipedia.org/wiki/Universal_Product_Code#EAN-13
|
||||
[77]: https://github.com/markusfisch/BinaryEye/issues/77
|
||||
[rs]: https://developer.android.com/guide/topics/renderscript/compute
|
||||
|
||||
@@ -9,8 +9,8 @@ android {
|
||||
minSdkVersion 9
|
||||
targetSdkVersion sdk_version
|
||||
|
||||
versionCode 36
|
||||
versionName '1.12.0'
|
||||
versionCode 69
|
||||
versionName '1.32.0'
|
||||
|
||||
// it's recommended to set this value to the lowest API level
|
||||
// able to provide all the functionality
|
||||
@@ -58,19 +58,21 @@ android {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
androidTestImplementation ('com.android.support.test:runner:1.0.2') {
|
||||
androidTestImplementation('com.android.support.test:runner:1.0.2') {
|
||||
exclude module: 'support-annotations'
|
||||
}
|
||||
androidTestImplementation ('com.android.support.test:rules:1.0.2') {
|
||||
androidTestImplementation('com.android.support.test:rules:1.0.2') {
|
||||
exclude module: 'support-annotations'
|
||||
}
|
||||
testImplementation 'junit:junit:4.12'
|
||||
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.0.1"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.2"
|
||||
implementation "com.android.support:appcompat-v7:$support_version"
|
||||
implementation "com.android.support:design:$support_version"
|
||||
// Do NOT upgrade ZXing because newer versions won't work on API < 24
|
||||
// https://github.com/zxing/zxing/issues/1170
|
||||
implementation 'com.google.zxing:core:3.3.3'
|
||||
implementation 'com.github.markusfisch:CameraView:1.8.4'
|
||||
implementation 'com.github.markusfisch:ScalingImageView:1.1.3'
|
||||
implementation 'com.github.markusfisch:CameraView:1.9.1'
|
||||
implementation 'com.github.markusfisch:ScalingImageView:1.2.0'
|
||||
}
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
<!-- Lint can't understand Kotlin sources and reports all resources
|
||||
as unused -->
|
||||
<issue id="UnusedResources">
|
||||
<ignore path="src/main/res/"/>
|
||||
<ignore path="src/main/res/" />
|
||||
</issue>
|
||||
<!-- Ignore RenderScript output -->
|
||||
<issue id="UnusedResources">
|
||||
<ignore path="*/res/rs/debug/raw/bc*"/>
|
||||
<ignore path="*/res/rs/debug/raw/bc*" />
|
||||
</issue>
|
||||
</lint>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
-dontwarn com.google.**
|
||||
-dontwarn android.support.v7.**
|
||||
-dontwarn android.support.v8.**
|
||||
|
||||
-keep class android.support.v7.widget.SearchView { *; }
|
||||
-keep class android.support.v8.renderscript.** { *; }
|
||||
|
||||
|
After Width: | Height: | Size: 97 KiB |
|
After Width: | Height: | Size: 79 KiB |
|
After Width: | Height: | Size: 88 KiB |
|
After Width: | Height: | Size: 77 KiB |
|
After Width: | Height: | Size: 52 KiB |
|
After Width: | Height: | Size: 47 KiB |
@@ -1,14 +1,14 @@
|
||||
package de.markusfisch.android.binaryeye
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import android.support.test.InstrumentationRegistry
|
||||
import android.support.test.runner.AndroidJUnit4
|
||||
|
||||
import de.markusfisch.android.binaryeye.rs.Preprocessor
|
||||
import de.markusfisch.android.binaryeye.zxing.Zxing
|
||||
|
||||
import org.junit.Assert.fail;
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
import java.util.regex.Pattern
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
@@ -19,34 +19,85 @@ class PreprocessorTest {
|
||||
val assets = InstrumentationRegistry.getInstrumentation()
|
||||
.context.assets
|
||||
val pattern = Pattern.compile(
|
||||
"[0-9]+-([0-9]+)x([0-9]+)-([0-9]+)deg.yuv"
|
||||
"[0-9]+-([0-9]+)deg.jpg"
|
||||
)
|
||||
|
||||
for (file in assets.list("yuv")) {
|
||||
val m = pattern.matcher(file)
|
||||
if (!m.find() || m.groupCount() < 3) {
|
||||
continue
|
||||
val samples = assets.list("samples")
|
||||
checkNotNull(samples) { "no samples found" }
|
||||
for (sample in samples) {
|
||||
val m = pattern.matcher(sample)
|
||||
if (!m.find() || m.groupCount() < 1) {
|
||||
fail("invalid sample: $sample")
|
||||
}
|
||||
|
||||
val frameWidth = m.group(1).toInt()
|
||||
val frameHeight = m.group(2).toInt()
|
||||
val frameOrientation = m.group(3).toInt()
|
||||
val frameData = assets.open("yuv/$file").readBytes()
|
||||
val bitmap = BitmapFactory.decodeStream(
|
||||
assets.open("samples/$sample")
|
||||
)
|
||||
val frameData = getNV21(bitmap)
|
||||
val frameWidth = bitmap.width
|
||||
val frameHeight = bitmap.height
|
||||
val frameOrientation = m.group(1) ?: return
|
||||
val preprocessor = Preprocessor(
|
||||
InstrumentationRegistry.getTargetContext(),
|
||||
frameWidth,
|
||||
frameHeight,
|
||||
frameOrientation
|
||||
)
|
||||
preprocessor.process(frameData)
|
||||
val result = zxing.decode(
|
||||
frameData,
|
||||
preprocessor.outWidth,
|
||||
preprocessor.outHeight
|
||||
null
|
||||
)
|
||||
val outWidth: Int
|
||||
val outHeight: Int
|
||||
val orientation = frameOrientation.toInt()
|
||||
if (orientation == 90 || orientation == 270) {
|
||||
preprocessor.resizeAndRotate(frameData)
|
||||
outWidth = preprocessor.outHeight
|
||||
outHeight = preprocessor.outWidth
|
||||
} else {
|
||||
preprocessor.resizeOnly(frameData)
|
||||
outWidth = preprocessor.outWidth
|
||||
outHeight = preprocessor.outHeight
|
||||
}
|
||||
val result = zxing.decode(frameData, outWidth, outHeight)
|
||||
preprocessor.destroy()
|
||||
|
||||
checkNotNull(result) { "no barcode found in $file" }
|
||||
checkNotNull(result) { "no barcode found in $sample" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getNV21(bitmap: Bitmap): ByteArray {
|
||||
val width = bitmap.width
|
||||
val height = bitmap.height
|
||||
val argb = IntArray(width * height)
|
||||
bitmap.getPixels(argb, 0, width, 0, 0, width, height)
|
||||
return encodeYUV420SP(argb, width, height)
|
||||
}
|
||||
|
||||
private fun encodeYUV420SP(argb: IntArray, width: Int, height: Int): ByteArray {
|
||||
val yuv = ByteArray(ceilIfUneven(width) * ceilIfUneven(height) * 3 / 2)
|
||||
var yIndex = 0
|
||||
var uvIndex = width * height
|
||||
var r: Int
|
||||
var g: Int
|
||||
var b: Int
|
||||
var Y: Int
|
||||
var u: Int
|
||||
var v: Int
|
||||
var index = 0
|
||||
for (y in 0 until height) {
|
||||
for (x in 0 until width) {
|
||||
val pixel = argb[index]
|
||||
r = pixel and 0xff0000 shr 16
|
||||
g = pixel and 0xff00 shr 8
|
||||
b = pixel and 0xff
|
||||
Y = (66 * r + 129 * g + 25 * b + 128 shr 8) + 16
|
||||
u = (-38 * r - 74 * g + 112 * b + 128 shr 8) + 128
|
||||
v = (112 * r - 94 * g - 18 * b + 128 shr 8) + 128
|
||||
yuv[yIndex++] = (if (Y < 0) 0 else if (Y > 255) 255 else Y).toByte()
|
||||
if (y % 2 == 0 && index % 2 == 0) {
|
||||
yuv[uvIndex++] = (if (v < 0) 0 else if (v > 255) 255 else v).toByte()
|
||||
yuv[uvIndex++] = (if (u < 0) 0 else if (u > 255) 255 else u).toByte()
|
||||
}
|
||||
++index
|
||||
}
|
||||
}
|
||||
return yuv
|
||||
}
|
||||
|
||||
private fun ceilIfUneven(n: Int) = if (n and 1 == 1) n + 1 else n
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
<supports-screens
|
||||
android:largeScreens="true"
|
||||
android:xlargeScreens="true"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
|
||||
<uses-permission android:name="android.permission.CAMERA"/>
|
||||
<uses-permission android:name="android.permission.VIBRATE"/>
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
||||
@@ -16,8 +17,9 @@
|
||||
tools:ignore="UnusedAttribute"
|
||||
android:name=".app.BinaryEyeApp"
|
||||
android:allowBackup="true"
|
||||
android:fullBackupContent="true"
|
||||
android:fullBackupContent="@xml/backup_rules"
|
||||
android:supportsRtl="true"
|
||||
android:resizeableActivity="true"
|
||||
android:label="@string/app_name"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:theme="@style/AppTheme">
|
||||
@@ -29,6 +31,9 @@
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
</intent-filter>
|
||||
<meta-data
|
||||
android:name="android.app.shortcuts"
|
||||
android:resource="@xml/shortcuts"/>
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".activity.CameraActivity"
|
||||
@@ -38,11 +43,6 @@
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<data android:mimeType="text/plain"/>
|
||||
</intent-filter>
|
||||
<intent-filter android:label="@string/decode_barcode">
|
||||
<action android:name="android.intent.action.SEND"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<data android:mimeType="image/*"/>
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="com.google.zxing.client.android.SCAN"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
@@ -57,9 +57,25 @@
|
||||
android:scheme="http"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".activity.PickActivity"
|
||||
android:label="@string/pick_code_to_scan"
|
||||
tools:ignore="AppLinkUrlError">
|
||||
<intent-filter android:label="@string/decode_barcode">
|
||||
<action android:name="android.intent.action.SEND"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<data android:mimeType="image/*"/>
|
||||
</intent-filter>
|
||||
<intent-filter android:label="@string/decode_barcode">
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<data android:mimeType="image/*"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".activity.MainActivity"
|
||||
android:label="@string/app_name"/>
|
||||
android:label="@string/app_name"
|
||||
android:exported="true"/>
|
||||
<provider
|
||||
android:name="android.support.v4.content.FileProvider"
|
||||
android:authorities="${applicationId}.provider"
|
||||
@@ -69,5 +85,8 @@
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/file_paths"/>
|
||||
</provider>
|
||||
<meta-data
|
||||
android:name="android.app.searchable"
|
||||
android:resource="@xml/searchable"/>
|
||||
</application>
|
||||
</manifest>
|
||||
|
||||
@@ -1,18 +1,30 @@
|
||||
package de.markusfisch.android.binaryeye.actions
|
||||
|
||||
import de.markusfisch.android.binaryeye.actions.mail.MailAction
|
||||
import de.markusfisch.android.binaryeye.actions.otpauth.OtpauthAction
|
||||
import de.markusfisch.android.binaryeye.actions.search.OpenOrSearchAction
|
||||
import de.markusfisch.android.binaryeye.actions.sms.SmsAction
|
||||
import de.markusfisch.android.binaryeye.actions.tel.TelAction
|
||||
import de.markusfisch.android.binaryeye.actions.vtype.vcard.VCardAction
|
||||
import de.markusfisch.android.binaryeye.actions.vtype.vevent.VEventAction
|
||||
import de.markusfisch.android.binaryeye.actions.web.WebAction
|
||||
import de.markusfisch.android.binaryeye.actions.wifi.WifiAction
|
||||
|
||||
object ActionRegistry {
|
||||
val REGISTRY: Set<IAction> = setOf(
|
||||
MailAction, SmsAction, TelAction, VCardAction, VEventAction, WifiAction
|
||||
val DEFAULT_ACTION: IAction = OpenOrSearchAction
|
||||
|
||||
private val REGISTRY: Set<IAction> = setOf(
|
||||
MailAction,
|
||||
OtpauthAction,
|
||||
SmsAction,
|
||||
TelAction,
|
||||
VCardAction,
|
||||
VEventAction,
|
||||
WebAction,
|
||||
WifiAction
|
||||
)
|
||||
|
||||
fun getAction(data: ByteArray): IAction? = REGISTRY.find {
|
||||
fun getAction(data: ByteArray): IAction = REGISTRY.find {
|
||||
it.canExecuteOn(data)
|
||||
}
|
||||
} ?: DEFAULT_ACTION
|
||||
}
|
||||
|
||||
@@ -1,37 +1,48 @@
|
||||
package de.markusfisch.android.binaryeye.actions
|
||||
|
||||
import de.markusfisch.android.binaryeye.app.execShareIntent
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.widget.Toast
|
||||
import de.markusfisch.android.binaryeye.app.execShareIntent
|
||||
import de.markusfisch.android.binaryeye.app.parseAndNormalizeUri
|
||||
import de.markusfisch.android.binaryeye.widget.toast
|
||||
|
||||
interface IAction {
|
||||
val iconResId: Int
|
||||
val titleResId: Int
|
||||
|
||||
fun canExecuteOn(data: ByteArray): Boolean
|
||||
fun execute(context: Context, data: ByteArray)
|
||||
suspend fun execute(context: Context, data: ByteArray)
|
||||
}
|
||||
|
||||
abstract class SimpleIntentIAction : IAction {
|
||||
abstract class IntentAction : IAction {
|
||||
abstract val errorMsg: Int
|
||||
|
||||
final override fun execute(context: Context, data: ByteArray) {
|
||||
val intent =
|
||||
executeForIntent(context, data) ?: return Toast.makeText(
|
||||
context,
|
||||
errorMsg,
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
final override suspend fun execute(context: Context, data: ByteArray) {
|
||||
val intent = createIntent(context, data) ?: return context.toast(
|
||||
errorMsg
|
||||
)
|
||||
execShareIntent(context, intent)
|
||||
}
|
||||
|
||||
abstract fun executeForIntent(context: Context, data: ByteArray): Intent?
|
||||
abstract suspend fun createIntent(context: Context, data: ByteArray): Intent?
|
||||
}
|
||||
|
||||
fun IAction?.validateOrGetNew(data: ByteArray): IAction? {
|
||||
return this?.takeIf {
|
||||
canExecuteOn(data)
|
||||
} ?: ActionRegistry.getAction(data)
|
||||
abstract class SchemeAction : IAction {
|
||||
abstract val scheme: String
|
||||
open val intentAction: String = Intent.ACTION_VIEW
|
||||
open val buildRegex: Boolean = false
|
||||
|
||||
final override fun canExecuteOn(data: ByteArray): Boolean {
|
||||
val content = String(data)
|
||||
return if (buildRegex) {
|
||||
content.matches("""^$scheme://[\w\W]+$""".toRegex(RegexOption.IGNORE_CASE))
|
||||
} else {
|
||||
content.startsWith("$scheme://", ignoreCase = true)
|
||||
}
|
||||
}
|
||||
|
||||
final override suspend fun execute(context: Context, data: ByteArray) {
|
||||
val uri = parseAndNormalizeUri(String(data))
|
||||
execShareIntent(context, Intent(intentAction, uri))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
package de.markusfisch.android.binaryeye.actions.mail
|
||||
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.actions.SimpleIntentIAction
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.actions.IntentAction
|
||||
|
||||
object MailAction : SimpleIntentIAction() {
|
||||
object MailAction : IntentAction() {
|
||||
private val mailRegex =
|
||||
"""^mail(?:to)?:([\w.%+-]+@[A-Za-z\d.-]+\.[A-Za-z]{2,6}(?:[?&](?:subject|body)=[\S\s]*?){0,2})$""".toRegex()
|
||||
|
||||
@@ -19,7 +18,7 @@ object MailAction : SimpleIntentIAction() {
|
||||
return String(data).matches(mailRegex)
|
||||
}
|
||||
|
||||
override fun executeForIntent(context: Context, data: ByteArray): Intent? {
|
||||
override suspend fun createIntent(context: Context, data: ByteArray): Intent? {
|
||||
val mailWithMessage = mailRegex.matchEntire(
|
||||
String(data)
|
||||
)?.groupValues?.get(1) ?: return null
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
package de.markusfisch.android.binaryeye.actions.otpauth
|
||||
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.actions.SchemeAction
|
||||
|
||||
object OtpauthAction : SchemeAction() {
|
||||
override val iconResId: Int = R.drawable.ic_action_otpauth
|
||||
override val titleResId: Int = R.string.otpauth_add
|
||||
override val scheme: String = "otpauth"
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package de.markusfisch.android.binaryeye.actions.search
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.actions.IAction
|
||||
import de.markusfisch.android.binaryeye.app.alertDialog
|
||||
import de.markusfisch.android.binaryeye.app.execShareIntent
|
||||
import de.markusfisch.android.binaryeye.app.parseAndNormalizeUri
|
||||
import de.markusfisch.android.binaryeye.app.prefs
|
||||
import de.markusfisch.android.binaryeye.widget.toast
|
||||
import java.net.URLEncoder
|
||||
|
||||
object OpenOrSearchAction : IAction {
|
||||
override val iconResId: Int = R.drawable.ic_action_search
|
||||
override val titleResId: Int = R.string.search_web
|
||||
|
||||
override fun canExecuteOn(data: ByteArray): Boolean = false
|
||||
|
||||
override suspend fun execute(context: Context, data: ByteArray) {
|
||||
val intent = openUri(context, String(data)) ?: return
|
||||
execShareIntent(context, intent)
|
||||
}
|
||||
|
||||
private suspend fun openUri(context: Context, data: String, search: Boolean = true): Intent? {
|
||||
val uri = parseAndNormalizeUri(data)
|
||||
val intent = Intent(Intent.ACTION_VIEW, uri)
|
||||
when {
|
||||
intent.resolveActivity(context.packageManager) != null -> return intent
|
||||
search -> return getSearchIntent(context, data)
|
||||
else -> context.toast(R.string.cannot_resolve_action)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private suspend fun getSearchIntent(context: Context, query: String): Intent? {
|
||||
val names = context.resources.getStringArray(
|
||||
R.array.search_engines_names
|
||||
).toMutableList()
|
||||
val urls = context.resources.getStringArray(
|
||||
R.array.search_engines_values
|
||||
).toMutableList()
|
||||
if (prefs.openWithUrl.isNotEmpty()) {
|
||||
names.add(prefs.openWithUrl)
|
||||
urls.add(prefs.openWithUrl)
|
||||
}
|
||||
val queryUri = alertDialog<String>(context) { resume ->
|
||||
setTitle(R.string.pick_search_engine)
|
||||
setItems(names.toTypedArray()) { _, which ->
|
||||
resume(urls[which] + URLEncoder.encode(query, "utf-8"))
|
||||
}
|
||||
} ?: return null
|
||||
return openUri(context, queryUri, false)
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,14 @@
|
||||
package de.markusfisch.android.binaryeye.actions.sms
|
||||
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.actions.SimpleIntentIAction
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.actions.IntentAction
|
||||
|
||||
object SmsAction : SimpleIntentIAction() {
|
||||
private val smsRegex = """^sms(?:to)?:(\+?[0-9]+)(?::([\S\s]*))?$""".toRegex(RegexOption.IGNORE_CASE)
|
||||
object SmsAction : IntentAction() {
|
||||
private val smsRegex =
|
||||
"""^sms(?:to)?:(\+?[0-9]+)(?::([\S\s]*))?$""".toRegex(RegexOption.IGNORE_CASE)
|
||||
|
||||
override val iconResId: Int = R.drawable.ic_action_sms
|
||||
override val titleResId: Int = R.string.sms_send
|
||||
@@ -18,7 +18,7 @@ object SmsAction : SimpleIntentIAction() {
|
||||
return String(data).matches(smsRegex)
|
||||
}
|
||||
|
||||
override fun executeForIntent(context: Context, data: ByteArray): Intent? {
|
||||
override suspend fun createIntent(context: Context, data: ByteArray): Intent? {
|
||||
val (number: String, message: String) = smsRegex.matchEntire(
|
||||
String(data)
|
||||
)?.let {
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
package de.markusfisch.android.binaryeye.actions.tel
|
||||
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.actions.SimpleIntentIAction
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.actions.IntentAction
|
||||
import de.markusfisch.android.binaryeye.app.parseAndNormalizeUri
|
||||
|
||||
object TelAction : SimpleIntentIAction() {
|
||||
object TelAction : IntentAction() {
|
||||
private val telRegex = """^tel:(\+?[0-9]+)$""".toRegex(RegexOption.IGNORE_CASE)
|
||||
|
||||
override val iconResId: Int = R.drawable.ic_action_tel
|
||||
@@ -18,7 +17,7 @@ object TelAction : SimpleIntentIAction() {
|
||||
return String(data).matches(telRegex)
|
||||
}
|
||||
|
||||
override fun executeForIntent(context: Context, data: ByteArray): Intent {
|
||||
return Intent(Intent.ACTION_DIAL, Uri.parse(String(data)))
|
||||
override suspend fun createIntent(context: Context, data: ByteArray): Intent {
|
||||
return Intent(Intent.ACTION_DIAL, parseAndNormalizeUri(String(data)))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,32 @@
|
||||
package de.markusfisch.android.binaryeye.actions.vtype
|
||||
|
||||
import java.util.*
|
||||
|
||||
object VTypeParser {
|
||||
private const val BACKSLASH_R_LEGACY =
|
||||
"(?:\u000D\u000A|[\u000A\u000B\u000C\u000D\u0085\u2028\u2029])"
|
||||
private val vTypeRegex =
|
||||
"""^BEGIN:(VCARD|VEVENT)(?:\R.+?:[\s\S]+?)+?\REND:\1\R?$""".toRegex(RegexOption.IGNORE_CASE)
|
||||
private val propertyRegex = """^(.+?):([\s\S]*?)$""".toRegex(RegexOption.MULTILINE)
|
||||
"""^BEGIN:(V.+)(?:$BACKSLASH_R_LEGACY.+?:[\s\S]+?)+?${BACKSLASH_R_LEGACY}END:\1$BACKSLASH_R_LEGACY?$""".toRegex(
|
||||
RegexOption.IGNORE_CASE
|
||||
)
|
||||
private val propertyRegex = """^(.+?):([\s\S]*?)$""".toRegex(
|
||||
RegexOption.MULTILINE
|
||||
)
|
||||
|
||||
fun parseVType(data: String): String? = vTypeRegex.matchEntire(data)?.groupValues?.get(1)?.toUpperCase()
|
||||
fun parseVType(data: String): String? = vTypeRegex.matchEntire(
|
||||
data
|
||||
)?.groupValues?.get(1)?.toUpperCase(Locale.US)
|
||||
|
||||
fun parseMap(data: String): Map<String, List<VTypeProperty>> = propertyRegex.findAll(data).map {
|
||||
fun parseMap(
|
||||
data: String
|
||||
): Map<String, List<VTypeProperty>> = propertyRegex.findAll(data).map {
|
||||
it.groupValues[1].split(';').let { typeAndInfo ->
|
||||
typeAndInfo[0] to VTypeProperty(typeAndInfo.drop(1), it.groupValues[2])
|
||||
typeAndInfo[0] to VTypeProperty(
|
||||
typeAndInfo.drop(1),
|
||||
it.groupValues[2]
|
||||
)
|
||||
}
|
||||
}.groupBy({ it.first.toUpperCase() }) { it.second }
|
||||
}.groupBy({ it.first.toUpperCase(Locale.US) }) { it.second }
|
||||
}
|
||||
|
||||
data class VTypeProperty(
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
package de.markusfisch.android.binaryeye.actions.vtype.vcard
|
||||
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.actions.SimpleIntentIAction
|
||||
import de.markusfisch.android.binaryeye.actions.vtype.VTypeParser
|
||||
|
||||
import android.content.ContentValues
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.provider.ContactsContract
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.actions.IntentAction
|
||||
import de.markusfisch.android.binaryeye.actions.vtype.VTypeParser
|
||||
import java.util.*
|
||||
import kotlin.collections.ArrayList
|
||||
|
||||
object VCardAction : SimpleIntentIAction() {
|
||||
object VCardAction : IntentAction() {
|
||||
override val iconResId: Int
|
||||
get() = R.drawable.ic_action_vcard
|
||||
override val titleResId: Int
|
||||
@@ -22,7 +23,7 @@ object VCardAction : SimpleIntentIAction() {
|
||||
return VTypeParser.parseVType(String(data)) == "VCARD"
|
||||
}
|
||||
|
||||
override fun executeForIntent(context: Context, data: ByteArray): Intent? {
|
||||
override suspend fun createIntent(context: Context, data: ByteArray): Intent? {
|
||||
val info = VTypeParser.parseMap(String(data))
|
||||
|
||||
return Intent(Intent.ACTION_INSERT_OR_EDIT).apply {
|
||||
@@ -47,7 +48,10 @@ object VCardAction : SimpleIntentIAction() {
|
||||
putExtra(extraPhoneType, phoneType)
|
||||
} ?: putExtra(extraPhoneType, it)
|
||||
}
|
||||
putExtra(extraPhone, phoneProperty.value.toLowerCase().removePrefix("tel:"))
|
||||
putExtra(
|
||||
extraPhone,
|
||||
phoneProperty.value.toLowerCase(Locale.US).removePrefix("tel:")
|
||||
)
|
||||
}
|
||||
info["EMAIL"]?.forEachIndexed { index, mailProperty ->
|
||||
val (extraMailType, extraMail) = when (index) {
|
||||
@@ -116,18 +120,29 @@ object VCardAction : SimpleIntentIAction() {
|
||||
}
|
||||
|
||||
private val String.locationFormat: String
|
||||
get() = """^([^;]*?);([^;]*?);([^;]*?);([^;]*?);([^;]*?);([^;]*?);([^;]*?)$""".toRegex().matchEntire(this)?.groupValues?.let {
|
||||
listOf(it[1], it[2], it[3], "${it[4]} ${it[6]}", "${it[5]} ${it[7]}").filter { line -> line.isNotBlank() }
|
||||
.joinToString("\n").takeIf { location -> location.isNotBlank() }
|
||||
} ?: this
|
||||
get() = """^([^;]*?);([^;]*?);([^;]*?);([^;]*?);([^;]*?);([^;]*?);([^;]*?)$""".toRegex()
|
||||
.matchEntire(
|
||||
this
|
||||
)?.groupValues?.let {
|
||||
listOf(
|
||||
it[1],
|
||||
it[2],
|
||||
it[3],
|
||||
"${it[4]} ${it[6]}",
|
||||
"${it[5]} ${it[7]}"
|
||||
).filter { line -> line.isNotBlank() }
|
||||
.joinToString("\n").takeIf { location -> location.isNotBlank() }
|
||||
} ?: this
|
||||
|
||||
private val String.nameFormat: String
|
||||
get() = """^([^;]*?);([^;]*?);([^;]*?);([^;]*?);([^;]*?)$""".toRegex().matchEntire(this)?.groupValues?.let {
|
||||
listOf(it[4], it[2], it[3], it[1], it[5]).joinToString(" ").takeIf { name -> name.isNotBlank() }
|
||||
} ?: this
|
||||
get() = """^([^;]*?);([^;]*?);([^;]*?);([^;]*?);([^;]*?)$""".toRegex()
|
||||
.matchEntire(this)?.groupValues?.let {
|
||||
listOf(it[4], it[2], it[3], it[1], it[5]).joinToString(" ")
|
||||
.takeIf { name -> name.isNotBlank() }
|
||||
} ?: this
|
||||
|
||||
private val String.mailType: Int?
|
||||
get() = when (this.toUpperCase()) {
|
||||
get() = when (this.toUpperCase(Locale.US)) {
|
||||
"HOME" -> ContactsContract.CommonDataKinds.Email.TYPE_HOME
|
||||
"WORK" -> ContactsContract.CommonDataKinds.Email.TYPE_WORK
|
||||
"OTHER" -> ContactsContract.CommonDataKinds.Email.TYPE_OTHER
|
||||
@@ -136,7 +151,7 @@ object VCardAction : SimpleIntentIAction() {
|
||||
}
|
||||
|
||||
private val String.phoneType: Int?
|
||||
get() = when (this.toUpperCase()) {
|
||||
get() = when (this.toUpperCase(Locale.US)) {
|
||||
"HOME" -> ContactsContract.CommonDataKinds.Phone.TYPE_HOME
|
||||
"MOBILE", "CELL" -> ContactsContract.CommonDataKinds.Phone.TYPE_MOBILE
|
||||
"WORK" -> ContactsContract.CommonDataKinds.Phone.TYPE_WORK
|
||||
@@ -161,7 +176,7 @@ object VCardAction : SimpleIntentIAction() {
|
||||
}
|
||||
|
||||
private val String.addressType: Int?
|
||||
get() = when (this.toUpperCase()) {
|
||||
get() = when (this.toUpperCase(Locale.US)) {
|
||||
"HOME" -> ContactsContract.CommonDataKinds.StructuredPostal.TYPE_HOME
|
||||
"WORK" -> ContactsContract.CommonDataKinds.StructuredPostal.TYPE_WORK
|
||||
"OTHER" -> ContactsContract.CommonDataKinds.StructuredPostal.TYPE_OTHER
|
||||
|
||||
@@ -1,20 +1,18 @@
|
||||
package de.markusfisch.android.binaryeye.actions.vtype.vevent
|
||||
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.actions.SimpleIntentIAction
|
||||
import de.markusfisch.android.binaryeye.actions.vtype.VTypeParser
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.provider.CalendarContract
|
||||
import android.support.annotation.RequiresApi
|
||||
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.actions.IntentAction
|
||||
import de.markusfisch.android.binaryeye.actions.vtype.VTypeParser
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.*
|
||||
|
||||
object VEventAction : SimpleIntentIAction() {
|
||||
object VEventAction : IntentAction() {
|
||||
override val iconResId: Int
|
||||
get() = R.drawable.ic_action_vevent
|
||||
override val titleResId: Int
|
||||
@@ -23,12 +21,15 @@ object VEventAction : SimpleIntentIAction() {
|
||||
get() = R.string.vevent_failed
|
||||
|
||||
override fun canExecuteOn(data: ByteArray): Boolean {
|
||||
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH
|
||||
&& VTypeParser.parseVType(String(data)) == "VEVENT"
|
||||
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 fun executeForIntent(context: Context, data: ByteArray): Intent? {
|
||||
override suspend fun createIntent(context: Context, data: ByteArray): Intent? {
|
||||
val info = VTypeParser.parseMap(String(data))
|
||||
|
||||
return Intent(Intent.ACTION_EDIT).apply {
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
package de.markusfisch.android.binaryeye.actions.web
|
||||
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.actions.SchemeAction
|
||||
|
||||
object WebAction : SchemeAction() {
|
||||
override val iconResId: Int = R.drawable.ic_action_open
|
||||
override val titleResId: Int = R.string.open_url
|
||||
override val scheme: String = "https?"
|
||||
override val buildRegex: Boolean = true
|
||||
}
|
||||
@@ -1,97 +1,33 @@
|
||||
package de.markusfisch.android.binaryeye.actions.wifi
|
||||
|
||||
import android.content.Context
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.actions.IAction
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Context.WIFI_SERVICE
|
||||
import android.net.wifi.WifiConfiguration
|
||||
import android.net.wifi.WifiManager
|
||||
import android.widget.Toast
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import de.markusfisch.android.binaryeye.widget.toast
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
object WifiAction : IAction {
|
||||
override val iconResId = R.drawable.ic_action_wifi
|
||||
override val titleResId = R.string.connect_to_wifi
|
||||
|
||||
var password: String? = null
|
||||
|
||||
override fun canExecuteOn(data: ByteArray): Boolean =
|
||||
WifiConfigurationFactory.parse(String(data)) != null
|
||||
WifiConnector.parse(String(data)) {
|
||||
password = it
|
||||
} != null
|
||||
|
||||
override fun execute(context: Context, data: ByteArray) {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
val wifiConfig = WifiConfigurationFactory.parse(
|
||||
String(data)
|
||||
) ?: return@launch
|
||||
val wifiManager = context.applicationContext.getSystemService(
|
||||
WIFI_SERVICE
|
||||
) as WifiManager
|
||||
|
||||
wifiManager.enableWifi(context)
|
||||
wifiManager.mayRemoveOldNetwork(wifiConfig)
|
||||
wifiManager.enableNewNetwork(wifiConfig)
|
||||
withContext(Dispatchers.Main) {
|
||||
Toast.makeText(
|
||||
context,
|
||||
R.string.wifi_added,
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
}
|
||||
override suspend fun execute(
|
||||
context: Context,
|
||||
data: ByteArray
|
||||
) = withContext(Dispatchers.IO) {
|
||||
val wifiConfig = WifiConnector.parse(
|
||||
String(data)
|
||||
) ?: return@withContext
|
||||
val message = WifiConnector.addNetwork(context, wifiConfig)
|
||||
withContext(Dispatchers.Main) {
|
||||
context.toast(message)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun WifiManager.enableWifi(context: Context): Boolean {
|
||||
if (!this.isWifiEnabled) {
|
||||
if (!this.setWifiEnabled(true)) {
|
||||
withContext(Dispatchers.Main) {
|
||||
Toast.makeText(
|
||||
context,
|
||||
R.string.wifi_config_failed,
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
}
|
||||
return false
|
||||
}
|
||||
var i = 0
|
||||
while (!this.isWifiEnabled) {
|
||||
if (i >= 10) {
|
||||
withContext(Dispatchers.Main) {
|
||||
Toast.makeText(
|
||||
context,
|
||||
R.string.wifi_config_failed,
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
}
|
||||
return false
|
||||
}
|
||||
delay(1000)
|
||||
i++
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private suspend fun WifiManager.mayRemoveOldNetwork(
|
||||
wifiConfig: WifiConfiguration
|
||||
) {
|
||||
configuredNetworks?.firstOrNull {
|
||||
it.SSID == wifiConfig.SSID &&
|
||||
it.allowedKeyManagement == wifiConfig.allowedKeyManagement
|
||||
}?.networkId?.also {
|
||||
removeNetwork(it)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun WifiManager.enableNewNetwork(
|
||||
wifiConfig: WifiConfiguration
|
||||
) {
|
||||
val id = addNetwork(wifiConfig)
|
||||
disconnect()
|
||||
enableNetwork(id, true)
|
||||
reconnect()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
package de.markusfisch.android.binaryeye.actions.wifi
|
||||
|
||||
import android.content.Context
|
||||
import android.net.wifi.WifiConfiguration
|
||||
import android.net.wifi.WifiEnterpriseConfig
|
||||
import android.net.wifi.WifiManager
|
||||
import android.net.wifi.WifiNetworkSuggestion
|
||||
import android.os.Build
|
||||
import android.support.annotation.RequiresApi
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Normal:
|
||||
@@ -28,11 +34,53 @@ import android.os.Build
|
||||
*
|
||||
* The fields can appear in any order. Only "S:" is required.
|
||||
*/
|
||||
object WifiConfigurationFactory {
|
||||
fun parse(input: String): WifiConfiguration? {
|
||||
object WifiConnector {
|
||||
fun parse(
|
||||
input: String,
|
||||
passwordBlock: ((password: String?) -> Unit)? = null
|
||||
): Any? {
|
||||
val inputMap = parseMap(input) ?: return null
|
||||
val parsedData = SimpleDataAccessor.of(inputMap) ?: return null
|
||||
return WifiConfiguration().apply(parsedData)
|
||||
passwordBlock?.apply {
|
||||
invoke(parsedData.password)
|
||||
}
|
||||
return if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
||||
// WifiConfiguration is deprecated in Android Q
|
||||
@Suppress("DEPRECATION")
|
||||
WifiConfiguration().apply(parsedData)
|
||||
} else {
|
||||
WifiNetworkSuggestion.Builder().apply(parsedData)
|
||||
}
|
||||
}
|
||||
|
||||
fun addNetwork(context: Context, config: Any): Int {
|
||||
val wifiManager = context.applicationContext.getSystemService(
|
||||
Context.WIFI_SERVICE
|
||||
) as WifiManager
|
||||
return if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
||||
// WifiConfiguration is deprecated in Android Q
|
||||
@Suppress("DEPRECATION")
|
||||
val wifiConfig = config as WifiConfiguration
|
||||
if (wifiManager.enableWifi() &&
|
||||
wifiManager.removeOldNetwork(wifiConfig) &&
|
||||
wifiManager.enableNewNetwork(wifiConfig)
|
||||
) {
|
||||
R.string.wifi_added
|
||||
} else {
|
||||
R.string.wifi_config_failed
|
||||
}
|
||||
} else {
|
||||
val suggestion = (config as WifiNetworkSuggestion.Builder).build()
|
||||
val suggestions = listOf(suggestion)
|
||||
// remove previous conflicting network suggestion
|
||||
wifiManager.removeNetworkSuggestions(suggestions)
|
||||
val result = wifiManager.addNetworkSuggestions(suggestions)
|
||||
if (result == WifiManager.STATUS_NETWORK_SUGGESTIONS_SUCCESS) {
|
||||
R.string.wifi_added
|
||||
} else {
|
||||
R.string.wifi_config_failed
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal fun parseMap(string: String): Map<String, String>? {
|
||||
@@ -40,14 +88,13 @@ object WifiConfigurationFactory {
|
||||
// generators don't add it
|
||||
val wifiRegex = """^WIFI:((?:.+?:(?:[^\\;]|\\.)*;)+);?$""".toRegex()
|
||||
// should be: ^(.+):((?:[^\\;,":]|\\.)*);$ but allows unescaped , "
|
||||
// and : because many qr creator doesn't escape
|
||||
// and : because many QR Code creators don't escape properly
|
||||
val pairRegex = """(.+?):((?:[^\\;]|\\.)*);""".toRegex()
|
||||
|
||||
return wifiRegex.matchEntire(
|
||||
string
|
||||
)?.groupValues?.get(1)?.let { pairs ->
|
||||
pairRegex.findAll(pairs).map { pair ->
|
||||
pair.groupValues[1].toUpperCase() to pair.groupValues[2]
|
||||
pair.groupValues[1].toUpperCase(Locale.US) to pair.groupValues[2]
|
||||
}.toMap()
|
||||
}
|
||||
}
|
||||
@@ -55,26 +102,19 @@ object WifiConfigurationFactory {
|
||||
internal class SimpleDataAccessor private constructor(
|
||||
private val inputMap: Map<String, String>
|
||||
) {
|
||||
private val hexRegex = """^[0-9a-f]+$""".toRegex(
|
||||
RegexOption.IGNORE_CASE
|
||||
)
|
||||
// keep possibility of wrongly not escaped \ by explicitly searching
|
||||
// for special chars
|
||||
private val escapedRegex = """\\([\\;,":])""".toRegex()
|
||||
|
||||
// this should be private but because of testing it isn't possible
|
||||
internal val ssid: String
|
||||
get() = inputMap.getValue("S").quotedUnlessHex.unescaped
|
||||
get() = inputMap.getValue("S").unescape
|
||||
internal val securityType: String
|
||||
get() = inputMap["T"]?.unescaped ?: ""
|
||||
get() = inputMap["T"]?.unescape ?: ""
|
||||
internal val password: String?
|
||||
get() = inputMap["P"]?.quotedUnlessHex?.unescaped
|
||||
get() = inputMap["P"]?.unescape
|
||||
internal val hidden: Boolean
|
||||
get() = inputMap["H"]?.unescaped == "true"
|
||||
get() = inputMap["H"]?.unescape == "true"
|
||||
internal val anonymousIdentity: String
|
||||
get() = inputMap["AI"]?.unescaped ?: ""
|
||||
get() = inputMap["AI"]?.unescape ?: ""
|
||||
internal val identity: String
|
||||
get() = inputMap["I"]?.unescaped ?: ""
|
||||
get() = inputMap["I"]?.unescape ?: ""
|
||||
internal val eapMethod: Int?
|
||||
get() = if (inputMap["E"].isNullOrEmpty()) {
|
||||
requireSdk(Build.VERSION_CODES.JELLY_BEAN_MR2) {
|
||||
@@ -109,16 +149,6 @@ object WifiConfigurationFactory {
|
||||
else -> null
|
||||
}
|
||||
|
||||
private val String.unescaped: String
|
||||
get() = this.replace(escapedRegex) { escaped ->
|
||||
escaped.groupValues[1]
|
||||
}
|
||||
|
||||
private val String.quotedUnlessHex: String
|
||||
get() = if (matches(hexRegex) || (startsWith("\"") &&
|
||||
endsWith("\""))
|
||||
) this else "\"$this\""
|
||||
|
||||
internal companion object {
|
||||
internal fun of(inputMap: Map<String, String>): SimpleDataAccessor? {
|
||||
return SimpleDataAccessor(inputMap).takeUnless {
|
||||
@@ -128,9 +158,68 @@ object WifiConfigurationFactory {
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.Q)
|
||||
private fun WifiNetworkSuggestion.Builder.apply(
|
||||
data: SimpleDataAccessor
|
||||
): WifiNetworkSuggestion.Builder? {
|
||||
fun WifiNetworkSuggestion.Builder.applyCommon(
|
||||
data: SimpleDataAccessor
|
||||
): WifiNetworkSuggestion.Builder? {
|
||||
setSsid(data.ssid)
|
||||
return this
|
||||
}
|
||||
|
||||
fun WifiNetworkSuggestion.Builder.applySecurity(
|
||||
data: SimpleDataAccessor
|
||||
): WifiNetworkSuggestion.Builder? {
|
||||
when (data.securityType) {
|
||||
"WPA", "WPA2" -> {
|
||||
data.password?.let {
|
||||
setWpa2Passphrase(it)
|
||||
}
|
||||
}
|
||||
"WPA2-EAP" -> {
|
||||
data.password?.let {
|
||||
setWpa2Passphrase(it)
|
||||
}
|
||||
setWpa2EnterpriseConfig(WifiEnterpriseConfig().apply {
|
||||
identity = data.identity
|
||||
anonymousIdentity = data.anonymousIdentity
|
||||
password = data.password
|
||||
data.eapMethod?.let { eapMethod = it }
|
||||
data.phase2Method?.let { phase2Method = it }
|
||||
})
|
||||
}
|
||||
"WPA3" -> {
|
||||
data.password?.let {
|
||||
setWpa3Passphrase(it)
|
||||
}
|
||||
}
|
||||
"WPA3-EAP" -> {
|
||||
data.password?.let { setWpa3Passphrase(it) }
|
||||
setWpa3EnterpriseConfig(WifiEnterpriseConfig().apply {
|
||||
identity = data.identity
|
||||
anonymousIdentity = data.anonymousIdentity
|
||||
password = data.password
|
||||
data.eapMethod?.let { eapMethod = it }
|
||||
data.phase2Method?.let { phase2Method = it }
|
||||
})
|
||||
}
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
return this.applyCommon(data)?.applySecurity(data)
|
||||
}
|
||||
|
||||
// WifiConfiguration is deprecated in Android Q
|
||||
@Suppress("DEPRECATION")
|
||||
private fun WifiConfiguration.apply(
|
||||
data: SimpleDataAccessor
|
||||
): WifiConfiguration? {
|
||||
val ssidWithQuotes = data.ssid.quote
|
||||
val passwordWithQuotes = data.password?.quoteUnlessHex
|
||||
|
||||
fun WifiConfiguration.applyCommon(
|
||||
data: SimpleDataAccessor
|
||||
): WifiConfiguration? {
|
||||
@@ -140,7 +229,7 @@ object WifiConfigurationFactory {
|
||||
allowedPairwiseCiphers.clear()
|
||||
allowedProtocols.clear()
|
||||
|
||||
SSID = data.ssid
|
||||
SSID = ssidWithQuotes
|
||||
hiddenSSID = data.hidden
|
||||
return this
|
||||
}
|
||||
@@ -151,7 +240,7 @@ object WifiConfigurationFactory {
|
||||
when (data.securityType) {
|
||||
"", "nopass" -> allowedKeyManagement.set(WifiConfiguration.KeyMgmt.NONE)
|
||||
"WEP" -> @Suppress("DEPRECATION") /* WEP as insecure */ {
|
||||
data.password?.also {
|
||||
passwordWithQuotes?.let {
|
||||
wepKeys[0] = it
|
||||
} ?: return null
|
||||
wepTxKeyIndex = 0
|
||||
@@ -163,7 +252,7 @@ object WifiConfigurationFactory {
|
||||
allowedGroupCiphers.set(WifiConfiguration.GroupCipher.WEP104)
|
||||
}
|
||||
"WPA" -> {
|
||||
data.password?.also {
|
||||
passwordWithQuotes?.let {
|
||||
preSharedKey = it
|
||||
} ?: return null
|
||||
allowedAuthAlgorithms.set(WifiConfiguration.AuthAlgorithm.OPEN)
|
||||
@@ -179,7 +268,7 @@ object WifiConfigurationFactory {
|
||||
allowedGroupCiphers.set(WifiConfiguration.GroupCipher.CCMP)
|
||||
}
|
||||
"WPA2-EAP" -> requireSdk(Build.VERSION_CODES.JELLY_BEAN_MR2) {
|
||||
data.password?.also {
|
||||
passwordWithQuotes?.let {
|
||||
preSharedKey = it
|
||||
} ?: return null
|
||||
allowedAuthAlgorithms.set(WifiConfiguration.AuthAlgorithm.OPEN)
|
||||
@@ -194,12 +283,15 @@ object WifiConfigurationFactory {
|
||||
enterpriseConfig.identity = data.identity
|
||||
enterpriseConfig.anonymousIdentity = data.anonymousIdentity
|
||||
enterpriseConfig.password = data.password
|
||||
enterpriseConfig.eapMethod = data.eapMethod ?: return null // non valid eapMethod
|
||||
enterpriseConfig.phase2Method = data.phase2Method ?: return null // non valid phase2Method
|
||||
enterpriseConfig.eapMethod =
|
||||
data.eapMethod ?: return null // non valid eapMethod
|
||||
enterpriseConfig.phase2Method =
|
||||
data.phase2Method ?: return null // non valid phase2Method
|
||||
} ?: return null // api isn't high enough
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
return this.applyCommon(data)?.applySecurity(data)
|
||||
}
|
||||
|
||||
@@ -207,3 +299,72 @@ object WifiConfigurationFactory {
|
||||
return if (Build.VERSION.SDK_INT >= version) return block() else null
|
||||
}
|
||||
}
|
||||
|
||||
// keep possibility of wrongly unescaped \ by explicitly searching
|
||||
// for special chars
|
||||
private val escapedRegex = """\\([\\;,":])""".toRegex()
|
||||
private val String.unescape: String
|
||||
get() = this.replace(escapedRegex) { escaped ->
|
||||
escaped.groupValues[1]
|
||||
}
|
||||
|
||||
private val String.quote: String
|
||||
get() = if (startsWith("\"") && endsWith("\"")) this else "\"$this\""
|
||||
|
||||
private val String.quoteUnlessHex: String
|
||||
get() = if (isHex()) this else this.quote
|
||||
|
||||
// Unfortunately, we can never know whether a string consisting only
|
||||
// of 0-f is a hex string. So we just make this one assumption that
|
||||
// a string of 0-f that is exactly 64 characters long, is _probably_
|
||||
// a hex string for raw PSK. Of course, this may fail for any string
|
||||
// that just happens to be 64 characters long and contains just 0-f.
|
||||
private val hexRegex = """^[0-9a-f]+$""".toRegex(
|
||||
RegexOption.IGNORE_CASE
|
||||
)
|
||||
|
||||
private fun String.isHex() = length == 64 && matches(hexRegex)
|
||||
|
||||
private fun WifiManager.enableWifi(): Boolean {
|
||||
// setWifiEnabled() will always return false for Android Q
|
||||
// because Q doesn't allow apps to enable/disable Wi-Fi anymore
|
||||
@Suppress("DEPRECATION")
|
||||
return isWifiEnabled || setWifiEnabled(true)
|
||||
}
|
||||
|
||||
// WifiConfiguration is deprecated in Android Q
|
||||
@Suppress("DEPRECATION")
|
||||
private fun WifiManager.removeOldNetwork(
|
||||
wifiConfig: WifiConfiguration
|
||||
): Boolean {
|
||||
try {
|
||||
configuredNetworks?.firstOrNull {
|
||||
it.SSID == wifiConfig.SSID &&
|
||||
it.allowedKeyManagement == wifiConfig.allowedKeyManagement
|
||||
}?.networkId?.also {
|
||||
removeNetwork(it)
|
||||
}
|
||||
} catch (e: SecurityException) {
|
||||
// the user didn't allow ACCESS_FINE_LOCATION which is
|
||||
// required to access configuredNetworks and that's fine
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// WifiConfiguration is deprecated in Android Q
|
||||
@Suppress("DEPRECATION")
|
||||
private fun WifiManager.enableNewNetwork(
|
||||
wifiConfig: WifiConfiguration
|
||||
): Boolean {
|
||||
val id = addNetwork(wifiConfig)
|
||||
if (id == -1) {
|
||||
return false
|
||||
}
|
||||
disconnect()
|
||||
return if (enableNetwork(id, true)) {
|
||||
reconnect()
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
@@ -1,54 +1,72 @@
|
||||
package de.markusfisch.android.binaryeye.activity
|
||||
|
||||
import com.google.zxing.Result
|
||||
import com.google.zxing.ResultMetadataType
|
||||
|
||||
import de.markusfisch.android.cameraview.widget.CameraView
|
||||
|
||||
import de.markusfisch.android.binaryeye.app.db
|
||||
import de.markusfisch.android.binaryeye.app.hasCameraPermission
|
||||
import de.markusfisch.android.binaryeye.app.initSystemBars
|
||||
import de.markusfisch.android.binaryeye.app.prefs
|
||||
import de.markusfisch.android.binaryeye.rs.Preprocessor
|
||||
import de.markusfisch.android.binaryeye.zxing.Zxing
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Rect
|
||||
import android.graphics.RectF
|
||||
import android.hardware.Camera
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.os.Parcelable
|
||||
import android.os.Vibrator
|
||||
import android.provider.MediaStore
|
||||
import android.support.design.widget.FloatingActionButton
|
||||
import android.support.v7.app.AppCompatActivity
|
||||
import android.support.v7.widget.Toolbar
|
||||
import android.support.v8.renderscript.RSRuntimeException
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.widget.SeekBar
|
||||
import android.widget.Toast
|
||||
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
import java.io.IOException
|
||||
import com.google.zxing.Result
|
||||
import com.google.zxing.ResultMetadataType
|
||||
import com.google.zxing.ResultPointCallback
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.app.*
|
||||
import de.markusfisch.android.binaryeye.database.Scan
|
||||
import de.markusfisch.android.binaryeye.graphics.Mapping
|
||||
import de.markusfisch.android.binaryeye.graphics.frameToView
|
||||
import de.markusfisch.android.binaryeye.graphics.isPortrait
|
||||
import de.markusfisch.android.binaryeye.rs.Preprocessor
|
||||
import de.markusfisch.android.binaryeye.view.setPaddingFromWindowInsets
|
||||
import de.markusfisch.android.binaryeye.widget.DetectorView
|
||||
import de.markusfisch.android.binaryeye.widget.toast
|
||||
import de.markusfisch.android.binaryeye.zxing.Zxing
|
||||
import de.markusfisch.android.cameraview.widget.CameraView
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
class CameraActivity : AppCompatActivity() {
|
||||
private val zxing = Zxing()
|
||||
private val zxing = Zxing(ResultPointCallback { point ->
|
||||
point?.let {
|
||||
getMapping()?.map(it)?.let {
|
||||
detectorView.post {
|
||||
detectorView.mark(listOf(it))
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
private lateinit var vibrator: Vibrator
|
||||
private lateinit var cameraView: CameraView
|
||||
private lateinit var detectorView: DetectorView
|
||||
private lateinit var zoomBar: SeekBar
|
||||
private lateinit var flashFab: View
|
||||
private lateinit var flashFab: FloatingActionButton
|
||||
|
||||
private var preprocessor: Preprocessor? = null
|
||||
private var nativeMapping: Mapping? = null
|
||||
private var rotatedMapping: Mapping? = null
|
||||
private var recreatePreprocessor = false
|
||||
private var rotate = false
|
||||
private var invert = false
|
||||
private var flash = false
|
||||
private var decoding = true
|
||||
private var returnResult = false
|
||||
private var frontFacing = false
|
||||
private var bulkMode = false
|
||||
private var ignoreNext: String? = null
|
||||
private var fallbackBuffer: IntArray? = null
|
||||
|
||||
override fun onRequestPermissionsResult(
|
||||
@@ -57,65 +75,84 @@ class CameraActivity : AppCompatActivity() {
|
||||
grantResults: IntArray
|
||||
) {
|
||||
when (requestCode) {
|
||||
REQUEST_CAMERA -> if (grantResults.isNotEmpty() &&
|
||||
PERMISSION_CAMERA -> if (grantResults.isNotEmpty() &&
|
||||
grantResults[0] != PackageManager.PERMISSION_GRANTED
|
||||
) {
|
||||
Toast.makeText(
|
||||
this,
|
||||
R.string.no_camera_no_fun,
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
toast(R.string.no_camera_no_fun)
|
||||
finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onActivityResult(
|
||||
requestCode: Int,
|
||||
resultCode: Int,
|
||||
resultData: Intent?
|
||||
) {
|
||||
when (requestCode) {
|
||||
PICK_FILE_RESULT_CODE -> {
|
||||
if (resultCode == Activity.RESULT_OK && resultData != null) {
|
||||
val pick = Intent(this, PickActivity::class.java)
|
||||
pick.action = Intent.ACTION_VIEW
|
||||
pick.setDataAndType(resultData.data, "image/*")
|
||||
startActivity(pick)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate(state: Bundle?) {
|
||||
super.onCreate(state)
|
||||
setContentView(R.layout.activity_camera)
|
||||
|
||||
vibrator = getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
|
||||
|
||||
initSystemBars(this)
|
||||
setSupportActionBar(findViewById(R.id.toolbar) as Toolbar)
|
||||
|
||||
vibrator = getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
|
||||
|
||||
cameraView = findViewById(R.id.camera_view) as CameraView
|
||||
detectorView = findViewById(R.id.detector_view) as DetectorView
|
||||
zoomBar = findViewById(R.id.zoom) as SeekBar
|
||||
flashFab = findViewById(R.id.flash)
|
||||
flashFab.setOnClickListener { toggleTorchMode() }
|
||||
flashFab = findViewById(R.id.flash) as FloatingActionButton
|
||||
|
||||
initCameraView()
|
||||
initZoomBar()
|
||||
restoreZoom()
|
||||
initDetectorView()
|
||||
|
||||
if (intent?.action == Intent.ACTION_SEND) {
|
||||
if (intent.type == "text/plain") {
|
||||
handleSendText(intent)
|
||||
} else if (intent.type?.startsWith("image/") == true) {
|
||||
handleSendImage(intent)
|
||||
}
|
||||
if (intent?.action == Intent.ACTION_SEND &&
|
||||
intent.type == "text/plain"
|
||||
) {
|
||||
handleSendText(intent)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
preprocessor?.destroy()
|
||||
resetPreProcessor()
|
||||
fallbackBuffer = null
|
||||
saveZoom()
|
||||
saveCropHandlePos()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
System.gc()
|
||||
zxing.updateHints(prefs.tryHarder)
|
||||
returnResult = "com.google.zxing.client.android.SCAN" == intent.action
|
||||
if (hasCameraPermission(this, REQUEST_CAMERA)) {
|
||||
if (hasCameraPermission(this)) {
|
||||
openCamera()
|
||||
}
|
||||
}
|
||||
|
||||
private fun resetPreProcessor() {
|
||||
preprocessor?.destroy()
|
||||
preprocessor = null
|
||||
}
|
||||
|
||||
private fun openCamera() {
|
||||
cameraView.openAsync(
|
||||
CameraView.findCameraId(
|
||||
@Suppress("DEPRECATION")
|
||||
if (frontFacing) {
|
||||
Camera.CameraInfo.CAMERA_FACING_FRONT
|
||||
} else {
|
||||
@@ -132,39 +169,72 @@ class CameraActivity : AppCompatActivity() {
|
||||
|
||||
private fun closeCamera() {
|
||||
cameraView.close()
|
||||
// closing the camera will also shut off the flash
|
||||
flash = false
|
||||
}
|
||||
|
||||
override fun onRestoreInstanceState(savedState: Bundle) {
|
||||
super.onRestoreInstanceState(savedState)
|
||||
zoomBar.max = savedState.getInt(ZOOM_MAX)
|
||||
zoomBar.progress = savedState.getInt(ZOOM_LEVEL)
|
||||
frontFacing = savedState.getBoolean(FRONT_FACING)
|
||||
bulkMode = savedState.getBoolean(BULK_MODE)
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
outState.putInt(ZOOM_MAX, zoomBar.max)
|
||||
outState.putInt(ZOOM_LEVEL, zoomBar.progress)
|
||||
outState.putBoolean(FRONT_FACING, frontFacing)
|
||||
outState.putBoolean(BULK_MODE, bulkMode)
|
||||
super.onSaveInstanceState(outState)
|
||||
}
|
||||
|
||||
override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
|
||||
// always give crop handle precedence over other controls
|
||||
// because it can easily overlap and would then be inaccessible
|
||||
if (detectorView.onTouchEvent(ev)) {
|
||||
return true
|
||||
}
|
||||
return super.dispatchTouchEvent(ev)
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||
menuInflater.inflate(R.menu.activity_camera, menu)
|
||||
menu.findItem(R.id.bulk_mode).isChecked = bulkMode
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
return when (item.itemId) {
|
||||
R.id.create -> {
|
||||
startActivity(MainActivity.getEncodeIntent(this))
|
||||
createBarcode()
|
||||
true
|
||||
}
|
||||
R.id.history -> {
|
||||
startActivity(MainActivity.getHistoryIntent(this))
|
||||
true
|
||||
}
|
||||
R.id.pick_file -> {
|
||||
val chooseFile = Intent(Intent.ACTION_GET_CONTENT)
|
||||
chooseFile.type = "image/*"
|
||||
startActivityForResult(
|
||||
Intent.createChooser(
|
||||
chooseFile,
|
||||
getString(R.string.pick_file)
|
||||
),
|
||||
PICK_FILE_RESULT_CODE
|
||||
)
|
||||
true
|
||||
}
|
||||
R.id.switch_camera -> {
|
||||
switchCamera()
|
||||
true
|
||||
}
|
||||
R.id.bulk_mode -> {
|
||||
bulkMode = bulkMode xor true
|
||||
item.isChecked = bulkMode
|
||||
true
|
||||
}
|
||||
R.id.preferences -> {
|
||||
startActivity(MainActivity.getPreferencesIntent(this))
|
||||
true
|
||||
@@ -177,6 +247,10 @@ class CameraActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun createBarcode() {
|
||||
startActivity(MainActivity.getEncodeIntent(this))
|
||||
}
|
||||
|
||||
private fun switchCamera() {
|
||||
closeCamera()
|
||||
frontFacing = frontFacing xor true
|
||||
@@ -184,13 +258,15 @@ class CameraActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
private fun openReadme() {
|
||||
startActivity(
|
||||
Intent(
|
||||
Intent.ACTION_VIEW, Uri.parse(
|
||||
"https://github.com/markusfisch/BinaryEye/blob/master/README.md"
|
||||
)
|
||||
)
|
||||
val intent = Intent(
|
||||
Intent.ACTION_VIEW,
|
||||
Uri.parse(getString(R.string.project_url))
|
||||
)
|
||||
if (intent.resolveActivity(packageManager) != null) {
|
||||
startActivity(intent)
|
||||
} else {
|
||||
toast(R.string.project_url)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleSendText(intent: Intent) {
|
||||
@@ -201,38 +277,52 @@ class CameraActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleSendImage(intent: Intent) {
|
||||
val uri = intent.getParcelableExtra<Parcelable>(Intent.EXTRA_STREAM) as? Uri ?: return
|
||||
val bitmap = try {
|
||||
MediaStore.Images.Media.getBitmap(contentResolver, uri)
|
||||
} catch (e: IOException) {
|
||||
null
|
||||
}
|
||||
if (bitmap == null) {
|
||||
Toast.makeText(
|
||||
this,
|
||||
R.string.error_no_content,
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
return
|
||||
}
|
||||
val result = zxing.decodePositiveNegative(downsizeIfBigger(bitmap, 1024))
|
||||
if (result != null) {
|
||||
showResult(result)
|
||||
finish()
|
||||
return
|
||||
}
|
||||
Toast.makeText(
|
||||
this,
|
||||
R.string.no_barcode_found,
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
finish()
|
||||
}
|
||||
|
||||
private fun initCameraView() {
|
||||
cameraView.setUseOrientationListener(true)
|
||||
cameraView.setTapToFocus()
|
||||
@Suppress("ClickableViewAccessibility")
|
||||
cameraView.setOnTouchListener(object : View.OnTouchListener {
|
||||
var focus = true
|
||||
var offset = -1f
|
||||
var progress = 0
|
||||
|
||||
override fun onTouch(v: View?, event: MotionEvent?): Boolean {
|
||||
event ?: return false
|
||||
val pos = event.y
|
||||
when (event.actionMasked) {
|
||||
MotionEvent.ACTION_DOWN -> {
|
||||
offset = pos
|
||||
progress = zoomBar.progress
|
||||
return true
|
||||
}
|
||||
MotionEvent.ACTION_MOVE -> {
|
||||
if (prefs.zoomBySwiping) {
|
||||
v ?: return false
|
||||
val dist = offset - pos
|
||||
val maxValue = zoomBar.max
|
||||
val change = maxValue / v.height.toFloat() * 2f * dist
|
||||
zoomBar.progress = min(
|
||||
maxValue,
|
||||
max(progress + change.roundToInt(), 0)
|
||||
)
|
||||
return true
|
||||
}
|
||||
}
|
||||
MotionEvent.ACTION_UP -> {
|
||||
// stop calling focusTo() as soon as it returns false
|
||||
// to avoid throwing and catching future exceptions
|
||||
if (focus) {
|
||||
focus = cameraView.focusTo(v, event.x, event.y)
|
||||
if (focus) {
|
||||
v?.performClick()
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
})
|
||||
@Suppress("DEPRECATION")
|
||||
cameraView.setOnCameraListener(object : CameraView.OnCameraListener {
|
||||
override fun onConfigureParameters(
|
||||
parameters: Camera.Parameters
|
||||
@@ -262,33 +352,32 @@ class CameraActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
override fun onCameraError() {
|
||||
Toast.makeText(
|
||||
this@CameraActivity,
|
||||
R.string.camera_error,
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
this@CameraActivity.toast(R.string.camera_error)
|
||||
finish()
|
||||
}
|
||||
|
||||
override fun onCameraReady(camera: Camera) {
|
||||
// reset preprocessor to make sure it always fits the current
|
||||
// frame orientation; important for landscape to landscape
|
||||
// orientation changes
|
||||
resetPreProcessor()
|
||||
val frameWidth = cameraView.frameWidth
|
||||
val frameHeight = cameraView.frameHeight
|
||||
val frameOrientation = cameraView.frameOrientation
|
||||
var decoding = true
|
||||
ignoreNext = null
|
||||
decoding = true
|
||||
camera.setPreviewCallback { frameData, _ ->
|
||||
if (decoding) {
|
||||
val result = decodeFrame(
|
||||
decodeFrame(
|
||||
frameData,
|
||||
frameWidth,
|
||||
frameHeight,
|
||||
frameOrientation
|
||||
)
|
||||
result?.let {
|
||||
cameraView.post {
|
||||
vibrator.vibrate(100)
|
||||
showResult(result)
|
||||
)?.let { result ->
|
||||
if (result.text != ignoreNext) {
|
||||
postResult(result)
|
||||
decoding = false
|
||||
}
|
||||
decoding = false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -318,8 +407,10 @@ class CameraActivity : AppCompatActivity() {
|
||||
|
||||
override fun onStopTrackingTouch(seekBar: SeekBar) {}
|
||||
})
|
||||
restoreZoom()
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
private fun setZoom(zoom: Int) {
|
||||
val camera: Camera? = cameraView.camera
|
||||
camera?.let {
|
||||
@@ -348,14 +439,44 @@ class CameraActivity : AppCompatActivity() {
|
||||
)
|
||||
}
|
||||
|
||||
private fun updateFlashFab(available: Boolean) {
|
||||
flashFab.visibility = if (available) {
|
||||
View.GONE
|
||||
private fun initDetectorView() {
|
||||
detectorView.onRoiChange = {
|
||||
decoding = false
|
||||
}
|
||||
detectorView.onRoiChanged = {
|
||||
decoding = true
|
||||
recreatePreprocessor = true
|
||||
}
|
||||
detectorView.setPaddingFromWindowInsets()
|
||||
restoreCropHandlePos()
|
||||
}
|
||||
|
||||
private fun saveCropHandlePos() {
|
||||
val pos = detectorView.getCropHandlePos()
|
||||
prefs.cropHandleX = pos.x
|
||||
prefs.cropHandleY = pos.y
|
||||
prefs.cropHandleOrientation = detectorView.currentOrientation
|
||||
}
|
||||
|
||||
private fun restoreCropHandlePos() {
|
||||
detectorView.setCropHandlePos(
|
||||
prefs.cropHandleX,
|
||||
prefs.cropHandleY,
|
||||
prefs.cropHandleOrientation
|
||||
)
|
||||
}
|
||||
|
||||
private fun updateFlashFab(unavailable: Boolean) {
|
||||
if (unavailable) {
|
||||
flashFab.setImageResource(R.drawable.ic_action_create)
|
||||
flashFab.setOnClickListener { createBarcode() }
|
||||
} else {
|
||||
View.VISIBLE
|
||||
flashFab.setImageResource(R.drawable.ic_action_flash)
|
||||
flashFab.setOnClickListener { toggleTorchMode() }
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
private fun toggleTorchMode() {
|
||||
val camera = cameraView.camera
|
||||
val parameters = camera?.parameters
|
||||
@@ -381,92 +502,226 @@ class CameraActivity : AppCompatActivity() {
|
||||
frameOrientation: Int
|
||||
): Result? {
|
||||
frameData ?: return null
|
||||
invert = invert xor true
|
||||
val pp = preprocessor ?: Preprocessor(
|
||||
this,
|
||||
rotate = if (prefs.autoRotate) {
|
||||
rotate xor true
|
||||
} else {
|
||||
invert = invert xor true
|
||||
isPortrait(frameOrientation)
|
||||
}
|
||||
return try {
|
||||
if (recreatePreprocessor) {
|
||||
resetPreProcessor()
|
||||
recreatePreprocessor = false
|
||||
}
|
||||
val pp = preprocessor ?: createPreprocessorAndMapping(
|
||||
frameWidth,
|
||||
frameHeight,
|
||||
frameOrientation
|
||||
)
|
||||
val w: Int
|
||||
val h: Int
|
||||
if (rotate) {
|
||||
pp.resizeAndRotate(frameData)
|
||||
w = pp.outHeight
|
||||
h = pp.outWidth
|
||||
} else {
|
||||
invert = invert xor true
|
||||
pp.resizeOnly(frameData)
|
||||
w = pp.outWidth
|
||||
h = pp.outHeight
|
||||
}
|
||||
preprocessor = pp
|
||||
zxing.decode(frameData, w, h, invert)
|
||||
} catch (e: RSRuntimeException) {
|
||||
prefs.forceCompat = prefs.forceCompat xor true
|
||||
// now the only option is to restart the app because
|
||||
// RenderScript.forceCompat() needs to be called before
|
||||
// RenderScript is initialized
|
||||
restartApp(this)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun createPreprocessorAndMapping(
|
||||
frameWidth: Int,
|
||||
frameHeight: Int,
|
||||
frameOrientation: Int
|
||||
): Preprocessor {
|
||||
val frameRoi = calculateFrameRect(
|
||||
frameWidth,
|
||||
frameHeight,
|
||||
frameOrientation
|
||||
)
|
||||
pp.process(frameData)
|
||||
preprocessor = pp
|
||||
return zxing.decode(
|
||||
frameData,
|
||||
val pp = Preprocessor(
|
||||
this,
|
||||
frameWidth,
|
||||
frameHeight,
|
||||
frameRoi
|
||||
)
|
||||
val viewRect = if (frameRoi != null) {
|
||||
detectorView.roi
|
||||
} else {
|
||||
cameraView.previewRect
|
||||
}
|
||||
nativeMapping = frameToView(
|
||||
pp.outWidth,
|
||||
pp.outHeight,
|
||||
invert
|
||||
frameOrientation,
|
||||
viewRect
|
||||
)
|
||||
rotatedMapping = frameToView(
|
||||
pp.outHeight,
|
||||
pp.outWidth,
|
||||
(frameOrientation - 90 + 360) % 360,
|
||||
viewRect
|
||||
)
|
||||
return pp
|
||||
}
|
||||
|
||||
private fun showResult(result: Result) {
|
||||
val rawBytes = getRawBytes(result)
|
||||
|
||||
if (prefs.useHistory) {
|
||||
GlobalScope.launch {
|
||||
db.insertScan(
|
||||
System.currentTimeMillis(),
|
||||
result.text,
|
||||
rawBytes,
|
||||
result.barcodeFormat.toString()
|
||||
private fun calculateFrameRect(
|
||||
frameWidth: Int,
|
||||
frameHeight: Int,
|
||||
frameOrientation: Int
|
||||
): Rect? {
|
||||
val viewRoi = detectorView.roi
|
||||
return if (
|
||||
viewRoi.width() == 0 ||
|
||||
viewRoi.height() == 0
|
||||
) {
|
||||
null
|
||||
} else {
|
||||
// map ROI in detectorView to cameraView because cameraView may
|
||||
// be larger than the detectorView
|
||||
val previewRect = cameraView.previewRect
|
||||
val previewRoi = Rect(
|
||||
previewRect.left + viewRoi.left,
|
||||
previewRect.top + viewRoi.top,
|
||||
previewRect.left + viewRoi.right,
|
||||
previewRect.top + viewRoi.bottom
|
||||
)
|
||||
val previewRectWidth = previewRect.width().toFloat()
|
||||
val previewRectHeight = previewRect.height().toFloat()
|
||||
val normalizedRoi = RectF(
|
||||
previewRoi.left.toFloat() / previewRectWidth,
|
||||
previewRoi.top.toFloat() / previewRectHeight,
|
||||
previewRoi.right.toFloat() / previewRectWidth,
|
||||
previewRoi.bottom.toFloat() / previewRectHeight
|
||||
)
|
||||
// since the ROI is always centered and symmetric, we don't
|
||||
// need to distinguish between 0 and 180 or 90 and 270 degree
|
||||
if (isPortrait(frameOrientation)) {
|
||||
Rect(
|
||||
(normalizedRoi.top * frameWidth.toFloat()).roundToInt(),
|
||||
(normalizedRoi.left * frameHeight.toFloat()).roundToInt(),
|
||||
(normalizedRoi.bottom * frameWidth.toFloat()).roundToInt(),
|
||||
(normalizedRoi.right * frameHeight.toFloat()).roundToInt()
|
||||
)
|
||||
} else {
|
||||
Rect(
|
||||
(normalizedRoi.left * frameWidth.toFloat()).roundToInt(),
|
||||
(normalizedRoi.top * frameHeight.toFloat()).roundToInt(),
|
||||
(normalizedRoi.right * frameWidth.toFloat()).roundToInt(),
|
||||
(normalizedRoi.bottom * frameHeight.toFloat()).roundToInt()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (returnResult) {
|
||||
val resultIntent = Intent()
|
||||
resultIntent.putExtra("SCAN_RESULT", result.text)
|
||||
setResult(RESULT_OK, resultIntent)
|
||||
finish()
|
||||
return
|
||||
}
|
||||
|
||||
startActivity(
|
||||
MainActivity.getDecodeIntent(
|
||||
this,
|
||||
result.text,
|
||||
result.barcodeFormat,
|
||||
rawBytes
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun postResult(result: Result) {
|
||||
// get mapping for the current rotate value
|
||||
val mapping = getMapping()
|
||||
cameraView.post {
|
||||
val rp = result.resultPoints
|
||||
if (mapping != null && rp != null && rp.isNotEmpty()) {
|
||||
detectorView.mark(mapping.map(rp))
|
||||
}
|
||||
vibrator.vibrate()
|
||||
showResult(
|
||||
this@CameraActivity,
|
||||
result,
|
||||
returnResult,
|
||||
bulkMode
|
||||
)
|
||||
if (bulkMode) {
|
||||
ignoreNext = result.text
|
||||
toast(result.text)
|
||||
detectorView.postDelayed({
|
||||
decoding = true
|
||||
}, 500)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getMapping() = if (rotate) rotatedMapping else nativeMapping
|
||||
|
||||
companion object {
|
||||
private const val REQUEST_CAMERA = 1
|
||||
private const val PICK_FILE_RESULT_CODE = 1
|
||||
private const val ZOOM_MAX = "zoom_max"
|
||||
private const val ZOOM_LEVEL = "zoom_level"
|
||||
private const val FRONT_FACING = "front_facing"
|
||||
private const val BULK_MODE = "bulk_mode"
|
||||
}
|
||||
}
|
||||
|
||||
fun downsizeIfBigger(bitmap: Bitmap, max: Int): Bitmap {
|
||||
return if (Math.max(bitmap.width, bitmap.height) > max) {
|
||||
var width: Int = max
|
||||
var height: Int = max
|
||||
if (bitmap.width > bitmap.height) {
|
||||
height = Math.round(
|
||||
width.toFloat() / bitmap.width.toFloat() *
|
||||
bitmap.height.toFloat()
|
||||
)
|
||||
} else {
|
||||
width = Math.round(
|
||||
height.toFloat() / bitmap.height.toFloat() *
|
||||
bitmap.width.toFloat()
|
||||
fun showResult(
|
||||
activity: Activity,
|
||||
result: Result,
|
||||
isResult: Boolean = false,
|
||||
bulkMode: Boolean = false
|
||||
) {
|
||||
if (isResult) {
|
||||
activity.setResult(Activity.RESULT_OK, getReturnIntent(result))
|
||||
activity.finish()
|
||||
return
|
||||
}
|
||||
val scan = Scan(result)
|
||||
if (prefs.useHistory) {
|
||||
scan.id = db.insertScan(scan)
|
||||
}
|
||||
if (!bulkMode) {
|
||||
activity.startActivity(
|
||||
MainActivity.getDecodeIntent(activity, scan)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun getReturnIntent(result: Result): Intent {
|
||||
val intent = Intent()
|
||||
intent.putExtra("SCAN_RESULT", result.text)
|
||||
intent.putExtra(
|
||||
"SCAN_RESULT_FORMAT",
|
||||
result.barcodeFormat.toString()
|
||||
)
|
||||
if (result.rawBytes?.isNotEmpty() == true) {
|
||||
intent.putExtra("SCAN_RESULT_BYTES", result.rawBytes)
|
||||
}
|
||||
result.resultMetadata?.let { metadata ->
|
||||
metadata[ResultMetadataType.UPC_EAN_EXTENSION]?.let {
|
||||
intent.putExtra(
|
||||
"SCAN_RESULT_ORIENTATION",
|
||||
it.toString()
|
||||
)
|
||||
}
|
||||
Bitmap.createScaledBitmap(bitmap, width, height, true)
|
||||
} else {
|
||||
bitmap
|
||||
metadata[ResultMetadataType.ERROR_CORRECTION_LEVEL]?.let {
|
||||
intent.putExtra(
|
||||
"SCAN_RESULT_ERROR_CORRECTION_LEVEL",
|
||||
it.toString()
|
||||
)
|
||||
}
|
||||
metadata[ResultMetadataType.UPC_EAN_EXTENSION]?.let {
|
||||
intent.putExtra(
|
||||
"SCAN_RESULT_UPC_EAN_EXTENSION",
|
||||
it.toString()
|
||||
)
|
||||
}
|
||||
metadata[ResultMetadataType.BYTE_SEGMENTS]?.let {
|
||||
var i = 0
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
for (seg in it as Iterable<ByteArray>) {
|
||||
intent.putExtra("SCAN_RESULT_BYTE_SEGMENTS_$i", seg)
|
||||
++i
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getRawBytes(result: Result): ByteArray? {
|
||||
val metadata = result.resultMetadata ?: return null
|
||||
val segments = metadata[ResultMetadataType.BYTE_SEGMENTS] ?: return null
|
||||
var bytes = ByteArray(0)
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
for (seg in segments as Iterable<ByteArray>) {
|
||||
bytes += seg
|
||||
}
|
||||
// byte segments can never be shorter than the text.
|
||||
// Zxing cuts off content prefixes like "WIFI:"
|
||||
return if (bytes.size >= result.text.length) bytes else null
|
||||
return intent
|
||||
}
|
||||
|
||||
@@ -1,21 +1,22 @@
|
||||
package de.markusfisch.android.binaryeye.activity
|
||||
|
||||
import com.google.zxing.BarcodeFormat
|
||||
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.app.initSystemBars
|
||||
import de.markusfisch.android.binaryeye.app.setFragment
|
||||
import de.markusfisch.android.binaryeye.fragment.DecodeFragment
|
||||
import de.markusfisch.android.binaryeye.fragment.EncodeFragment
|
||||
import de.markusfisch.android.binaryeye.fragment.HistoryFragment
|
||||
import de.markusfisch.android.binaryeye.fragment.PreferencesFragment
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.support.v4.app.Fragment
|
||||
import android.support.v7.app.AppCompatActivity
|
||||
import android.support.v7.widget.Toolbar
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.app.colorSystemAndToolBars
|
||||
import de.markusfisch.android.binaryeye.app.initSystemBars
|
||||
import de.markusfisch.android.binaryeye.app.setFragment
|
||||
import de.markusfisch.android.binaryeye.database.Scan
|
||||
import de.markusfisch.android.binaryeye.fragment.DecodeFragment
|
||||
import de.markusfisch.android.binaryeye.fragment.EncodeFragment
|
||||
import de.markusfisch.android.binaryeye.fragment.HistoryFragment
|
||||
import de.markusfisch.android.binaryeye.fragment.PreferencesFragment
|
||||
import de.markusfisch.android.binaryeye.view.recordToolbarHeight
|
||||
|
||||
class MainActivity : AppCompatActivity() {
|
||||
override fun onSupportNavigateUp(): Boolean {
|
||||
@@ -31,33 +32,19 @@ class MainActivity : AppCompatActivity() {
|
||||
override fun onCreate(state: Bundle?) {
|
||||
super.onCreate(state)
|
||||
setContentView(R.layout.activity_main)
|
||||
initSystemBars(this)
|
||||
|
||||
setSupportActionBar(findViewById(R.id.toolbar) as Toolbar)
|
||||
initSystemBars(this)
|
||||
val toolbar = findViewById(R.id.toolbar) as Toolbar
|
||||
recordToolbarHeight(toolbar)
|
||||
setSupportActionBar(toolbar)
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
|
||||
supportFragmentManager.addOnBackStackChangedListener {
|
||||
colorSystemAndToolBars(this@MainActivity)
|
||||
}
|
||||
|
||||
if (state == null) {
|
||||
setFragment(
|
||||
supportFragmentManager, when {
|
||||
intent?.hasExtra(PREFERENCES) == true ->
|
||||
PreferencesFragment()
|
||||
intent?.hasExtra(HISTORY) == true ->
|
||||
HistoryFragment()
|
||||
intent?.hasExtra(ENCODE) == true ->
|
||||
EncodeFragment.newInstance(
|
||||
intent.getStringExtra(ENCODE)
|
||||
)
|
||||
intent?.hasExtra(DECODED_TEXT) == true ->
|
||||
DecodeFragment.newInstance(
|
||||
intent.getStringExtra(DECODED_TEXT),
|
||||
intent.getSerializableExtra(
|
||||
DECODED_FORMAT
|
||||
) as BarcodeFormat,
|
||||
intent.getByteArrayExtra(DECODED_RAW)
|
||||
)
|
||||
else -> DecodeFragment()
|
||||
}
|
||||
)
|
||||
setFragment(supportFragmentManager, getFragmentForIntent(intent))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,9 +52,22 @@ class MainActivity : AppCompatActivity() {
|
||||
private const val PREFERENCES = "preferences"
|
||||
private const val HISTORY = "history"
|
||||
private const val ENCODE = "encode"
|
||||
private const val DECODED_TEXT = "decoded_text"
|
||||
private const val DECODED_FORMAT = "decoded_format"
|
||||
private const val DECODED_RAW = "decoded_raw"
|
||||
private const val DECODED = "decoded"
|
||||
|
||||
private fun getFragmentForIntent(intent: Intent?): Fragment {
|
||||
intent ?: return PreferencesFragment()
|
||||
return when {
|
||||
intent.hasExtra(PREFERENCES) -> PreferencesFragment()
|
||||
intent.hasExtra(HISTORY) -> HistoryFragment()
|
||||
intent.hasExtra(ENCODE) -> EncodeFragment.newInstance(
|
||||
intent.getStringExtra(ENCODE)!!
|
||||
)
|
||||
intent.hasExtra(DECODED) -> DecodeFragment.newInstance(
|
||||
intent.getParcelableExtra(DECODED)!!
|
||||
)
|
||||
else -> PreferencesFragment()
|
||||
}
|
||||
}
|
||||
|
||||
fun getPreferencesIntent(context: Context): Intent {
|
||||
val intent = Intent(context, MainActivity::class.java)
|
||||
@@ -89,9 +89,10 @@ class MainActivity : AppCompatActivity() {
|
||||
val intent = Intent(context, MainActivity::class.java)
|
||||
intent.putExtra(ENCODE, text)
|
||||
if (isExternal) {
|
||||
val flagActivityClearTask = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
|
||||
Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||
} else 0
|
||||
val flagActivityClearTask =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
|
||||
Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||
} else 0
|
||||
intent.addFlags(
|
||||
Intent.FLAG_ACTIVITY_NO_HISTORY or
|
||||
flagActivityClearTask or
|
||||
@@ -101,18 +102,9 @@ class MainActivity : AppCompatActivity() {
|
||||
return intent
|
||||
}
|
||||
|
||||
fun getDecodeIntent(
|
||||
context: Context,
|
||||
text: String,
|
||||
format: BarcodeFormat,
|
||||
raw: ByteArray? = null
|
||||
): Intent {
|
||||
fun getDecodeIntent(context: Context, scan: Scan): Intent {
|
||||
val intent = Intent(context, MainActivity::class.java)
|
||||
intent.putExtra(DECODED_TEXT, text)
|
||||
intent.putExtra(DECODED_FORMAT, format)
|
||||
if (raw != null) {
|
||||
intent.putExtra(DECODED_RAW, raw)
|
||||
}
|
||||
intent.putExtra(DECODED, scan)
|
||||
return intent
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
package de.markusfisch.android.binaryeye.activity
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Rect
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.os.Parcelable
|
||||
import android.os.Vibrator
|
||||
import android.support.v7.app.AppCompatActivity
|
||||
import android.support.v7.widget.Toolbar
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import com.google.zxing.Result
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.app.colorSystemAndToolBars
|
||||
import de.markusfisch.android.binaryeye.app.initSystemBars
|
||||
import de.markusfisch.android.binaryeye.app.vibrate
|
||||
import de.markusfisch.android.binaryeye.graphics.crop
|
||||
import de.markusfisch.android.binaryeye.graphics.loadImageUri
|
||||
import de.markusfisch.android.binaryeye.graphics.mapResult
|
||||
import de.markusfisch.android.binaryeye.view.doOnApplyWindowInsets
|
||||
import de.markusfisch.android.binaryeye.view.recordToolbarHeight
|
||||
import de.markusfisch.android.binaryeye.widget.CropImageView
|
||||
import de.markusfisch.android.binaryeye.widget.toast
|
||||
import de.markusfisch.android.binaryeye.zxing.Zxing
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
class PickActivity : AppCompatActivity() {
|
||||
private val zxing = Zxing()
|
||||
|
||||
private lateinit var vibrator: Vibrator
|
||||
private lateinit var cropImageView: CropImageView
|
||||
|
||||
override fun onCreate(state: Bundle?) {
|
||||
super.onCreate(state)
|
||||
setContentView(R.layout.activity_pick)
|
||||
|
||||
zxing.updateHints(true)
|
||||
vibrator = getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
|
||||
|
||||
initSystemBars(this)
|
||||
val toolbar = findViewById(R.id.toolbar) as Toolbar
|
||||
recordToolbarHeight(toolbar)
|
||||
setSupportActionBar(toolbar)
|
||||
|
||||
supportFragmentManager.addOnBackStackChangedListener {
|
||||
colorSystemAndToolBars(this@PickActivity)
|
||||
}
|
||||
|
||||
val bitmap = if (
|
||||
intent?.action == Intent.ACTION_SEND &&
|
||||
intent.type?.startsWith("image/") == true
|
||||
) {
|
||||
loadSentImage(intent)
|
||||
} else if (
|
||||
intent?.action == Intent.ACTION_VIEW &&
|
||||
intent.type?.startsWith("image/") == true
|
||||
) {
|
||||
loadImageToOpen(intent)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
if (bitmap == null) {
|
||||
applicationContext.toast(R.string.error_no_content)
|
||||
finish()
|
||||
return
|
||||
}
|
||||
|
||||
var result: Result? = null
|
||||
val scope = CoroutineScope(Dispatchers.IO)
|
||||
val scannedRect = Rect()
|
||||
fun scanWithinBounds() = crop(
|
||||
bitmap,
|
||||
cropImageView.normalizedRectInBounds,
|
||||
cropImageView.imageRotation
|
||||
)?.let {
|
||||
scannedRect.set(0, 0, it.width, it.height)
|
||||
scope.launch(Dispatchers.IO) {
|
||||
result = zxing.decodePositiveNegative(it)
|
||||
result?.let {
|
||||
withContext(Dispatchers.Main) {
|
||||
if (!isFinishing) {
|
||||
vibrator.vibrate()
|
||||
}
|
||||
cropImageView.updateResultPoints(
|
||||
mapResult(
|
||||
scannedRect.width(),
|
||||
scannedRect.height(),
|
||||
0,
|
||||
cropImageView.getBoundsRect(),
|
||||
it
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cropImageView = findViewById(R.id.image) as CropImageView
|
||||
cropImageView.setImageBitmap(bitmap)
|
||||
cropImageView.onScan = {
|
||||
scanWithinBounds()
|
||||
}
|
||||
cropImageView.doOnApplyWindowInsets { v, insets ->
|
||||
(v as CropImageView).windowInsets.set(insets)
|
||||
}
|
||||
|
||||
findViewById(R.id.scan).setOnClickListener {
|
||||
scanImage(result)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||
menuInflater.inflate(R.menu.activity_pick, menu)
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
return when (item.itemId) {
|
||||
R.id.rotate -> {
|
||||
rotateClockwise()
|
||||
true
|
||||
}
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
}
|
||||
|
||||
private fun rotateClockwise() {
|
||||
cropImageView.imageRotation = cropImageView.imageRotation + 90 % 360
|
||||
}
|
||||
|
||||
private fun loadSentImage(intent: Intent): Bitmap? {
|
||||
val uri = intent.getParcelableExtra<Parcelable>(
|
||||
Intent.EXTRA_STREAM
|
||||
) as? Uri ?: return null
|
||||
return loadImageUri(contentResolver, uri)
|
||||
}
|
||||
|
||||
private fun loadImageToOpen(intent: Intent): Bitmap? {
|
||||
val uri = intent.data ?: return null
|
||||
return loadImageUri(contentResolver, uri)
|
||||
}
|
||||
|
||||
private fun scanImage(result: Result?) {
|
||||
if (result != null) {
|
||||
showResult(this, result)
|
||||
finish()
|
||||
return
|
||||
}
|
||||
applicationContext.toast(R.string.no_barcode_found)
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package de.markusfisch.android.binaryeye.activity
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.support.v7.app.AppCompatActivity
|
||||
import de.markusfisch.android.binaryeye.app.setRestartCount
|
||||
|
||||
class SplashActivity : AppCompatActivity() {
|
||||
override fun onCreate(state: Bundle?) {
|
||||
@@ -12,6 +13,7 @@ class SplashActivity : AppCompatActivity() {
|
||||
// because that would happen after the app is fully
|
||||
// initialized what is too late
|
||||
|
||||
setRestartCount(intent)
|
||||
startActivity(Intent(applicationContext, CameraActivity::class.java))
|
||||
finish()
|
||||
}
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
package de.markusfisch.android.binaryeye.adapter
|
||||
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.data.Database
|
||||
|
||||
import android.content.Context
|
||||
import android.database.Cursor
|
||||
import android.view.LayoutInflater
|
||||
@@ -10,22 +7,48 @@ import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.CursorAdapter
|
||||
import android.widget.TextView
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.database.Database
|
||||
|
||||
class ScansAdapter(context: Context, cursor: Cursor) :
|
||||
CursorAdapter(context, cursor, false) {
|
||||
var selectedScanId = 0L
|
||||
var selectedScanPosition = -1
|
||||
|
||||
private val idIndex = cursor.getColumnIndex(Database.SCANS_ID)
|
||||
private val timeIndex = cursor.getColumnIndex(Database.SCANS_DATETIME)
|
||||
private val nameIndex = cursor.getColumnIndex(Database.SCANS_NAME)
|
||||
private val contentIndex = cursor.getColumnIndex(Database.SCANS_CONTENT)
|
||||
private val formatIndex = cursor.getColumnIndex(Database.SCANS_FORMAT)
|
||||
|
||||
fun select(id: Long, position: Int) {
|
||||
selectedScanId = id
|
||||
selectedScanPosition = position
|
||||
}
|
||||
|
||||
fun clearSelection() {
|
||||
selectedScanId = 0L
|
||||
selectedScanPosition = -1
|
||||
}
|
||||
|
||||
fun getSelectedContent() = if (selectedScanPosition < 0) {
|
||||
null
|
||||
} else {
|
||||
val cursor = getItem(selectedScanPosition)
|
||||
if (cursor is Cursor) {
|
||||
cursor.getString(contentIndex)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
override fun newView(
|
||||
context: Context,
|
||||
cursor: Cursor,
|
||||
parent: ViewGroup
|
||||
): View {
|
||||
return LayoutInflater.from(parent.context).inflate(
|
||||
R.layout.row_scan, parent, false
|
||||
)
|
||||
}
|
||||
): View? = LayoutInflater.from(parent.context).inflate(
|
||||
R.layout.row_scan, parent, false
|
||||
)
|
||||
|
||||
override fun bindView(
|
||||
view: View,
|
||||
@@ -33,14 +56,27 @@ class ScansAdapter(context: Context, cursor: Cursor) :
|
||||
cursor: Cursor
|
||||
) {
|
||||
val holder = getViewHolder(view)
|
||||
val content = cursor.getString(contentIndex)
|
||||
holder.timeView.text = cursor.getString(timeIndex)
|
||||
holder.contentView.text = if (content.isEmpty()) {
|
||||
context.getString(R.string.binary_data)
|
||||
} else {
|
||||
content
|
||||
val name = cursor.getString(nameIndex)
|
||||
val content = cursor.getString(contentIndex)
|
||||
var icon = 0
|
||||
holder.contentView.text = when {
|
||||
name?.isNotEmpty() == true -> {
|
||||
icon = R.drawable.ic_label
|
||||
name
|
||||
}
|
||||
content?.isEmpty() == true -> context.getString(R.string.binary_data)
|
||||
else -> content
|
||||
}
|
||||
holder.contentView.setCompoundDrawablesWithIntrinsicBounds(
|
||||
icon, 0, 0, 0
|
||||
)
|
||||
holder.formatView.text = cursor.getString(formatIndex)
|
||||
// view.isSelected needs to be put on the queue to work
|
||||
val selected = cursor.getLong(idIndex) == selectedScanId
|
||||
view.post {
|
||||
view.isSelected = selected
|
||||
}
|
||||
}
|
||||
|
||||
private fun getViewHolder(view: View): ViewHolder {
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
package de.markusfisch.android.binaryeye.app
|
||||
|
||||
import android.content.Context
|
||||
import android.support.v7.app.AlertDialog
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
|
||||
suspend inline fun <T : Any> alertDialog(
|
||||
context: Context,
|
||||
crossinline build: AlertDialog.Builder.(resume: (T?) -> Unit) -> Unit
|
||||
): T? = withContext(Dispatchers.Main) {
|
||||
suspendCoroutine<T?> { continuation ->
|
||||
AlertDialog.Builder(context).apply {
|
||||
setOnCancelListener {
|
||||
continuation.resume(null)
|
||||
}
|
||||
build(continuation::resume)
|
||||
show()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,9 @@
|
||||
package de.markusfisch.android.binaryeye.app
|
||||
|
||||
import de.markusfisch.android.binaryeye.data.Database
|
||||
import de.markusfisch.android.binaryeye.preference.Preferences
|
||||
|
||||
import android.app.Application
|
||||
import android.support.v8.renderscript.RenderScript
|
||||
import de.markusfisch.android.binaryeye.database.Database
|
||||
import de.markusfisch.android.binaryeye.preference.Preferences
|
||||
|
||||
val db = Database()
|
||||
val prefs = Preferences()
|
||||
@@ -12,12 +11,11 @@ val prefs = Preferences()
|
||||
class BinaryEyeApp : Application() {
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
|
||||
// required to make RenderScript work for Lineage 16.0
|
||||
// possibly because of a system/device bug
|
||||
RenderScript.forceCompat()
|
||||
|
||||
db.open(this)
|
||||
prefs.init(this)
|
||||
|
||||
if (prefs.forceCompat) {
|
||||
RenderScript.forceCompat()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
package de.markusfisch.android.binaryeye.app
|
||||
|
||||
import android.content.ClipData
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
|
||||
fun Context.copyToClipboard(text: String) {
|
||||
val service = getSystemService(Context.CLIPBOARD_SERVICE)
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) {
|
||||
@Suppress("DEPRECATION")
|
||||
(service as android.text.ClipboardManager).text = text
|
||||
} else {
|
||||
(service as android.content.ClipboardManager).setPrimaryClip(
|
||||
ClipData.newPlainText("simple text", text)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,9 @@
|
||||
package de.markusfisch.android.binaryeye.app
|
||||
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.support.v4.app.Fragment
|
||||
import android.support.v4.app.FragmentManager
|
||||
import android.support.v4.app.FragmentTransaction
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
|
||||
fun setFragment(fm: FragmentManager?, fragment: Fragment) {
|
||||
fm?.let { getTransaction(fm, fragment).commit() }
|
||||
@@ -19,6 +17,4 @@ fun addFragment(fm: FragmentManager?, fragment: Fragment) {
|
||||
private fun getTransaction(
|
||||
fm: FragmentManager,
|
||||
fragment: Fragment
|
||||
): FragmentTransaction {
|
||||
return fm.beginTransaction().replace(R.id.content_frame, fragment)
|
||||
}
|
||||
) = fm.beginTransaction().replace(R.id.content_frame, fragment)
|
||||
@@ -1,7 +1,14 @@
|
||||
package de.markusfisch.android.binaryeye.app
|
||||
|
||||
import java.util.regex.Pattern
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
|
||||
private val nonPrintable = Pattern.compile("[\\x00-\\x08\\x0e-\\x1f]")
|
||||
private val nonPrintable = "[\\x00-\\x08\\x0e-\\x1f]".toRegex()
|
||||
|
||||
fun hasNonPrintableCharacters(s: String) = nonPrintable.matcher(s).find()
|
||||
fun hasNonPrintableCharacters(s: String) = nonPrintable.containsMatchIn(s)
|
||||
|
||||
fun parseAndNormalizeUri(input: String): Uri = Uri.parse(input).let {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) it.normalizeScheme() else it
|
||||
}
|
||||
|
||||
fun ByteArray.toHexString() = joinToString("") { "%02X".format(it) }
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
package de.markusfisch.android.binaryeye.app
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.ContentValues
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.os.Environment
|
||||
import android.provider.MediaStore
|
||||
import android.support.annotation.MainThread
|
||||
import android.widget.EditText
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
import java.io.OutputStream
|
||||
|
||||
fun addSuffixIfNotGiven(name: String, suffix: String): String {
|
||||
val trimmed = name.trim()
|
||||
return if (suffix.isNotEmpty() && !trimmed.endsWith(suffix)) {
|
||||
"$trimmed$suffix"
|
||||
} else {
|
||||
trimmed
|
||||
}
|
||||
}
|
||||
|
||||
// Dialogs doesn't have any root layout, need to be inflated with null root
|
||||
@Suppress("InflateParams")
|
||||
@MainThread
|
||||
suspend fun Activity.askForFileName(suffix: String = ""): String? {
|
||||
val view = layoutInflater.inflate(R.layout.dialog_save_file, null)
|
||||
val editText = view.findViewById<EditText>(R.id.file_name)
|
||||
return alertDialog<String>(this as Context) { resume ->
|
||||
setView(view)
|
||||
setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
resume(editText.text.toString())
|
||||
}
|
||||
}?.let { name ->
|
||||
addSuffixIfNotGiven(name, suffix)
|
||||
}
|
||||
}
|
||||
|
||||
fun Boolean.toSaveResult() = if (this) {
|
||||
R.string.saved_in_downloads
|
||||
} else {
|
||||
R.string.error_saving_file
|
||||
}
|
||||
|
||||
fun writeExternalFile(
|
||||
context: Context,
|
||||
fileName: String,
|
||||
mimeType: String,
|
||||
write: (outputStream: OutputStream) -> Unit
|
||||
): Boolean = try {
|
||||
openExternalOutputStream(context, fileName, mimeType).use { write(it) }
|
||||
true
|
||||
} catch (e: IOException) {
|
||||
false
|
||||
}
|
||||
|
||||
private fun openExternalOutputStream(
|
||||
context: Context,
|
||||
fileName: String,
|
||||
mimeType: String
|
||||
): OutputStream = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
||||
@Suppress("DEPRECATION")
|
||||
val file = File(
|
||||
Environment.getExternalStoragePublicDirectory(
|
||||
Environment.DIRECTORY_DOWNLOADS
|
||||
),
|
||||
fileName
|
||||
)
|
||||
if (file.exists()) {
|
||||
throw IOException()
|
||||
}
|
||||
FileOutputStream(file)
|
||||
} else {
|
||||
val resolver = context.contentResolver
|
||||
val uri = resolver.insert(
|
||||
MediaStore.Downloads.EXTERNAL_CONTENT_URI,
|
||||
ContentValues().apply {
|
||||
put(MediaStore.Downloads.DISPLAY_NAME, fileName)
|
||||
put(MediaStore.Downloads.MIME_TYPE, mimeType)
|
||||
}
|
||||
) ?: throw IOException()
|
||||
resolver.openOutputStream(uri) ?: throw IOException()
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package de.markusfisch.android.binaryeye.app
|
||||
|
||||
import android.content.Context
|
||||
import android.view.View
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
|
||||
fun Context.hideSoftKeyboard(view: View) {
|
||||
(getSystemService(
|
||||
Context.INPUT_METHOD_SERVICE
|
||||
) as InputMethodManager).hideSoftInputFromWindow(
|
||||
view.windowToken, 0
|
||||
)
|
||||
}
|
||||
@@ -1,38 +1,53 @@
|
||||
package de.markusfisch.android.binaryeye.app
|
||||
|
||||
import android.Manifest
|
||||
import android.app.Activity
|
||||
import android.content.pm.PackageManager
|
||||
import android.support.v4.app.ActivityCompat
|
||||
import android.support.v4.content.ContextCompat
|
||||
import android.Manifest
|
||||
|
||||
fun hasCameraPermission(activity: Activity, requestCode: Int): Boolean {
|
||||
return hasPermission(activity, Manifest.permission.CAMERA, requestCode)
|
||||
}
|
||||
|
||||
fun hasWritePermission(activity: Activity, requestCode: Int = 1): Boolean {
|
||||
const val PERMISSION_CAMERA = 1
|
||||
fun hasCameraPermission(activity: Activity): Boolean {
|
||||
return hasPermission(
|
||||
activity,
|
||||
Manifest.permission.WRITE_EXTERNAL_STORAGE,
|
||||
requestCode
|
||||
Manifest.permission.CAMERA,
|
||||
PERMISSION_CAMERA
|
||||
)
|
||||
}
|
||||
|
||||
fun hasPermission(
|
||||
const val PERMISSION_WRITE = 2
|
||||
fun hasWritePermission(activity: Activity): Boolean {
|
||||
return hasPermission(
|
||||
activity,
|
||||
Manifest.permission.WRITE_EXTERNAL_STORAGE,
|
||||
PERMISSION_WRITE
|
||||
)
|
||||
}
|
||||
|
||||
const val PERMISSION_LOCATION = 3
|
||||
fun hasLocationPermission(activity: Activity): Boolean {
|
||||
return hasPermission(
|
||||
activity,
|
||||
Manifest.permission.ACCESS_FINE_LOCATION,
|
||||
PERMISSION_LOCATION
|
||||
)
|
||||
}
|
||||
|
||||
private fun hasPermission(
|
||||
activity: Activity,
|
||||
permission: String,
|
||||
requestCode: Int
|
||||
): Boolean {
|
||||
return if (ContextCompat.checkSelfPermission(activity, permission) !=
|
||||
PackageManager.PERMISSION_GRANTED
|
||||
) {
|
||||
ActivityCompat.requestPermissions(
|
||||
activity,
|
||||
arrayOf(permission),
|
||||
requestCode
|
||||
)
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
) = if (ContextCompat.checkSelfPermission(
|
||||
activity,
|
||||
permission
|
||||
) != PackageManager.PERMISSION_GRANTED
|
||||
) {
|
||||
ActivityCompat.requestPermissions(
|
||||
activity,
|
||||
arrayOf(permission),
|
||||
requestCode
|
||||
)
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
package de.markusfisch.android.binaryeye.app
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import de.markusfisch.android.binaryeye.activity.SplashActivity
|
||||
|
||||
private const val RESTART_COUNT = "restart_count"
|
||||
|
||||
private var restartCount = 0
|
||||
|
||||
fun setRestartCount(intent: Intent?) {
|
||||
intent?.let {
|
||||
restartCount = intent.getIntExtra(RESTART_COUNT, restartCount)
|
||||
}
|
||||
}
|
||||
|
||||
fun restartApp(activity: Activity? = null) {
|
||||
if (activity != null && restartCount < 3) {
|
||||
val intent = Intent(activity, SplashActivity::class.java)
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
intent.putExtra(RESTART_COUNT, ++restartCount)
|
||||
activity.startActivity(intent)
|
||||
activity.finish()
|
||||
}
|
||||
Runtime.getRuntime().exit(0)
|
||||
}
|
||||
@@ -1,15 +1,13 @@
|
||||
package de.markusfisch.android.binaryeye.app
|
||||
|
||||
import de.markusfisch.android.binaryeye.BuildConfig
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.support.v4.content.FileProvider
|
||||
import android.widget.Toast
|
||||
|
||||
import de.markusfisch.android.binaryeye.BuildConfig
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.widget.toast
|
||||
import java.io.File
|
||||
|
||||
fun shareText(context: Context, text: String, type: String = "text/plain") {
|
||||
@@ -31,19 +29,17 @@ fun execShareIntent(context: Context, intent: Intent) {
|
||||
if (intent.resolveActivity(context.packageManager) != null) {
|
||||
context.startActivity(intent)
|
||||
} else {
|
||||
Toast.makeText(
|
||||
context,
|
||||
R.string.cannot_resolve_action,
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
context.toast(R.string.cannot_resolve_action)
|
||||
}
|
||||
}
|
||||
|
||||
fun shareFile(context: Context, file: File, type: String) {
|
||||
shareUri(context, getUriForFile(context, file), type)
|
||||
getUriForFile(context, file)?.let {
|
||||
shareUri(context, it, type)
|
||||
}
|
||||
}
|
||||
|
||||
fun getUriForFile(context: Context, file: File): Uri {
|
||||
fun getUriForFile(context: Context, file: File): Uri? {
|
||||
return if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
|
||||
Uri.fromFile(file)
|
||||
} else {
|
||||
|
||||
@@ -1,46 +1,117 @@
|
||||
package de.markusfisch.android.binaryeye.app
|
||||
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
|
||||
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.v4.view.ViewCompat
|
||||
import android.support.v7.app.AppCompatActivity
|
||||
import android.view.View
|
||||
import android.view.Window
|
||||
import android.widget.AbsListView
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
|
||||
fun initSystemBars(activity: AppCompatActivity?) {
|
||||
val view = activity?.findViewById(R.id.main_layout) ?: return
|
||||
setWindowInsetListener(view)
|
||||
setSystemBarColor(
|
||||
activity.window,
|
||||
ContextCompat.getColor(activity, android.R.color.transparent)
|
||||
val systemBarScrollListener = object : AbsListView.OnScrollListener {
|
||||
override fun onScroll(
|
||||
view: AbsListView,
|
||||
firstVisibleItem: Int,
|
||||
visibleItemCount: Int,
|
||||
totalItemCount: Int
|
||||
) {
|
||||
// give Android some time to settle down before running this,
|
||||
// not putting it on the queue makes it only work sometimes
|
||||
view.post {
|
||||
val scrolled = firstVisibleItem > 0 ||
|
||||
(totalItemCount > 0 && firstChildScrolled(view))
|
||||
val scrollable = if (scrolled) {
|
||||
true
|
||||
} else {
|
||||
totalItemCount > 0 && lastChildOutOfView(view)
|
||||
}
|
||||
colorSystemAndToolBars(view.context, scrolled, scrollable)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onScrollStateChanged(
|
||||
view: AbsListView,
|
||||
scrollState: Int
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
private fun firstChildScrolled(listView: AbsListView): Boolean {
|
||||
val child = listView.getChildAt(0)
|
||||
return child != null && child.top < listView.paddingTop
|
||||
}
|
||||
|
||||
private fun lastChildOutOfView(listView: AbsListView): Boolean {
|
||||
val child = listView.getChildAt(listView.lastVisiblePosition)
|
||||
return child != null && child.bottom >= listView.height
|
||||
}
|
||||
|
||||
fun initSystemBars(activity: AppCompatActivity) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
activity.window.decorView.systemUiVisibility =
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_STABLE or
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
|
||||
}
|
||||
colorSystemAndToolBars(activity)
|
||||
}
|
||||
|
||||
private var statusBarColorLocked = false
|
||||
fun lockStatusBarColor() {
|
||||
statusBarColorLocked = true
|
||||
}
|
||||
|
||||
fun unlockStatusBarColor() {
|
||||
statusBarColorLocked = false
|
||||
}
|
||||
|
||||
private var translucentPrimaryColor = 0
|
||||
private val actionBarBackground = ColorDrawable()
|
||||
fun colorSystemAndToolBars(
|
||||
context: Context,
|
||||
scrolled: Boolean = false,
|
||||
scrollable: Boolean = false
|
||||
) {
|
||||
if (translucentPrimaryColor == 0) {
|
||||
translucentPrimaryColor = ContextCompat.getColor(
|
||||
context,
|
||||
R.color.primary_translucent
|
||||
)
|
||||
}
|
||||
val topColor = if (scrolled) translucentPrimaryColor else 0
|
||||
val activity = getAppCompatActivity(context) ?: return
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
val window = activity.window
|
||||
if (!statusBarColorLocked) {
|
||||
window.statusBarColor = topColor
|
||||
}
|
||||
window.navigationBarColor = if (scrolled || scrollable) {
|
||||
translucentPrimaryColor
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
activity.supportActionBar?.setBackgroundDrawable(
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
|
||||
// avoid allocation on Honeycomb and better
|
||||
actionBarBackground.color = topColor
|
||||
actionBarBackground
|
||||
} else {
|
||||
// ColorDrawable.setColor() doesn't exist pre Honeycomb
|
||||
ColorDrawable(topColor)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
fun setWindowInsetListener(view: View) {
|
||||
ViewCompat.setOnApplyWindowInsetsListener(view) { v, insets ->
|
||||
if (insets.hasSystemWindowInsets()) {
|
||||
view.setPadding(
|
||||
insets.getSystemWindowInsetLeft(),
|
||||
insets.getSystemWindowInsetTop(),
|
||||
insets.getSystemWindowInsetRight(),
|
||||
insets.getSystemWindowInsetBottom()
|
||||
)
|
||||
private fun getAppCompatActivity(context: Context): AppCompatActivity? {
|
||||
var ctx = context
|
||||
while (ctx is ContextWrapper) {
|
||||
if (ctx is AppCompatActivity) {
|
||||
return ctx
|
||||
}
|
||||
insets.consumeSystemWindowInsets()
|
||||
ctx = ctx.baseContext ?: break
|
||||
}
|
||||
}
|
||||
|
||||
fun setSystemBarColor(window: Window, color: Int): Boolean {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
|
||||
return false
|
||||
}
|
||||
window.statusBarColor = color
|
||||
window.navigationBarColor = color
|
||||
window.decorView.systemUiVisibility =
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_STABLE or
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
|
||||
return true
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
package de.markusfisch.android.binaryeye.app
|
||||
|
||||
import android.os.Build
|
||||
import android.os.VibrationEffect
|
||||
import android.os.Vibrator
|
||||
|
||||
private const val MILLISECONDS = 100L
|
||||
|
||||
fun Vibrator.vibrate() {
|
||||
if (!prefs.vibrate) {
|
||||
return
|
||||
}
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
||||
@Suppress("DEPRECATION")
|
||||
this.vibrate(MILLISECONDS)
|
||||
} else {
|
||||
this.vibrate(
|
||||
VibrationEffect.createOneShot(
|
||||
MILLISECONDS,
|
||||
VibrationEffect.DEFAULT_AMPLITUDE
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,114 +0,0 @@
|
||||
package de.markusfisch.android.binaryeye.data
|
||||
|
||||
import de.markusfisch.android.binaryeye.app.hasNonPrintableCharacters
|
||||
|
||||
import android.content.ContentValues
|
||||
import android.content.Context
|
||||
import android.database.Cursor
|
||||
import android.database.sqlite.SQLiteDatabase
|
||||
import android.database.sqlite.SQLiteOpenHelper
|
||||
import android.text.format.DateFormat
|
||||
|
||||
class Database {
|
||||
private lateinit var db: SQLiteDatabase
|
||||
|
||||
fun open(context: Context) {
|
||||
db = OpenHelper(context).writableDatabase
|
||||
}
|
||||
|
||||
fun getScans(): Cursor? = db.rawQuery(
|
||||
"""SELECT
|
||||
$SCANS_ID,
|
||||
$SCANS_DATETIME,
|
||||
$SCANS_CONTENT,
|
||||
$SCANS_FORMAT
|
||||
FROM $SCANS
|
||||
ORDER BY $SCANS_DATETIME DESC
|
||||
""", null
|
||||
)
|
||||
|
||||
fun getScan(id: Long): Cursor? = db.rawQuery(
|
||||
"""SELECT
|
||||
$SCANS_DATETIME,
|
||||
$SCANS_CONTENT,
|
||||
$SCANS_RAW,
|
||||
$SCANS_FORMAT
|
||||
FROM $SCANS
|
||||
WHERE $SCANS_ID = ?
|
||||
""", arrayOf("$id")
|
||||
)
|
||||
|
||||
fun insertScan(
|
||||
timestamp: Long,
|
||||
content: String,
|
||||
raw: ByteArray?,
|
||||
format: String
|
||||
): Long {
|
||||
val cv = ContentValues()
|
||||
cv.put(
|
||||
SCANS_DATETIME, DateFormat.format(
|
||||
"yyyy-MM-dd HH:mm:ss",
|
||||
timestamp
|
||||
).toString()
|
||||
)
|
||||
if (hasNonPrintableCharacters(content) && raw != null) {
|
||||
cv.put(SCANS_CONTENT, "")
|
||||
cv.put(SCANS_RAW, raw)
|
||||
} else {
|
||||
cv.put(SCANS_CONTENT, content)
|
||||
}
|
||||
cv.put(SCANS_FORMAT, format)
|
||||
return db.insert(SCANS, null, cv)
|
||||
}
|
||||
|
||||
fun removeScan(id: Long) {
|
||||
db.delete(SCANS, "$SCANS_ID = ?", arrayOf("$id"))
|
||||
}
|
||||
|
||||
fun removeScans() {
|
||||
db.delete(SCANS, null, null)
|
||||
}
|
||||
|
||||
private class OpenHelper(context: Context) :
|
||||
SQLiteOpenHelper(context, "history.db", null, 2) {
|
||||
override fun onCreate(db: SQLiteDatabase) {
|
||||
createScans(db)
|
||||
}
|
||||
|
||||
override fun onUpgrade(
|
||||
db: SQLiteDatabase,
|
||||
oldVersion: Int,
|
||||
newVersion: Int
|
||||
) {
|
||||
if (oldVersion < 2) {
|
||||
addRawColumn(db)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val SCANS = "scans"
|
||||
const val SCANS_ID = "_id"
|
||||
const val SCANS_DATETIME = "_datetime"
|
||||
const val SCANS_CONTENT = "content"
|
||||
const val SCANS_RAW = "raw"
|
||||
const val SCANS_FORMAT = "format"
|
||||
|
||||
private fun createScans(db: SQLiteDatabase) {
|
||||
db.execSQL("DROP TABLE IF EXISTS $SCANS")
|
||||
db.execSQL(
|
||||
"""CREATE TABLE $SCANS (
|
||||
$SCANS_ID INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
$SCANS_DATETIME DATETIME NOT NULL,
|
||||
$SCANS_CONTENT TEXT NOT NULL,
|
||||
$SCANS_RAW BLOB,
|
||||
$SCANS_FORMAT TEXT NOT NULL
|
||||
)"""
|
||||
)
|
||||
}
|
||||
|
||||
private fun addRawColumn(db: SQLiteDatabase) {
|
||||
db.execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_RAW BLOB")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
package de.markusfisch.android.binaryeye.database
|
||||
|
||||
import android.content.Context
|
||||
import android.database.Cursor
|
||||
import de.markusfisch.android.binaryeye.app.toHexString
|
||||
import de.markusfisch.android.binaryeye.app.writeExternalFile
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.OutputStream
|
||||
|
||||
fun exportCsv(
|
||||
context: Context,
|
||||
name: String,
|
||||
cursor: Cursor,
|
||||
delimiter: String
|
||||
) = writeExternalFile(context, name, "text/csv") { outputStream ->
|
||||
exportCsv(outputStream, cursor, delimiter)
|
||||
}
|
||||
|
||||
fun exportCsv(cursor: Cursor, delimiter: String): String? {
|
||||
val outputStream = ByteArrayOutputStream()
|
||||
exportCsv(outputStream, cursor, delimiter)
|
||||
return outputStream.toString()
|
||||
}
|
||||
|
||||
private fun exportCsv(
|
||||
outputStream: OutputStream,
|
||||
cursor: Cursor,
|
||||
delimiter: String
|
||||
) {
|
||||
if (!cursor.moveToFirst()) {
|
||||
return
|
||||
}
|
||||
val columns = arrayOf(
|
||||
Database.SCANS_DATETIME,
|
||||
Database.SCANS_FORMAT,
|
||||
Database.SCANS_CONTENT,
|
||||
Database.SCANS_ERROR_CORRECTION_LEVEL,
|
||||
Database.SCANS_ISSUE_NUMBER,
|
||||
Database.SCANS_ORIENTATION,
|
||||
Database.SCANS_OTHER_META_DATA,
|
||||
Database.SCANS_PDF417_EXTRA_METADATA,
|
||||
Database.SCANS_POSSIBLE_COUNTRY,
|
||||
Database.SCANS_SUGGESTED_PRICE,
|
||||
Database.SCANS_UPC_EAN_EXTENSION
|
||||
)
|
||||
val indices = columns.map {
|
||||
cursor.getColumnIndex(it)
|
||||
}
|
||||
val contentIndex = cursor.getColumnIndex(Database.SCANS_CONTENT)
|
||||
val rawIndex = cursor.getColumnIndex(Database.SCANS_RAW)
|
||||
outputStream.write(
|
||||
columns.joinToString(
|
||||
delimiter,
|
||||
postfix = "\n"
|
||||
).toByteArray()
|
||||
)
|
||||
do {
|
||||
var deviation: Pair<Int, String>? = null
|
||||
if (cursor.getString(contentIndex)?.isEmpty() == true) {
|
||||
deviation = Pair(
|
||||
contentIndex,
|
||||
cursor.getBlob(rawIndex).toHexString()
|
||||
)
|
||||
}
|
||||
outputStream.write(
|
||||
cursor.toCsvRecord(indices, delimiter, deviation)
|
||||
)
|
||||
} while (cursor.moveToNext())
|
||||
}
|
||||
|
||||
private fun Cursor.toCsvRecord(
|
||||
indices: List<Int>,
|
||||
delimiter: String,
|
||||
deviation: Pair<Int, String>?
|
||||
): ByteArray {
|
||||
val sb = StringBuilder()
|
||||
indices.forEach {
|
||||
val value = if (deviation?.first == it) {
|
||||
deviation.second
|
||||
} else {
|
||||
this.getString(it)
|
||||
}
|
||||
sb.append(value?.quoteAndEscape() ?: "")
|
||||
sb.append(delimiter)
|
||||
}
|
||||
sb.append("\n")
|
||||
return sb.toString().toByteArray()
|
||||
}
|
||||
|
||||
private fun String.quoteAndEscape() = "\"${this
|
||||
.replace("\n", " ")
|
||||
.replace("\"", "\"\"")}\""
|
||||
@@ -0,0 +1,285 @@
|
||||
package de.markusfisch.android.binaryeye.database
|
||||
|
||||
import android.content.ContentValues
|
||||
import android.content.Context
|
||||
import android.database.Cursor
|
||||
import android.database.sqlite.SQLiteDatabase
|
||||
import android.database.sqlite.SQLiteOpenHelper
|
||||
import de.markusfisch.android.binaryeye.app.hasNonPrintableCharacters
|
||||
import de.markusfisch.android.binaryeye.app.prefs
|
||||
|
||||
class Database {
|
||||
private lateinit var db: SQLiteDatabase
|
||||
|
||||
fun open(context: Context) {
|
||||
db = OpenHelper(context).writableDatabase
|
||||
}
|
||||
|
||||
fun getScans(query: String? = null): Cursor? = db.rawQuery(
|
||||
"""SELECT
|
||||
$SCANS_ID,
|
||||
$SCANS_DATETIME,
|
||||
$SCANS_NAME,
|
||||
$SCANS_CONTENT,
|
||||
$SCANS_FORMAT
|
||||
FROM $SCANS
|
||||
${getWhereClause(query)}
|
||||
ORDER BY $SCANS_DATETIME DESC
|
||||
""", getWhereArguments(query)
|
||||
)
|
||||
|
||||
fun getScansDetailed(query: String? = null): Cursor? = db.rawQuery(
|
||||
"""SELECT
|
||||
$SCANS_ID,
|
||||
$SCANS_DATETIME,
|
||||
$SCANS_NAME,
|
||||
$SCANS_CONTENT,
|
||||
$SCANS_RAW,
|
||||
$SCANS_FORMAT,
|
||||
$SCANS_ERROR_CORRECTION_LEVEL,
|
||||
$SCANS_ISSUE_NUMBER,
|
||||
$SCANS_ORIENTATION,
|
||||
$SCANS_OTHER_META_DATA,
|
||||
$SCANS_PDF417_EXTRA_METADATA,
|
||||
$SCANS_POSSIBLE_COUNTRY,
|
||||
$SCANS_SUGGESTED_PRICE,
|
||||
$SCANS_UPC_EAN_EXTENSION
|
||||
FROM $SCANS
|
||||
${getWhereClause(query)}
|
||||
ORDER BY $SCANS_DATETIME DESC
|
||||
""", getWhereArguments(query)
|
||||
)
|
||||
|
||||
private fun getWhereClause(
|
||||
query: String?,
|
||||
prefix: String = "WHERE"
|
||||
) = if (query?.isNotEmpty() == true) {
|
||||
"""$prefix $SCANS_CONTENT LIKE ?
|
||||
OR $SCANS_NAME LIKE ?"""
|
||||
} else {
|
||||
""
|
||||
}
|
||||
|
||||
private fun getWhereArguments(
|
||||
query: String?
|
||||
) = if (query?.isNotEmpty() == true) {
|
||||
val instr = "%$query%"
|
||||
arrayOf(instr, instr)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
fun getScan(id: Long): Scan? = db.rawQuery(
|
||||
"""SELECT
|
||||
$SCANS_ID,
|
||||
$SCANS_DATETIME,
|
||||
$SCANS_NAME,
|
||||
$SCANS_CONTENT,
|
||||
$SCANS_RAW,
|
||||
$SCANS_FORMAT,
|
||||
$SCANS_ERROR_CORRECTION_LEVEL,
|
||||
$SCANS_ISSUE_NUMBER,
|
||||
$SCANS_ORIENTATION,
|
||||
$SCANS_OTHER_META_DATA,
|
||||
$SCANS_PDF417_EXTRA_METADATA,
|
||||
$SCANS_POSSIBLE_COUNTRY,
|
||||
$SCANS_SUGGESTED_PRICE,
|
||||
$SCANS_UPC_EAN_EXTENSION
|
||||
FROM $SCANS
|
||||
WHERE $SCANS_ID = ?
|
||||
""", arrayOf("$id")
|
||||
)?.use {
|
||||
if (it.moveToFirst()) {
|
||||
Scan(
|
||||
it.getString(SCANS_CONTENT),
|
||||
it.getBlob(SCANS_RAW),
|
||||
it.getString(SCANS_FORMAT),
|
||||
it.getString(SCANS_ERROR_CORRECTION_LEVEL),
|
||||
it.getString(SCANS_ISSUE_NUMBER),
|
||||
it.getString(SCANS_ORIENTATION),
|
||||
it.getString(SCANS_OTHER_META_DATA),
|
||||
it.getString(SCANS_PDF417_EXTRA_METADATA),
|
||||
it.getString(SCANS_POSSIBLE_COUNTRY),
|
||||
it.getString(SCANS_SUGGESTED_PRICE),
|
||||
it.getString(SCANS_UPC_EAN_EXTENSION),
|
||||
it.getString(SCANS_DATETIME),
|
||||
it.getLong(SCANS_ID)
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun insertScan(scan: Scan): Long {
|
||||
val cv = ContentValues()
|
||||
cv.put(SCANS_DATETIME, scan.timestamp)
|
||||
val isRaw = hasNonPrintableCharacters(scan.content)
|
||||
if (isRaw) {
|
||||
cv.put(SCANS_CONTENT, "")
|
||||
cv.put(SCANS_RAW, scan.raw ?: scan.content.toByteArray())
|
||||
} else {
|
||||
cv.put(SCANS_CONTENT, scan.content)
|
||||
}
|
||||
cv.put(SCANS_FORMAT, scan.format)
|
||||
scan.errorCorrectionLevel?.let {
|
||||
cv.put(SCANS_ERROR_CORRECTION_LEVEL, it)
|
||||
}
|
||||
scan.issueNumber?.let { cv.put(SCANS_ISSUE_NUMBER, it) }
|
||||
scan.orientation?.let { cv.put(SCANS_ORIENTATION, it) }
|
||||
scan.otherMetaData?.let { cv.put(SCANS_OTHER_META_DATA, it) }
|
||||
scan.pdf417ExtraMetaData?.let { cv.put(SCANS_PDF417_EXTRA_METADATA, it) }
|
||||
scan.possibleCountry?.let { cv.put(SCANS_POSSIBLE_COUNTRY, it) }
|
||||
scan.suggestedPrice?.let { cv.put(SCANS_SUGGESTED_PRICE, it) }
|
||||
scan.upcEanExtension?.let { cv.put(SCANS_UPC_EAN_EXTENSION, it) }
|
||||
if (prefs.ignoreConsecutiveDuplicates) {
|
||||
val id = getIdOfLastScan(
|
||||
cv.get(SCANS_CONTENT) as String,
|
||||
if (isRaw) cv.get(SCANS_RAW) as ByteArray else null,
|
||||
scan.format
|
||||
)
|
||||
if (id > 0L) {
|
||||
return id
|
||||
}
|
||||
}
|
||||
return db.insert(SCANS, null, cv)
|
||||
}
|
||||
|
||||
private fun getIdOfLastScan(
|
||||
content: String,
|
||||
raw: ByteArray?,
|
||||
format: String
|
||||
): Long {
|
||||
return db.rawQuery(
|
||||
"""SELECT
|
||||
$SCANS_ID,
|
||||
$SCANS_CONTENT,
|
||||
$SCANS_RAW,
|
||||
$SCANS_FORMAT
|
||||
FROM $SCANS
|
||||
ORDER BY $SCANS_ID DESC
|
||||
LIMIT 1
|
||||
""", null
|
||||
)?.use {
|
||||
if (it.count > 0 &&
|
||||
it.moveToFirst() &&
|
||||
it.getString(it.getColumnIndex(SCANS_CONTENT)) == content &&
|
||||
(raw == null || it.getBlob(it.getColumnIndex(SCANS_RAW))
|
||||
?.contentEquals(raw) == true) &&
|
||||
it.getString(it.getColumnIndex(SCANS_FORMAT)) == format
|
||||
) {
|
||||
it.getLong(it.getColumnIndex(SCANS_ID))
|
||||
} else {
|
||||
0L
|
||||
}
|
||||
} ?: 0L
|
||||
}
|
||||
|
||||
fun removeScan(id: Long) {
|
||||
db.delete(SCANS, "$SCANS_ID = ?", arrayOf("$id"))
|
||||
}
|
||||
|
||||
fun removeScans(query: String? = null) {
|
||||
db.delete(SCANS, getWhereClause(query, ""), getWhereArguments(query))
|
||||
}
|
||||
|
||||
fun renameScan(id: Long, name: String) {
|
||||
val cv = ContentValues()
|
||||
cv.put(SCANS_NAME, name)
|
||||
db.update(SCANS, cv, "$SCANS_ID = ?", arrayOf("$id"))
|
||||
}
|
||||
|
||||
private class OpenHelper(context: Context) :
|
||||
SQLiteOpenHelper(context, FILE_NAME, null, 4) {
|
||||
override fun onCreate(db: SQLiteDatabase) {
|
||||
createScans(db)
|
||||
}
|
||||
|
||||
override fun onUpgrade(
|
||||
db: SQLiteDatabase,
|
||||
oldVersion: Int,
|
||||
newVersion: Int
|
||||
) {
|
||||
if (oldVersion < 2) {
|
||||
addRawColumn(db)
|
||||
}
|
||||
if (oldVersion < 3) {
|
||||
addMetaDataColumns(db)
|
||||
}
|
||||
if (oldVersion < 4) {
|
||||
addNameColumn(db)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val FILE_NAME = "history.db"
|
||||
const val SCANS = "scans"
|
||||
const val SCANS_ID = "_id"
|
||||
const val SCANS_DATETIME = "_datetime"
|
||||
const val SCANS_NAME = "name"
|
||||
const val SCANS_CONTENT = "content"
|
||||
const val SCANS_RAW = "raw"
|
||||
const val SCANS_FORMAT = "format"
|
||||
const val SCANS_ERROR_CORRECTION_LEVEL = "error_correction_level"
|
||||
const val SCANS_ISSUE_NUMBER = "issue_number"
|
||||
const val SCANS_ORIENTATION = "orientation"
|
||||
const val SCANS_OTHER_META_DATA = "other_meta_data"
|
||||
const val SCANS_PDF417_EXTRA_METADATA = "pdf417_extra_metadata"
|
||||
const val SCANS_POSSIBLE_COUNTRY = "possible_country"
|
||||
const val SCANS_SUGGESTED_PRICE = "suggested_price"
|
||||
const val SCANS_UPC_EAN_EXTENSION = "upc_ean_extension"
|
||||
|
||||
private fun createScans(db: SQLiteDatabase) {
|
||||
db.execSQL("DROP TABLE IF EXISTS $SCANS")
|
||||
db.execSQL(
|
||||
"""CREATE TABLE $SCANS (
|
||||
$SCANS_ID INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
$SCANS_DATETIME DATETIME NOT NULL,
|
||||
$SCANS_NAME TEXT,
|
||||
$SCANS_CONTENT TEXT NOT NULL,
|
||||
$SCANS_RAW BLOB,
|
||||
$SCANS_FORMAT TEXT NOT NULL,
|
||||
$SCANS_ERROR_CORRECTION_LEVEL TEXT,
|
||||
$SCANS_ISSUE_NUMBER INT,
|
||||
$SCANS_ORIENTATION INT,
|
||||
$SCANS_OTHER_META_DATA TEXT,
|
||||
$SCANS_PDF417_EXTRA_METADATA TEXT,
|
||||
$SCANS_POSSIBLE_COUNTRY TEXT,
|
||||
$SCANS_SUGGESTED_PRICE TEXT,
|
||||
$SCANS_UPC_EAN_EXTENSION TEXT
|
||||
)"""
|
||||
)
|
||||
}
|
||||
|
||||
private fun addRawColumn(db: SQLiteDatabase) {
|
||||
db.execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_RAW BLOB")
|
||||
}
|
||||
|
||||
private fun addMetaDataColumns(db: SQLiteDatabase) {
|
||||
db.execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_ERROR_CORRECTION_LEVEL TEXT")
|
||||
db.execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_ISSUE_NUMBER INT")
|
||||
db.execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_ORIENTATION INT")
|
||||
db.execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_OTHER_META_DATA TEXT")
|
||||
db.execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_PDF417_EXTRA_METADATA TEXT")
|
||||
db.execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_POSSIBLE_COUNTRY TEXT")
|
||||
db.execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_SUGGESTED_PRICE TEXT")
|
||||
db.execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_UPC_EAN_EXTENSION TEXT")
|
||||
}
|
||||
|
||||
private fun addNameColumn(db: SQLiteDatabase) {
|
||||
db.execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_NAME TEXT")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun Cursor.getString(name: String) = this.getString(
|
||||
this.getColumnIndex(name)
|
||||
)
|
||||
|
||||
private fun Cursor.getBlob(name: String) = this.getBlob(
|
||||
this.getColumnIndex(name)
|
||||
)
|
||||
|
||||
private fun Cursor.getLong(name: String) = this.getLong(
|
||||
this.getColumnIndex(name)
|
||||
)
|
||||
@@ -0,0 +1,24 @@
|
||||
package de.markusfisch.android.binaryeye.database
|
||||
|
||||
import android.app.Activity
|
||||
import android.os.Environment
|
||||
import de.markusfisch.android.binaryeye.app.writeExternalFile
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
|
||||
fun exportDatabase(activity: Activity, fileName: String): Boolean {
|
||||
val dbFile = File(
|
||||
Environment.getDataDirectory(),
|
||||
"//data//${activity.packageName}//databases//${Database.FILE_NAME}"
|
||||
)
|
||||
if (!dbFile.exists()) {
|
||||
return false
|
||||
}
|
||||
return writeExternalFile(
|
||||
activity,
|
||||
fileName,
|
||||
"application/vnd.sqlite3"
|
||||
) {
|
||||
FileInputStream(dbFile).copyTo(it)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package de.markusfisch.android.binaryeye.database
|
||||
|
||||
import android.content.Context
|
||||
import android.database.Cursor
|
||||
import de.markusfisch.android.binaryeye.app.toHexString
|
||||
import de.markusfisch.android.binaryeye.app.writeExternalFile
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONException
|
||||
import org.json.JSONObject
|
||||
|
||||
fun exportJson(
|
||||
context: Context,
|
||||
name: String,
|
||||
cursor: Cursor
|
||||
) = writeExternalFile(context, name, "application/json") { outputStream ->
|
||||
exportJson(cursor)?.let {
|
||||
outputStream.write(it.toByteArray())
|
||||
}
|
||||
}
|
||||
|
||||
fun exportJson(cursor: Cursor): String? {
|
||||
if (!cursor.moveToFirst()) {
|
||||
return null
|
||||
}
|
||||
val columns = arrayOf(
|
||||
Database.SCANS_DATETIME,
|
||||
Database.SCANS_FORMAT,
|
||||
Database.SCANS_CONTENT,
|
||||
Database.SCANS_ERROR_CORRECTION_LEVEL,
|
||||
Database.SCANS_ISSUE_NUMBER,
|
||||
Database.SCANS_ORIENTATION,
|
||||
Database.SCANS_OTHER_META_DATA,
|
||||
Database.SCANS_PDF417_EXTRA_METADATA,
|
||||
Database.SCANS_POSSIBLE_COUNTRY,
|
||||
Database.SCANS_SUGGESTED_PRICE,
|
||||
Database.SCANS_UPC_EAN_EXTENSION
|
||||
)
|
||||
val indices = columns.map {
|
||||
Pair(cursor.getColumnIndex(it), it)
|
||||
}
|
||||
val contentIndex = cursor.getColumnIndex(Database.SCANS_CONTENT)
|
||||
val rawIndex = cursor.getColumnIndex(Database.SCANS_RAW)
|
||||
val root = JSONArray()
|
||||
do {
|
||||
var deviation: Pair<Int, String>? = null
|
||||
if (cursor.getString(contentIndex)?.isEmpty() == true) {
|
||||
deviation = Pair(
|
||||
contentIndex,
|
||||
cursor.getBlob(rawIndex).toHexString()
|
||||
)
|
||||
}
|
||||
root.put(cursor.toJsonObject(indices, deviation))
|
||||
} while (cursor.moveToNext())
|
||||
return root.toString()
|
||||
}
|
||||
|
||||
private fun Cursor.toJsonObject(
|
||||
indices: List<Pair<Int, String>>,
|
||||
deviation: Pair<Int, String>?
|
||||
): JSONObject {
|
||||
val obj = JSONObject()
|
||||
return try {
|
||||
indices.forEach {
|
||||
val value = if (deviation?.first == it.first) {
|
||||
deviation.second
|
||||
} else {
|
||||
this.getString(it.first)
|
||||
}
|
||||
obj.put(it.second, value ?: "")
|
||||
}
|
||||
obj
|
||||
} catch (e: JSONException) {
|
||||
obj
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
package de.markusfisch.android.binaryeye.database
|
||||
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
import android.text.format.DateFormat
|
||||
import com.google.zxing.Result
|
||||
import com.google.zxing.ResultMetadataType
|
||||
|
||||
data class Scan(
|
||||
val content: String,
|
||||
val raw: ByteArray?,
|
||||
val format: String,
|
||||
val errorCorrectionLevel: String?,
|
||||
val issueNumber: String?,
|
||||
val orientation: String?,
|
||||
val otherMetaData: String?,
|
||||
val pdf417ExtraMetaData: String?,
|
||||
val possibleCountry: String?,
|
||||
val suggestedPrice: String?,
|
||||
val upcEanExtension: String?,
|
||||
val timestamp: String = getDateTime(),
|
||||
var id: Long = 0L
|
||||
) : Parcelable {
|
||||
constructor(result: Result) : this(
|
||||
result.text,
|
||||
getRawBytes(result),
|
||||
result.barcodeFormat.toString(),
|
||||
result.getMetaString(ResultMetadataType.ERROR_CORRECTION_LEVEL),
|
||||
result.getMetaString(ResultMetadataType.ISSUE_NUMBER),
|
||||
result.getMetaString(ResultMetadataType.ORIENTATION),
|
||||
result.getMetaString(ResultMetadataType.OTHER),
|
||||
result.getMetaString(ResultMetadataType.PDF417_EXTRA_METADATA),
|
||||
result.getMetaString(ResultMetadataType.POSSIBLE_COUNTRY),
|
||||
result.getMetaString(ResultMetadataType.SUGGESTED_PRICE),
|
||||
result.getMetaString(ResultMetadataType.UPC_EAN_EXTENSION)
|
||||
)
|
||||
|
||||
// Needs to be overwritten manually, as ByteArray is an array and
|
||||
// this isn't handled well by Kotlin
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) {
|
||||
return true
|
||||
}
|
||||
if (javaClass != other?.javaClass) {
|
||||
return false
|
||||
}
|
||||
|
||||
other as Scan
|
||||
|
||||
return id == other.id &&
|
||||
timestamp == other.timestamp &&
|
||||
content == other.content &&
|
||||
((raw == null && other.raw == null) ||
|
||||
(raw != null && other.raw != null && raw.contentEquals(other.raw))) &&
|
||||
format == other.format &&
|
||||
errorCorrectionLevel == other.errorCorrectionLevel &&
|
||||
issueNumber == other.issueNumber &&
|
||||
orientation == other.orientation &&
|
||||
otherMetaData == other.otherMetaData &&
|
||||
pdf417ExtraMetaData == other.pdf417ExtraMetaData &&
|
||||
possibleCountry == other.possibleCountry &&
|
||||
suggestedPrice == other.suggestedPrice &&
|
||||
upcEanExtension == other.upcEanExtension
|
||||
}
|
||||
|
||||
// Needs to be overwritten manually, as ByteArray is an array and
|
||||
// this isn't handled well by Kotlin
|
||||
override fun hashCode(): Int {
|
||||
var result = id.hashCode()
|
||||
result = 31 * result + timestamp.hashCode()
|
||||
result = 31 * result + content.hashCode()
|
||||
result = 31 * result + (raw?.contentHashCode() ?: 0)
|
||||
result = 31 * result + format.hashCode()
|
||||
result = 31 * result + (errorCorrectionLevel?.hashCode() ?: 0)
|
||||
result = 31 * result + (issueNumber?.hashCode() ?: 0)
|
||||
result = 31 * result + (orientation?.hashCode() ?: 0)
|
||||
result = 31 * result + (otherMetaData?.hashCode() ?: 0)
|
||||
result = 31 * result + (pdf417ExtraMetaData?.hashCode() ?: 0)
|
||||
result = 31 * result + (possibleCountry?.hashCode() ?: 0)
|
||||
result = 31 * result + (suggestedPrice?.hashCode() ?: 0)
|
||||
result = 31 * result + (upcEanExtension?.hashCode() ?: 0)
|
||||
return result
|
||||
}
|
||||
|
||||
private constructor(parcel: Parcel) : this(
|
||||
content = parcel.readString() ?: "",
|
||||
raw = readByteArray(parcel),
|
||||
format = parcel.readString() ?: "",
|
||||
errorCorrectionLevel = parcel.readString(),
|
||||
issueNumber = parcel.readString(),
|
||||
orientation = parcel.readString(),
|
||||
otherMetaData = parcel.readString(),
|
||||
pdf417ExtraMetaData = parcel.readString(),
|
||||
possibleCountry = parcel.readString(),
|
||||
suggestedPrice = parcel.readString(),
|
||||
upcEanExtension = parcel.readString(),
|
||||
timestamp = parcel.readString() ?: "",
|
||||
id = parcel.readLong()
|
||||
)
|
||||
|
||||
override fun writeToParcel(parcel: Parcel, flags: Int) {
|
||||
parcel.writeString(content)
|
||||
writeByteArray(parcel, raw)
|
||||
parcel.writeString(format)
|
||||
parcel.writeString(errorCorrectionLevel)
|
||||
parcel.writeString(issueNumber)
|
||||
parcel.writeString(orientation)
|
||||
parcel.writeString(otherMetaData)
|
||||
parcel.writeString(pdf417ExtraMetaData)
|
||||
parcel.writeString(possibleCountry)
|
||||
parcel.writeString(suggestedPrice)
|
||||
parcel.writeString(upcEanExtension)
|
||||
parcel.writeString(timestamp)
|
||||
parcel.writeLong(id)
|
||||
}
|
||||
|
||||
override fun describeContents() = 0
|
||||
|
||||
companion object {
|
||||
@JvmField
|
||||
val CREATOR = object : Parcelable.Creator<Scan> {
|
||||
override fun createFromParcel(parcel: Parcel) = Scan(parcel)
|
||||
override fun newArray(size: Int) = arrayOfNulls<Scan>(size)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getDateTime(time: Long = System.currentTimeMillis()) = DateFormat.format(
|
||||
"yyyy-MM-dd HH:mm:ss",
|
||||
time
|
||||
).toString()
|
||||
|
||||
private fun getRawBytes(result: Result): ByteArray? {
|
||||
val metadata = result.resultMetadata ?: return null
|
||||
val segments = metadata[ResultMetadataType.BYTE_SEGMENTS] ?: return null
|
||||
var bytes = ByteArray(0)
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
for (seg in segments as Iterable<ByteArray>) {
|
||||
bytes += seg
|
||||
}
|
||||
// byte segments can never be shorter than the text.
|
||||
// Zxing cuts off content prefixes like "WIFI:"
|
||||
return if (bytes.size >= result.text.length) bytes else null
|
||||
}
|
||||
|
||||
private fun Result.getMetaString(
|
||||
key: ResultMetadataType
|
||||
): String? = this.resultMetadata?.let {
|
||||
it[key]?.toString()
|
||||
}
|
||||
|
||||
private fun writeByteArray(parcel: Parcel, array: ByteArray?) {
|
||||
val size = array?.size ?: 0
|
||||
parcel.writeInt(size)
|
||||
if (size > 0) {
|
||||
parcel.writeByteArray(array)
|
||||
}
|
||||
}
|
||||
|
||||
private fun readByteArray(parcel: Parcel): ByteArray? {
|
||||
val size = parcel.readInt()
|
||||
return if (size > 0) {
|
||||
val array = ByteArray(size)
|
||||
parcel.readByteArray(array)
|
||||
array
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
@@ -1,35 +1,51 @@
|
||||
package de.markusfisch.android.binaryeye.fragment
|
||||
|
||||
import com.google.zxing.BarcodeFormat
|
||||
|
||||
import de.markusfisch.android.binaryeye.app.shareFile
|
||||
import de.markusfisch.android.binaryeye.zxing.Zxing
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
|
||||
import android.os.Bundle
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.AlertDialog
|
||||
import android.graphics.Bitmap
|
||||
import android.os.Bundle
|
||||
import android.support.v4.app.Fragment
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.Toast
|
||||
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
|
||||
import android.view.*
|
||||
import android.widget.EditText
|
||||
import com.google.zxing.BarcodeFormat
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.app.*
|
||||
import de.markusfisch.android.binaryeye.view.doOnApplyWindowInsets
|
||||
import de.markusfisch.android.binaryeye.view.setPaddingFromWindowInsets
|
||||
import de.markusfisch.android.binaryeye.widget.ConfinedScalingImageView
|
||||
import de.markusfisch.android.binaryeye.widget.toast
|
||||
import de.markusfisch.android.binaryeye.zxing.Zxing
|
||||
import kotlinx.coroutines.Dispatchers.Main
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
import java.io.OutputStream
|
||||
import java.util.*
|
||||
|
||||
class BarcodeFragment : Fragment() {
|
||||
private enum class FileType {
|
||||
PNG, SVG
|
||||
}
|
||||
|
||||
private var barcodeBitmap: Bitmap? = null
|
||||
private var barcodeSvg: String? = null
|
||||
private var content: String = ""
|
||||
private var format: BarcodeFormat? = null
|
||||
|
||||
override fun onCreate(state: Bundle?) {
|
||||
super.onCreate(state)
|
||||
setHasOptionsMenu(true)
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
state: Bundle?
|
||||
): View? {
|
||||
activity.setTitle(R.string.view_barcode)
|
||||
val ac = activity ?: return null
|
||||
ac.setTitle(R.string.view_barcode)
|
||||
|
||||
val view = inflater.inflate(
|
||||
R.layout.fragment_barcode,
|
||||
@@ -37,68 +53,140 @@ class BarcodeFragment : Fragment() {
|
||||
false
|
||||
)
|
||||
|
||||
val args = arguments
|
||||
args?.let {
|
||||
val content = args.getString(CONTENT)
|
||||
val format = args.getSerializable(FORMAT) as BarcodeFormat?
|
||||
if (content != null && format != null) {
|
||||
val size = args.getInt(SIZE)
|
||||
val bitmap: Bitmap?
|
||||
try {
|
||||
bitmap = Zxing.encodeAsBitmap(
|
||||
content,
|
||||
format,
|
||||
size,
|
||||
size
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
var message = e.message
|
||||
if (message == null || message.isEmpty()) {
|
||||
message = getString(R.string.error_encoding_barcode)
|
||||
}
|
||||
Toast.makeText(
|
||||
activity,
|
||||
message,
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
fragmentManager.popBackStack()
|
||||
return null
|
||||
}
|
||||
view.findViewById<ImageView>(R.id.barcode).setImageBitmap(bitmap)
|
||||
view.findViewById<View>(R.id.share).setOnClickListener {
|
||||
bitmap?.let {
|
||||
share(bitmap)
|
||||
}
|
||||
}
|
||||
val args = arguments ?: return view
|
||||
val content = args.getString(CONTENT) ?: return view
|
||||
val format = args.getSerializable(FORMAT) as BarcodeFormat? ?: return view
|
||||
val size = args.getInt(SIZE)
|
||||
try {
|
||||
barcodeBitmap = Zxing.encodeAsBitmap(content, format, size, size)
|
||||
barcodeSvg = Zxing.encodeAsSvg(content, format, size, size)
|
||||
} catch (e: Exception) {
|
||||
var message = e.message
|
||||
if (message == null || message.isEmpty()) {
|
||||
message = getString(R.string.error_encoding_barcode)
|
||||
}
|
||||
message?.let {
|
||||
ac.toast(message)
|
||||
}
|
||||
fragmentManager.popBackStack()
|
||||
return null
|
||||
}
|
||||
this.content = content
|
||||
this.format = format
|
||||
|
||||
val imageView = view.findViewById<ConfinedScalingImageView>(
|
||||
R.id.barcode
|
||||
)
|
||||
imageView.setImageBitmap(barcodeBitmap)
|
||||
imageView.post {
|
||||
// make sure to invoke this after ScalingImageView.onLayout()
|
||||
imageView.minWidth /= 2f
|
||||
}
|
||||
|
||||
view.findViewById<View>(R.id.share).setOnClickListener {
|
||||
val bitmap = barcodeBitmap
|
||||
bitmap?.let {
|
||||
share(bitmap)
|
||||
}
|
||||
}
|
||||
|
||||
(view.findViewById(R.id.inset_layout) as View).setPaddingFromWindowInsets()
|
||||
imageView.doOnApplyWindowInsets { v, insets ->
|
||||
(v as ConfinedScalingImageView).insets.set(insets)
|
||||
}
|
||||
|
||||
return view
|
||||
}
|
||||
|
||||
private fun share(bitmap: Bitmap) {
|
||||
GlobalScope.launch {
|
||||
val file = saveBitmap(bitmap)
|
||||
GlobalScope.launch(Main) {
|
||||
file?.let {
|
||||
shareFile(context, file, "image/png")
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
inflater.inflate(R.menu.fragment_barcode, menu)
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
return when (item.itemId) {
|
||||
R.id.export_svg -> {
|
||||
askForFileNameAndSave(FileType.SVG)
|
||||
true
|
||||
}
|
||||
R.id.export_png -> {
|
||||
askForFileNameAndSave(FileType.PNG)
|
||||
true
|
||||
}
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
}
|
||||
|
||||
// dialogs do not have a parent view
|
||||
@SuppressLint("InflateParams")
|
||||
private fun askForFileNameAndSave(fileType: FileType) {
|
||||
val ac = activity ?: return
|
||||
if (!hasWritePermission(ac)) {
|
||||
return
|
||||
}
|
||||
val view = ac.layoutInflater.inflate(R.layout.dialog_save_file, null)
|
||||
val editText = view.findViewById<EditText>(R.id.file_name)
|
||||
editText.setText(encodeFileName("${format.toString()}_$content"))
|
||||
AlertDialog.Builder(ac)
|
||||
.setView(view)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
val fileName = editText.text.toString()
|
||||
when (fileType) {
|
||||
FileType.PNG -> saveAs(
|
||||
addSuffixIfNotGiven(fileName, ".png"),
|
||||
"image/png"
|
||||
) {
|
||||
barcodeBitmap?.saveAsPng(it)
|
||||
}
|
||||
FileType.SVG -> saveAs(
|
||||
addSuffixIfNotGiven(fileName, ".svg"),
|
||||
"image/svg+xmg"
|
||||
) { outputStream ->
|
||||
barcodeSvg?.let {
|
||||
outputStream.write(it.toByteArray())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||
}
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun saveAs(
|
||||
fileName: String,
|
||||
mimeType: String,
|
||||
write: (outputStream: OutputStream) -> Unit
|
||||
) {
|
||||
val ac = activity ?: return
|
||||
GlobalScope.launch {
|
||||
val message = writeExternalFile(ac, fileName, mimeType, write).toSaveResult()
|
||||
GlobalScope.launch(Main) {
|
||||
ac.toast(message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun saveBitmap(bitmap: Bitmap): File? {
|
||||
return try {
|
||||
private fun share(bitmap: Bitmap) {
|
||||
GlobalScope.launch {
|
||||
val file = File(
|
||||
context.externalCacheDir,
|
||||
"shared_barcode.png"
|
||||
)
|
||||
val fos = FileOutputStream(file)
|
||||
bitmap.compress(Bitmap.CompressFormat.PNG, 90, fos)
|
||||
fos.close()
|
||||
file
|
||||
} catch (e: IOException) {
|
||||
null
|
||||
val success = try {
|
||||
FileOutputStream(file).use {
|
||||
bitmap.saveAsPng(it)
|
||||
}
|
||||
true
|
||||
} catch (e: IOException) {
|
||||
false
|
||||
}
|
||||
GlobalScope.launch(Main) {
|
||||
if (success) {
|
||||
shareFile(context, file, "image/png")
|
||||
} else {
|
||||
activity?.toast(R.string.error_saving_file)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,3 +210,11 @@ class BarcodeFragment : Fragment() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun Bitmap.saveAsPng(outputStream: OutputStream, quality: Int = 90) {
|
||||
this.compress(Bitmap.CompressFormat.PNG, quality, outputStream)
|
||||
}
|
||||
|
||||
private val fileNameCharacters = "[^A-Za-z0-9]".toRegex()
|
||||
private fun encodeFileName(name: String): String =
|
||||
fileNameCharacters.replace(name, "_").take(16).trim('_').toLowerCase(Locale.getDefault())
|
||||
|
||||
@@ -1,55 +1,45 @@
|
||||
package de.markusfisch.android.binaryeye.fragment
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import com.google.zxing.BarcodeFormat
|
||||
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.actions.IAction
|
||||
import de.markusfisch.android.binaryeye.actions.validateOrGetNew
|
||||
import de.markusfisch.android.binaryeye.app.addFragment
|
||||
import de.markusfisch.android.binaryeye.app.hasNonPrintableCharacters
|
||||
import de.markusfisch.android.binaryeye.app.hasWritePermission
|
||||
import de.markusfisch.android.binaryeye.app.prefs
|
||||
import de.markusfisch.android.binaryeye.app.shareText
|
||||
|
||||
import android.app.AlertDialog
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Environment
|
||||
import android.support.design.widget.FloatingActionButton
|
||||
import android.support.v4.app.Fragment
|
||||
import android.text.ClipboardManager
|
||||
import android.text.Editable
|
||||
import android.text.TextWatcher
|
||||
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.view.*
|
||||
import android.widget.EditText
|
||||
import android.widget.ImageView
|
||||
import android.widget.TableLayout
|
||||
import android.widget.TableRow
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.net.URLEncoder
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.actions.ActionRegistry
|
||||
import de.markusfisch.android.binaryeye.actions.wifi.WifiAction
|
||||
import de.markusfisch.android.binaryeye.app.*
|
||||
import de.markusfisch.android.binaryeye.database.Scan
|
||||
import de.markusfisch.android.binaryeye.view.setPaddingFromWindowInsets
|
||||
import de.markusfisch.android.binaryeye.widget.toast
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class DecodeFragment : Fragment() {
|
||||
private lateinit var contentView: EditText
|
||||
private lateinit var formatView: TextView
|
||||
private lateinit var metaView: TableLayout
|
||||
private lateinit var hexView: TextView
|
||||
private lateinit var format: BarcodeFormat
|
||||
private lateinit var actionMenuItem: MenuItem
|
||||
private lateinit var format: String
|
||||
private lateinit var fab: FloatingActionButton
|
||||
|
||||
private var action: IAction? = null
|
||||
private var isBinary = false
|
||||
private val parentJob = Job()
|
||||
private val scope: CoroutineScope = CoroutineScope(Dispatchers.Main + parentJob)
|
||||
private val content: String
|
||||
get() = contentView.text.toString()
|
||||
|
||||
private var action = ActionRegistry.DEFAULT_ACTION
|
||||
private var isBinary = false
|
||||
private var id = 0L
|
||||
|
||||
override fun onCreate(state: Bundle?) {
|
||||
super.onCreate(state)
|
||||
setHasOptionsMenu(true)
|
||||
@@ -60,7 +50,7 @@ class DecodeFragment : Fragment() {
|
||||
container: ViewGroup?,
|
||||
state: Bundle?
|
||||
): View {
|
||||
activity.setTitle(R.string.content)
|
||||
activity?.setTitle(R.string.content)
|
||||
|
||||
val view = inflater.inflate(
|
||||
R.layout.fragment_decode,
|
||||
@@ -68,13 +58,19 @@ class DecodeFragment : Fragment() {
|
||||
false
|
||||
)
|
||||
|
||||
val inputContent = arguments?.getString(CONTENT) ?: ""
|
||||
isBinary = hasNonPrintableCharacters(inputContent) or inputContent.isEmpty()
|
||||
val raw = arguments?.getByteArray(RAW) ?: inputContent.toByteArray()
|
||||
format = arguments?.getSerializable(FORMAT) as BarcodeFormat? ?: BarcodeFormat.QR_CODE
|
||||
val scan = arguments?.getParcelable(SCAN) as Scan?
|
||||
?: throw IllegalArgumentException("DecodeFragment needs a Scan")
|
||||
id = scan.id
|
||||
|
||||
val inputContent = scan.content
|
||||
isBinary = hasNonPrintableCharacters(
|
||||
inputContent
|
||||
) or inputContent.isEmpty()
|
||||
val raw = scan.raw ?: inputContent.toByteArray()
|
||||
format = scan.format
|
||||
|
||||
contentView = view.findViewById(R.id.content)
|
||||
val shareFab = view.findViewById<ImageView>(R.id.share)
|
||||
fab = view.findViewById(R.id.open)
|
||||
|
||||
if (!isBinary) {
|
||||
contentView.setText(inputContent)
|
||||
@@ -99,53 +95,112 @@ class DecodeFragment : Fragment() {
|
||||
) {
|
||||
}
|
||||
})
|
||||
shareFab.setOnClickListener { v ->
|
||||
shareText(v.context, content)
|
||||
fab.setOnClickListener {
|
||||
executeAction(content.toByteArray())
|
||||
}
|
||||
if (prefs.openImmediately) {
|
||||
executeAction(content.toByteArray())
|
||||
}
|
||||
} else {
|
||||
contentView.setText(R.string.binary_data)
|
||||
contentView.isEnabled = false
|
||||
shareFab.setImageResource(R.drawable.ic_action_save)
|
||||
shareFab.setOnClickListener {
|
||||
fab.setImageResource(R.drawable.ic_action_save)
|
||||
fab.setOnClickListener {
|
||||
askForFileNameAndSave(raw)
|
||||
}
|
||||
}
|
||||
|
||||
formatView = view.findViewById(R.id.format)
|
||||
metaView = view.findViewById(R.id.meta)
|
||||
hexView = view.findViewById(R.id.hex)
|
||||
|
||||
updateViewsAndAction(raw)
|
||||
if (action is WifiAction) {
|
||||
val password = (action as WifiAction).password
|
||||
if (password != null) {
|
||||
activity?.apply {
|
||||
copyToClipboard(password)
|
||||
toast(R.string.copied_password_to_clipboard)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (prefs.showMetaData) {
|
||||
fillMetaView(metaView, scan)
|
||||
}
|
||||
|
||||
(view.findViewById(R.id.inset_layout) as View).setPaddingFromWindowInsets()
|
||||
(view.findViewById(R.id.scroll_view) as View).setPaddingFromWindowInsets()
|
||||
|
||||
return view
|
||||
}
|
||||
|
||||
private fun updateViewsAndAction(bytes: ByteArray) {
|
||||
action = action.validateOrGetNew(bytes)
|
||||
val prevAction = action
|
||||
if (!prevAction.canExecuteOn(bytes)) {
|
||||
action = ActionRegistry.getAction(bytes)
|
||||
}
|
||||
formatView.text = resources.getQuantityString(
|
||||
R.plurals.barcode_info,
|
||||
bytes.size,
|
||||
format.toString(),
|
||||
format,
|
||||
bytes.size
|
||||
)
|
||||
hexView.text = hexDump(bytes, 33)
|
||||
if (::actionMenuItem.isInitialized) {
|
||||
actionMenuItem.setIcon(action?.iconResId ?: R.drawable.ic_action_open)
|
||||
actionMenuItem.setTitle(action?.titleResId ?: R.string.open_url)
|
||||
hexView.text = if (prefs.showHexDump) hexDump(bytes) else ""
|
||||
if (prevAction !== action) {
|
||||
fab.setImageResource(action.iconResId)
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
||||
fab.setOnLongClickListener { v ->
|
||||
v.context.toast(action.titleResId)
|
||||
true
|
||||
}
|
||||
} else {
|
||||
fab.tooltipText = getString(action.titleResId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun fillMetaView(tableLayout: TableLayout, scan: Scan) {
|
||||
val ctx = tableLayout.context
|
||||
val spaceBetween = (ctx.resources.displayMetrics.density * 16f).toInt()
|
||||
var hasMeta = false
|
||||
sortedMapOf(
|
||||
R.string.error_correction_level to scan.errorCorrectionLevel,
|
||||
R.string.issue_number to scan.issueNumber,
|
||||
R.string.orientation to scan.orientation,
|
||||
R.string.other_meta_data to scan.otherMetaData,
|
||||
R.string.pdf417_extra_metadata to scan.pdf417ExtraMetaData,
|
||||
R.string.possible_country to scan.possibleCountry,
|
||||
R.string.suggested_price to scan.suggestedPrice,
|
||||
R.string.upc_ean_extension to scan.upcEanExtension
|
||||
).forEach { item ->
|
||||
item.value?.let {
|
||||
val tr = TableRow(ctx)
|
||||
val keyView = TextView(ctx)
|
||||
keyView.setText(item.key)
|
||||
val valueView = TextView(ctx)
|
||||
valueView.setPadding(spaceBetween, 0, 0, 0)
|
||||
valueView.text = it
|
||||
tr.addView(keyView)
|
||||
tr.addView(valueView)
|
||||
tableLayout.addView(tr)
|
||||
hasMeta = true
|
||||
}
|
||||
}
|
||||
if (hasMeta) {
|
||||
tableLayout.setPadding(0, 0, 0, spaceBetween)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
inflater.inflate(R.menu.fragment_decode, menu)
|
||||
actionMenuItem = menu.findItem(R.id.open_url)
|
||||
action?.also { action ->
|
||||
actionMenuItem.setIcon(action.iconResId)
|
||||
actionMenuItem.setTitle(action.titleResId)
|
||||
}
|
||||
if (isBinary) {
|
||||
menu.findItem(R.id.copy_to_clipboard).isVisible = false
|
||||
menu.findItem(R.id.open_url).isVisible = false
|
||||
menu.findItem(R.id.create).isVisible = false
|
||||
}
|
||||
if (id > 0L) {
|
||||
menu.findItem(R.id.remove).isVisible = true
|
||||
}
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
@@ -154,8 +209,8 @@ class DecodeFragment : Fragment() {
|
||||
copyToClipboard(content)
|
||||
true
|
||||
}
|
||||
R.id.open_url -> {
|
||||
openUrl(content)
|
||||
R.id.share -> {
|
||||
context?.also { shareText(it, content) }
|
||||
true
|
||||
}
|
||||
R.id.create -> {
|
||||
@@ -165,118 +220,75 @@ class DecodeFragment : Fragment() {
|
||||
)
|
||||
true
|
||||
}
|
||||
R.id.remove -> {
|
||||
db.removeScan(id)
|
||||
backOrFinish()
|
||||
true
|
||||
}
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
}
|
||||
|
||||
private fun copyToClipboard(text: String) {
|
||||
activity ?: return
|
||||
|
||||
val cm = activity.getSystemService(
|
||||
Context.CLIPBOARD_SERVICE
|
||||
) as ClipboardManager
|
||||
cm.text = text
|
||||
Toast.makeText(
|
||||
activity,
|
||||
R.string.put_into_clipboard,
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
|
||||
private fun openUrl(
|
||||
url: String,
|
||||
executeCustomAction: Boolean = true,
|
||||
searchIfNoUrl: Boolean = true
|
||||
) {
|
||||
if (activity == null || url.isEmpty()) {
|
||||
return
|
||||
}
|
||||
var uri = Uri.parse(url)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
|
||||
uri = uri.normalizeScheme()
|
||||
}
|
||||
val intent = Intent(Intent.ACTION_VIEW, uri)
|
||||
when {
|
||||
executeCustomAction && action != null -> action?.also { action ->
|
||||
action.execute(activity, url.toByteArray())
|
||||
}
|
||||
intent.resolveActivity(activity.packageManager) != null -> {
|
||||
startActivity(intent)
|
||||
}
|
||||
searchIfNoUrl -> pickSearchEngineAndSearch(activity, url)
|
||||
else -> Toast.makeText(
|
||||
activity,
|
||||
R.string.cannot_resolve_action,
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
activity?.apply {
|
||||
copyToClipboard(text)
|
||||
toast(R.string.copied_to_clipboard)
|
||||
}
|
||||
}
|
||||
|
||||
private fun pickSearchEngineAndSearch(context: Context, query: String) {
|
||||
val names = context.resources.getStringArray(
|
||||
R.array.search_engines_names
|
||||
).toMutableList()
|
||||
val urls = context.resources.getStringArray(
|
||||
R.array.search_engines_values
|
||||
).toMutableList()
|
||||
if (prefs.openWithUrl.isNotEmpty()) {
|
||||
names.add(prefs.openWithUrl)
|
||||
urls.add(prefs.openWithUrl)
|
||||
private fun backOrFinish() {
|
||||
val fm = fragmentManager
|
||||
if (fm != null && fm.backStackEntryCount > 0) {
|
||||
fm.popBackStack()
|
||||
} else {
|
||||
activity?.finish()
|
||||
}
|
||||
}
|
||||
|
||||
private fun executeAction(content: ByteArray) {
|
||||
val ac = activity ?: return
|
||||
if (content.isNotEmpty()) {
|
||||
if (action is WifiAction &&
|
||||
Build.VERSION.SDK_INT < Build.VERSION_CODES.Q &&
|
||||
!hasLocationPermission(ac)
|
||||
) {
|
||||
return
|
||||
}
|
||||
scope.launch {
|
||||
action.execute(ac, content)
|
||||
}
|
||||
}
|
||||
AlertDialog.Builder(context)
|
||||
.setTitle(R.string.pick_search_engine)
|
||||
.setItems(names.toTypedArray()) { _, which ->
|
||||
openUrl(
|
||||
urls[which] + URLEncoder.encode(query, "utf-8"),
|
||||
executeCustomAction = false,
|
||||
searchIfNoUrl = false
|
||||
)
|
||||
}
|
||||
.show()
|
||||
}
|
||||
|
||||
// dialogs don't have a parent layout and must therefore be
|
||||
// inflated with a null root layout
|
||||
@SuppressLint("InflateParams")
|
||||
private fun askForFileNameAndSave(raw: ByteArray) {
|
||||
val ac = activity ?: return
|
||||
val view = ac.layoutInflater.inflate(R.layout.dialog_save_file, null)
|
||||
val editText = view.findViewById<EditText>(R.id.file_name)
|
||||
AlertDialog.Builder(ac)
|
||||
.setView(view)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
if (hasWritePermission(ac)) {
|
||||
val messageId = saveByteArray(
|
||||
editText.text.toString(),
|
||||
raw
|
||||
)
|
||||
if (messageId > 0) {
|
||||
Toast.makeText(
|
||||
ac, messageId,
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
.show()
|
||||
if (!hasWritePermission(ac)) {
|
||||
return
|
||||
}
|
||||
scope.launch(Dispatchers.Main) {
|
||||
val name = ac.askForFileName() ?: return@launch
|
||||
val message = writeExternalFile(
|
||||
ac,
|
||||
name,
|
||||
"application/octet-stream"
|
||||
) {
|
||||
it.write(raw)
|
||||
}.toSaveResult()
|
||||
ac.toast(message)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
parentJob.cancel()
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val CONTENT = "content"
|
||||
private const val FORMAT = "format"
|
||||
private const val RAW = "raw"
|
||||
private const val SCAN = "scan"
|
||||
|
||||
fun newInstance(
|
||||
content: String,
|
||||
format: BarcodeFormat,
|
||||
raw: ByteArray? = null
|
||||
): Fragment {
|
||||
fun newInstance(scan: Scan): Fragment {
|
||||
val args = Bundle()
|
||||
args.putString(CONTENT, content)
|
||||
args.putSerializable(FORMAT, format)
|
||||
if (raw != null) {
|
||||
args.putByteArray(RAW, raw)
|
||||
}
|
||||
args.putParcelable(SCAN, scan)
|
||||
val fragment = DecodeFragment()
|
||||
fragment.arguments = args
|
||||
return fragment
|
||||
@@ -284,7 +296,7 @@ class DecodeFragment : Fragment() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun hexDump(bytes: ByteArray, charsPerLine: Int): String {
|
||||
fun hexDump(bytes: ByteArray, charsPerLine: Int = 33): String {
|
||||
if (charsPerLine < 4 || bytes.isEmpty()) {
|
||||
return ""
|
||||
}
|
||||
@@ -298,9 +310,7 @@ private fun hexDump(bytes: ByteArray, charsPerLine: Int): String {
|
||||
val ord = bytes[i]
|
||||
hex.append(String.format("%02X ", ord))
|
||||
ascii.append(if (ord > 31) ord.toChar() else " ")
|
||||
|
||||
++i
|
||||
|
||||
val posInLine = i % itemsPerLine
|
||||
val atEnd = i >= len
|
||||
var atLineEnd = posInLine == 0
|
||||
@@ -324,21 +334,3 @@ private fun hexDump(bytes: ByteArray, charsPerLine: Int): String {
|
||||
}
|
||||
return dump.toString()
|
||||
}
|
||||
|
||||
fun saveByteArray(name: String, raw: ByteArray): Int {
|
||||
return try {
|
||||
val file = File(
|
||||
Environment.getExternalStoragePublicDirectory(
|
||||
Environment.DIRECTORY_DOWNLOADS
|
||||
),
|
||||
name
|
||||
)
|
||||
if (file.exists()) {
|
||||
return R.string.error_file_exists
|
||||
}
|
||||
file.writeBytes(raw)
|
||||
0
|
||||
} catch (e: IOException) {
|
||||
R.string.error_saving_binary_data
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,28 +1,22 @@
|
||||
package de.markusfisch.android.binaryeye.fragment
|
||||
|
||||
import com.google.zxing.BarcodeFormat
|
||||
|
||||
import de.markusfisch.android.binaryeye.app.addFragment
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.support.v4.app.Fragment
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.EditText
|
||||
import android.widget.SeekBar
|
||||
import android.widget.Spinner
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import android.view.*
|
||||
import android.widget.*
|
||||
import com.google.zxing.BarcodeFormat
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.app.addFragment
|
||||
import de.markusfisch.android.binaryeye.app.hideSoftKeyboard
|
||||
import de.markusfisch.android.binaryeye.app.prefs
|
||||
import de.markusfisch.android.binaryeye.view.setPaddingFromWindowInsets
|
||||
|
||||
class EncodeFragment : Fragment() {
|
||||
private lateinit var formatView: Spinner
|
||||
private lateinit var sizeView: TextView
|
||||
private lateinit var sizeBarView: SeekBar
|
||||
private lateinit var contentView: EditText
|
||||
|
||||
private val writers = arrayListOf(
|
||||
BarcodeFormat.AZTEC,
|
||||
BarcodeFormat.CODABAR,
|
||||
@@ -37,12 +31,18 @@ class EncodeFragment : Fragment() {
|
||||
BarcodeFormat.UPC_A
|
||||
)
|
||||
|
||||
override fun onCreate(state: Bundle?) {
|
||||
super.onCreate(state)
|
||||
setHasOptionsMenu(true)
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
state: Bundle?
|
||||
): View {
|
||||
activity.setTitle(R.string.compose_barcode)
|
||||
): View? {
|
||||
val ac = activity ?: return null
|
||||
ac.setTitle(R.string.compose_barcode)
|
||||
|
||||
val view = inflater.inflate(
|
||||
R.layout.fragment_encode,
|
||||
@@ -51,8 +51,8 @@ class EncodeFragment : Fragment() {
|
||||
)
|
||||
|
||||
formatView = view.findViewById(R.id.format)
|
||||
formatView.adapter = ArrayAdapter<String>(
|
||||
activity,
|
||||
formatView.adapter = ArrayAdapter(
|
||||
ac,
|
||||
android.R.layout.simple_list_item_1,
|
||||
writers.map { it.name }
|
||||
)
|
||||
@@ -61,43 +61,72 @@ class EncodeFragment : Fragment() {
|
||||
sizeBarView = view.findViewById(R.id.size_bar)
|
||||
initSizeBar()
|
||||
|
||||
val contentView = view.findViewById<EditText>(R.id.content)
|
||||
contentView = view.findViewById(R.id.content)
|
||||
|
||||
val args = arguments
|
||||
args?.let {
|
||||
contentView.setText(args.getString(CONTENT))
|
||||
formatView.setSelection(
|
||||
writers.indexOf(
|
||||
args.getSerializable(FORMAT) as BarcodeFormat?
|
||||
)
|
||||
)
|
||||
args?.also {
|
||||
contentView.setText(it.getString(CONTENT))
|
||||
}
|
||||
|
||||
view.findViewById<View>(R.id.encode).setOnClickListener { v ->
|
||||
val format = writers[formatView.selectedItemPosition]
|
||||
val size = getSize(sizeBarView.progress)
|
||||
val content = contentView.text.toString()
|
||||
if (content.isEmpty()) {
|
||||
Toast.makeText(
|
||||
v.context,
|
||||
R.string.error_no_content,
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
} else {
|
||||
hideSoftKeyboard(contentView)
|
||||
addFragment(
|
||||
fragmentManager, BarcodeFragment.newInstance(
|
||||
content,
|
||||
format,
|
||||
size
|
||||
)
|
||||
)
|
||||
val barcodeFormat = args?.getString(FORMAT)
|
||||
if (barcodeFormat != null) {
|
||||
formatView.setSelection(
|
||||
writers.indexOf(resolveFormat(barcodeFormat))
|
||||
)
|
||||
} else if (state == null) {
|
||||
formatView.post {
|
||||
formatView.setSelection(prefs.indexOfLastSelectedFormat)
|
||||
}
|
||||
}
|
||||
|
||||
view.findViewById<View>(R.id.encode).setOnClickListener {
|
||||
encode()
|
||||
}
|
||||
|
||||
(view.findViewById(R.id.inset_layout) as View).setPaddingFromWindowInsets()
|
||||
(view.findViewById(R.id.scroll_view) as View).setPaddingFromWindowInsets()
|
||||
|
||||
return view
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
prefs.indexOfLastSelectedFormat = formatView.selectedItemPosition
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
inflater.inflate(R.menu.fragment_encode, menu)
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
return when (item.itemId) {
|
||||
R.id.next -> {
|
||||
encode()
|
||||
true
|
||||
}
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
}
|
||||
|
||||
private fun encode() {
|
||||
val format = writers[formatView.selectedItemPosition]
|
||||
val size = getSize(sizeBarView.progress)
|
||||
val content = contentView.text.toString()
|
||||
if (content.isEmpty()) {
|
||||
Toast.makeText(
|
||||
context,
|
||||
R.string.error_no_content,
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
} else {
|
||||
activity?.hideSoftKeyboard(contentView)
|
||||
addFragment(
|
||||
fragmentManager,
|
||||
BarcodeFragment.newInstance(content, format, size)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun initSizeBar() {
|
||||
updateSize(sizeBarView.progress)
|
||||
sizeBarView.setOnSeekBarChangeListener(
|
||||
@@ -123,29 +152,26 @@ class EncodeFragment : Fragment() {
|
||||
|
||||
private fun getSize(power: Int) = 128 * (power + 1)
|
||||
|
||||
private fun hideSoftKeyboard(view: View) {
|
||||
val im = activity?.getSystemService(
|
||||
Context.INPUT_METHOD_SERVICE
|
||||
) as InputMethodManager?
|
||||
im?.let {
|
||||
im.hideSoftInputFromWindow(view.windowToken, 0)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val CONTENT = "content"
|
||||
private const val FORMAT = "format"
|
||||
|
||||
fun newInstance(
|
||||
content: String,
|
||||
format: BarcodeFormat = BarcodeFormat.AZTEC
|
||||
format: String? = null
|
||||
): Fragment {
|
||||
val args = Bundle()
|
||||
args.putString(CONTENT, content)
|
||||
args.putSerializable(FORMAT, format)
|
||||
format?.let { args.putString(FORMAT, it) }
|
||||
val fragment = EncodeFragment()
|
||||
fragment.arguments = args
|
||||
return fragment
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun resolveFormat(name: String): BarcodeFormat = try {
|
||||
BarcodeFormat.valueOf(name)
|
||||
} catch (_: IllegalArgumentException) {
|
||||
BarcodeFormat.QR_CODE
|
||||
}
|
||||
|
||||
@@ -1,37 +1,118 @@
|
||||
package de.markusfisch.android.binaryeye.fragment
|
||||
|
||||
import com.google.zxing.BarcodeFormat
|
||||
|
||||
import de.markusfisch.android.binaryeye.adapter.ScansAdapter
|
||||
import de.markusfisch.android.binaryeye.app.addFragment
|
||||
import de.markusfisch.android.binaryeye.app.db
|
||||
import de.markusfisch.android.binaryeye.app.prefs
|
||||
import de.markusfisch.android.binaryeye.app.shareText
|
||||
import de.markusfisch.android.binaryeye.data.Database
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.AlertDialog
|
||||
import android.app.SearchManager
|
||||
import android.content.Context
|
||||
import android.database.Cursor
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Parcelable
|
||||
import android.support.v4.app.ActivityCompat
|
||||
import android.support.v4.app.Fragment
|
||||
import android.support.v4.content.ContextCompat
|
||||
import android.support.v7.app.AppCompatActivity
|
||||
import android.support.v7.view.ActionMode
|
||||
import android.support.v7.widget.SearchView
|
||||
import android.support.v7.widget.SwitchCompat
|
||||
import android.view.LayoutInflater
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.*
|
||||
import android.widget.EditText
|
||||
import android.widget.ListView
|
||||
|
||||
import kotlinx.coroutines.Dispatchers.Main
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.adapter.ScansAdapter
|
||||
import de.markusfisch.android.binaryeye.app.*
|
||||
import de.markusfisch.android.binaryeye.database.Database
|
||||
import de.markusfisch.android.binaryeye.database.exportCsv
|
||||
import de.markusfisch.android.binaryeye.database.exportDatabase
|
||||
import de.markusfisch.android.binaryeye.database.exportJson
|
||||
import de.markusfisch.android.binaryeye.view.setPaddingFromWindowInsets
|
||||
import de.markusfisch.android.binaryeye.view.useVisibility
|
||||
import de.markusfisch.android.binaryeye.widget.toast
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
class HistoryFragment : Fragment() {
|
||||
private lateinit var useHistorySwitch: SwitchCompat
|
||||
private lateinit var listView: ListView
|
||||
private lateinit var fab: View
|
||||
private lateinit var progressView: View
|
||||
|
||||
private val parentJob = Job()
|
||||
private val scope = CoroutineScope(Dispatchers.IO + parentJob)
|
||||
private val actionModeCallback = object : ActionMode.Callback {
|
||||
override fun onCreateActionMode(
|
||||
mode: ActionMode,
|
||||
menu: Menu
|
||||
): Boolean {
|
||||
mode.menuInflater.inflate(
|
||||
R.menu.fragment_history_edit,
|
||||
menu
|
||||
)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
lockStatusBarColor()
|
||||
val ac = activity ?: return false
|
||||
ac.window.statusBarColor = ContextCompat.getColor(
|
||||
ac,
|
||||
R.color.accent_dark
|
||||
)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onPrepareActionMode(
|
||||
mode: ActionMode,
|
||||
menu: Menu
|
||||
): Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onActionItemClicked(
|
||||
mode: ActionMode,
|
||||
item: MenuItem
|
||||
): Boolean {
|
||||
val ac = activity ?: return false
|
||||
return when (item.itemId) {
|
||||
R.id.copy_scan -> {
|
||||
scansAdapter?.getSelectedContent()?.let {
|
||||
ac.copyToClipboard(it)
|
||||
ac.toast(R.string.copied_to_clipboard)
|
||||
}
|
||||
closeActionMode()
|
||||
true
|
||||
}
|
||||
R.id.edit_scan -> {
|
||||
scansAdapter?.let {
|
||||
askForName(
|
||||
ac,
|
||||
it.selectedScanId,
|
||||
getScanName(it.selectedScanPosition)
|
||||
)
|
||||
}
|
||||
closeActionMode()
|
||||
true
|
||||
}
|
||||
R.id.remove_scan -> {
|
||||
scansAdapter?.let {
|
||||
askToRemoveScan(ac, it.selectedScanId)
|
||||
}
|
||||
closeActionMode()
|
||||
true
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroyActionMode(mode: ActionMode) {
|
||||
closeActionMode()
|
||||
}
|
||||
}
|
||||
|
||||
private var scansAdapter: ScansAdapter? = null
|
||||
private var listViewState: Parcelable? = null
|
||||
private var actionMode: ActionMode? = null
|
||||
private var filter: String? = null
|
||||
private var clearListMenuItem: MenuItem? = null
|
||||
private var exportHistoryMenuItem: MenuItem? = null
|
||||
|
||||
override fun onCreate(state: Bundle?) {
|
||||
super.onCreate(state)
|
||||
setHasOptionsMenu(true)
|
||||
@@ -41,8 +122,9 @@ class HistoryFragment : Fragment() {
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
state: Bundle?
|
||||
): View {
|
||||
activity.setTitle(R.string.history)
|
||||
): View? {
|
||||
val ac = activity ?: return null
|
||||
ac.setTitle(R.string.history)
|
||||
|
||||
val view = inflater.inflate(
|
||||
R.layout.fragment_history,
|
||||
@@ -50,20 +132,26 @@ class HistoryFragment : Fragment() {
|
||||
false
|
||||
)
|
||||
|
||||
val useHistorySwitch = view.findViewById(
|
||||
useHistorySwitch = view.findViewById(
|
||||
R.id.use_history
|
||||
) as SwitchCompat
|
||||
initHistorySwitch(useHistorySwitch)
|
||||
|
||||
listView = view.findViewById(R.id.scans)
|
||||
listView.emptyView = useHistorySwitch
|
||||
listView.setOnItemClickListener { _, _, _, id ->
|
||||
showScan(id)
|
||||
}
|
||||
listView.setOnItemLongClickListener { _, v, _, id ->
|
||||
askToRemoveScan(v.context, id)
|
||||
listView.setOnItemLongClickListener { _, v, position, id ->
|
||||
v.isSelected = true
|
||||
scansAdapter?.select(id, position)
|
||||
if (actionMode == null && ac is AppCompatActivity) {
|
||||
actionMode = ac.delegate.startSupportActionMode(
|
||||
actionModeCallback
|
||||
)
|
||||
}
|
||||
true
|
||||
}
|
||||
listView.setOnScrollListener(systemBarScrollListener)
|
||||
|
||||
fab = view.findViewById(R.id.share)
|
||||
fab.setOnClickListener { v ->
|
||||
@@ -72,27 +160,68 @@ class HistoryFragment : Fragment() {
|
||||
|
||||
progressView = view.findViewById(R.id.progress_view)
|
||||
|
||||
(view.findViewById(R.id.inset_layout) as View).setPaddingFromWindowInsets()
|
||||
listView.setPaddingFromWindowInsets()
|
||||
|
||||
update()
|
||||
|
||||
return view
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
(listView.adapter as ScansAdapter?)?.changeCursor(null)
|
||||
scansAdapter?.changeCursor(null)
|
||||
parentJob.cancel()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
update(context)
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
listViewState = listView.onSaveInstanceState()
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
inflater.inflate(R.menu.fragment_history, menu)
|
||||
initSearchView(menu.findItem(R.id.search))
|
||||
menu.setGroupVisible(R.id.scans_available, scansAdapter?.count != 0)
|
||||
clearListMenuItem = menu.findItem(R.id.clear)
|
||||
exportHistoryMenuItem = menu.findItem(R.id.export_history)
|
||||
}
|
||||
|
||||
private fun initSearchView(item: MenuItem?) {
|
||||
item ?: return
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) {
|
||||
item.isVisible = false
|
||||
} else {
|
||||
val ac = activity ?: return
|
||||
val searchView = item.actionView as SearchView
|
||||
val searchManager = ac.getSystemService(
|
||||
Context.SEARCH_SERVICE
|
||||
) as SearchManager
|
||||
searchView.setSearchableInfo(
|
||||
searchManager.getSearchableInfo(ac.componentName)
|
||||
)
|
||||
searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
|
||||
override fun onQueryTextSubmit(query: String): Boolean {
|
||||
update(query)
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onQueryTextChange(query: String): Boolean {
|
||||
update(query)
|
||||
return false
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
return when (item.itemId) {
|
||||
R.id.clear -> {
|
||||
askToRemoveAllScans(context)
|
||||
askToRemoveScans(context)
|
||||
true
|
||||
}
|
||||
R.id.export_history -> {
|
||||
askToExportToFile(context)
|
||||
true
|
||||
}
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
@@ -108,48 +237,90 @@ class HistoryFragment : Fragment() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun update(context: Context) {
|
||||
GlobalScope.launch {
|
||||
val cursor = db.getScans()
|
||||
GlobalScope.launch(Main) {
|
||||
fab.visibility = if (cursor != null && cursor.count > 0) {
|
||||
private fun updateAndClearFilter() {
|
||||
filter = null
|
||||
update()
|
||||
}
|
||||
|
||||
private fun update(query: String? = null) {
|
||||
query?.let { filter = it }
|
||||
scope.launch {
|
||||
val cursor = db.getScans(filter)
|
||||
withContext(Dispatchers.Main) {
|
||||
val ac = activity ?: return@withContext
|
||||
val hasScans = cursor != null && cursor.count > 0
|
||||
if (filter == null) {
|
||||
if (!hasScans) {
|
||||
listView.emptyView = useHistorySwitch
|
||||
}
|
||||
ActivityCompat.invalidateOptionsMenu(ac)
|
||||
}
|
||||
enableMenuItems(hasScans)
|
||||
fab.visibility = if (hasScans) {
|
||||
View.VISIBLE
|
||||
} else {
|
||||
View.GONE
|
||||
}
|
||||
cursor?.let {
|
||||
if (listView.adapter == null) {
|
||||
listView.adapter = ScansAdapter(context, cursor)
|
||||
} else {
|
||||
val adapter = listView.adapter as ScansAdapter
|
||||
adapter.changeCursor(cursor)
|
||||
adapter.notifyDataSetChanged()
|
||||
cursor?.let { cursor ->
|
||||
// close previous cursor
|
||||
scansAdapter?.also { it.changeCursor(null) }
|
||||
scansAdapter = ScansAdapter(ac, cursor)
|
||||
listView.adapter = scansAdapter
|
||||
listViewState?.also {
|
||||
listView.onRestoreInstanceState(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun showScan(id: Long) {
|
||||
val cursor = db.getScan(id)
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
val contentIndex = cursor.getColumnIndex(Database.SCANS_CONTENT)
|
||||
val rawIndex = cursor.getColumnIndex(Database.SCANS_RAW)
|
||||
val formatIndex = cursor.getColumnIndex(Database.SCANS_FORMAT)
|
||||
try {
|
||||
addFragment(
|
||||
fragmentManager,
|
||||
DecodeFragment.newInstance(
|
||||
cursor.getString(contentIndex),
|
||||
BarcodeFormat.valueOf(cursor.getString(formatIndex)),
|
||||
cursor.getBlob(rawIndex)
|
||||
)
|
||||
)
|
||||
} catch (e: IllegalArgumentException) {
|
||||
// shouldn't ever happen
|
||||
}
|
||||
private fun enableMenuItems(enabled: Boolean) {
|
||||
clearListMenuItem?.isEnabled = enabled
|
||||
exportHistoryMenuItem?.isEnabled = enabled
|
||||
}
|
||||
|
||||
private fun closeActionMode() {
|
||||
unlockStatusBarColor()
|
||||
scansAdapter?.clearSelection()
|
||||
actionMode?.finish()
|
||||
actionMode = null
|
||||
scansAdapter?.notifyDataSetChanged()
|
||||
}
|
||||
|
||||
private fun showScan(id: Long) = db.getScan(id)?.also { scan ->
|
||||
closeActionMode()
|
||||
try {
|
||||
addFragment(
|
||||
fragmentManager,
|
||||
DecodeFragment.newInstance(scan)
|
||||
)
|
||||
} catch (e: IllegalArgumentException) {
|
||||
// ignore, can never happen
|
||||
}
|
||||
cursor?.close()
|
||||
}
|
||||
|
||||
private fun getScanName(position: Int): String? {
|
||||
val cursor = scansAdapter?.getItem(position) as Cursor?
|
||||
return cursor?.getString(cursor.getColumnIndex(Database.SCANS_NAME))
|
||||
}
|
||||
|
||||
// dialogs don't have a parent layout
|
||||
@SuppressLint("InflateParams")
|
||||
private fun askForName(context: Context, id: Long, text: String?) {
|
||||
val view = LayoutInflater.from(context).inflate(
|
||||
R.layout.dialog_enter_name, null
|
||||
)
|
||||
val nameView = view.findViewById<EditText>(R.id.name)
|
||||
nameView.setText(text)
|
||||
AlertDialog.Builder(context)
|
||||
.setView(view)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
val name = nameView.text.toString()
|
||||
db.renameScan(id, name)
|
||||
update()
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun askToRemoveScan(context: Context, id: Long) {
|
||||
@@ -157,25 +328,74 @@ class HistoryFragment : Fragment() {
|
||||
.setMessage(R.string.really_remove_scan)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
db.removeScan(id)
|
||||
update(context)
|
||||
if (scansAdapter?.count == 1) {
|
||||
updateAndClearFilter()
|
||||
} else {
|
||||
update()
|
||||
}
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||
}
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun askToRemoveAllScans(context: Context) {
|
||||
private fun askToRemoveScans(context: Context) {
|
||||
AlertDialog.Builder(context)
|
||||
.setMessage(R.string.really_remove_all_scans)
|
||||
.setMessage(
|
||||
if (filter == null) {
|
||||
R.string.really_remove_all_scans
|
||||
} else {
|
||||
R.string.really_remove_selected_scans
|
||||
}
|
||||
)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
db.removeScans()
|
||||
update(context)
|
||||
db.removeScans(filter)
|
||||
updateAndClearFilter()
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||
}
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun askToExportToFile(context: Context) = scope.launch {
|
||||
val ac = activity ?: return@launch
|
||||
progressView.useVisibility {
|
||||
if (!hasWritePermission(ac)) {
|
||||
return@useVisibility
|
||||
}
|
||||
val options = context.resources.getStringArray(
|
||||
R.array.export_options_values
|
||||
)
|
||||
val delimiter = alertDialog<String>(context) { resume ->
|
||||
setTitle(R.string.export_as)
|
||||
setItems(R.array.export_options_names) { _, which ->
|
||||
resume(options[which])
|
||||
}
|
||||
} ?: return@useVisibility
|
||||
val name = withContext(Dispatchers.Main) {
|
||||
ac.askForFileName(
|
||||
when (delimiter) {
|
||||
"db" -> ".db"
|
||||
"json" -> ".json"
|
||||
else -> ".csv"
|
||||
}
|
||||
)
|
||||
} ?: return@useVisibility
|
||||
val message = when (delimiter) {
|
||||
"db" -> exportDatabase(ac, name)
|
||||
else -> db.getScansDetailed(filter)?.use {
|
||||
when (delimiter) {
|
||||
"json" -> exportJson(context, name, it)
|
||||
else -> exportCsv(context, name, it, delimiter)
|
||||
}
|
||||
} ?: false
|
||||
}.toSaveResult()
|
||||
withContext(Dispatchers.Main) {
|
||||
ac.toast(message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun pickListSeparatorAndShare(context: Context) {
|
||||
val separators = context.resources.getStringArray(
|
||||
R.array.list_separators_values
|
||||
@@ -188,33 +408,37 @@ class HistoryFragment : Fragment() {
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun shareScans(separator: String) {
|
||||
if (progressView.visibility == View.VISIBLE) {
|
||||
return
|
||||
}
|
||||
progressView.visibility = View.VISIBLE
|
||||
GlobalScope.launch {
|
||||
val cursor = db.getScans()
|
||||
cursor?.let {
|
||||
val sb = StringBuilder()
|
||||
if (cursor.moveToFirst()) {
|
||||
val contentIndex = cursor.getColumnIndex(
|
||||
Database.SCANS_CONTENT
|
||||
)
|
||||
do {
|
||||
sb.append(cursor.getString(contentIndex))
|
||||
sb.append(separator)
|
||||
} while (cursor.moveToNext())
|
||||
private fun shareScans(format: String) = scope.launch {
|
||||
progressView.useVisibility {
|
||||
var text: String? = null
|
||||
db.getScansDetailed(filter)?.use { cursor ->
|
||||
val details = format.split(":")
|
||||
text = when (details[0]) {
|
||||
"text" -> exportText(cursor, details[1])
|
||||
"csv" -> exportCsv(cursor, details[1])
|
||||
else -> exportJson(cursor)
|
||||
}
|
||||
cursor.close()
|
||||
val text = sb.toString()
|
||||
if (text.isNotEmpty()) {
|
||||
GlobalScope.launch(Main) {
|
||||
progressView.visibility = View.GONE
|
||||
shareText(context, text)
|
||||
}
|
||||
}
|
||||
text?.let {
|
||||
withContext(Dispatchers.Main) {
|
||||
shareText(context, it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun exportText(cursor: Cursor, separator: String): String {
|
||||
val sb = StringBuilder()
|
||||
val contentIndex = cursor.getColumnIndex(Database.SCANS_CONTENT)
|
||||
if (cursor.moveToFirst()) {
|
||||
do {
|
||||
val content = cursor.getString(contentIndex)
|
||||
if (content?.isNotEmpty() == true) {
|
||||
sb.append(content)
|
||||
sb.append(separator)
|
||||
}
|
||||
} while (cursor.moveToNext())
|
||||
}
|
||||
return sb.toString()
|
||||
}
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
package de.markusfisch.android.binaryeye.fragment
|
||||
|
||||
import de.markusfisch.android.binaryeye.app.prefs
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
|
||||
import android.os.Bundle
|
||||
import android.support.v4.app.Fragment
|
||||
import android.support.v7.widget.SwitchCompat
|
||||
@@ -10,9 +7,21 @@ import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.EditText
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.app.prefs
|
||||
import de.markusfisch.android.binaryeye.view.setPaddingFromWindowInsets
|
||||
|
||||
class PreferencesFragment : Fragment() {
|
||||
private lateinit var showCropHandleSwitch: SwitchCompat
|
||||
private lateinit var zoomBySwipingSwitch: SwitchCompat
|
||||
private lateinit var autoResizeSwitch: SwitchCompat
|
||||
private lateinit var tryHarderSwitch: SwitchCompat
|
||||
private lateinit var vibrateSwitch: SwitchCompat
|
||||
private lateinit var useHistorySwitch: SwitchCompat
|
||||
private lateinit var ignoreConsecutiveDuplicatesSwitch: SwitchCompat
|
||||
private lateinit var openImmediatelySwitch: SwitchCompat
|
||||
private lateinit var showMetaDataSwitch: SwitchCompat
|
||||
private lateinit var showHexDumpSwitch: SwitchCompat
|
||||
private lateinit var openWithUrlInput: EditText
|
||||
|
||||
override fun onCreateView(
|
||||
@@ -20,7 +29,7 @@ class PreferencesFragment : Fragment() {
|
||||
container: ViewGroup?,
|
||||
state: Bundle?
|
||||
): View? {
|
||||
activity.setTitle(R.string.preferences)
|
||||
activity?.setTitle(R.string.preferences)
|
||||
|
||||
val view = inflater.inflate(
|
||||
R.layout.fragment_preferences,
|
||||
@@ -28,20 +37,78 @@ class PreferencesFragment : Fragment() {
|
||||
false
|
||||
)
|
||||
|
||||
useHistorySwitch = view.findViewById<SwitchCompat>(R.id.use_history)
|
||||
showCropHandleSwitch = view.findViewById(R.id.show_crop_handle)
|
||||
if (prefs.showCropHandle) {
|
||||
showCropHandleSwitch.toggle()
|
||||
}
|
||||
|
||||
zoomBySwipingSwitch = view.findViewById(R.id.zoom_by_swiping)
|
||||
if (prefs.zoomBySwiping) {
|
||||
zoomBySwipingSwitch.toggle()
|
||||
}
|
||||
|
||||
autoResizeSwitch = view.findViewById(R.id.auto_rotate)
|
||||
if (prefs.autoRotate) {
|
||||
autoResizeSwitch.toggle()
|
||||
}
|
||||
|
||||
tryHarderSwitch = view.findViewById(R.id.try_harder)
|
||||
if (prefs.tryHarder) {
|
||||
tryHarderSwitch.toggle()
|
||||
}
|
||||
|
||||
vibrateSwitch = view.findViewById(R.id.vibrate)
|
||||
if (prefs.vibrate) {
|
||||
vibrateSwitch.toggle()
|
||||
}
|
||||
|
||||
useHistorySwitch = view.findViewById(R.id.use_history)
|
||||
if (prefs.useHistory) {
|
||||
useHistorySwitch.toggle()
|
||||
}
|
||||
|
||||
openWithUrlInput = view.findViewById<EditText>(R.id.open_with_url)
|
||||
ignoreConsecutiveDuplicatesSwitch = view.findViewById(
|
||||
R.id.ignore_consecutive_duplicates
|
||||
)
|
||||
if (prefs.ignoreConsecutiveDuplicates) {
|
||||
ignoreConsecutiveDuplicatesSwitch.toggle()
|
||||
}
|
||||
|
||||
openImmediatelySwitch = view.findViewById(R.id.open_immediately)
|
||||
if (prefs.openImmediately) {
|
||||
openImmediatelySwitch.toggle()
|
||||
}
|
||||
|
||||
showMetaDataSwitch = view.findViewById(R.id.show_meta_data)
|
||||
if (prefs.showMetaData) {
|
||||
showMetaDataSwitch.toggle()
|
||||
}
|
||||
|
||||
showHexDumpSwitch = view.findViewById(R.id.show_hex_dump)
|
||||
if (prefs.showHexDump) {
|
||||
showHexDumpSwitch.toggle()
|
||||
}
|
||||
|
||||
openWithUrlInput = view.findViewById(R.id.open_with_url)
|
||||
openWithUrlInput.setText(prefs.openWithUrl)
|
||||
|
||||
(view.findViewById(R.id.scroll_view) as View).setPaddingFromWindowInsets()
|
||||
|
||||
return view
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
prefs.useHistory = useHistorySwitch.isChecked()
|
||||
override fun onStop() {
|
||||
super.onStop()
|
||||
prefs.showCropHandle = showCropHandleSwitch.isChecked
|
||||
prefs.zoomBySwiping = zoomBySwipingSwitch.isChecked
|
||||
prefs.autoRotate = autoResizeSwitch.isChecked
|
||||
prefs.tryHarder = tryHarderSwitch.isChecked
|
||||
prefs.vibrate = vibrateSwitch.isChecked
|
||||
prefs.useHistory = useHistorySwitch.isChecked
|
||||
prefs.ignoreConsecutiveDuplicates = ignoreConsecutiveDuplicatesSwitch.isChecked
|
||||
prefs.openImmediately = openImmediatelySwitch.isChecked
|
||||
prefs.showMetaData = showMetaDataSwitch.isChecked
|
||||
prefs.showHexDump = showHexDumpSwitch.isChecked
|
||||
prefs.openWithUrl = openWithUrlInput.text.toString()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
package de.markusfisch.android.binaryeye.graphics
|
||||
|
||||
import android.content.ContentResolver
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import android.graphics.Matrix
|
||||
import android.graphics.RectF
|
||||
import android.net.Uri
|
||||
import java.io.IOException
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
fun loadImageUri(cr: ContentResolver, uri: Uri): Bitmap? = try {
|
||||
val options = BitmapFactory.Options()
|
||||
cr.openInputStream(uri)?.use {
|
||||
options.inJustDecodeBounds = true
|
||||
BitmapFactory.decodeStream(it, null, options)
|
||||
options.inSampleSize = calculateInSampleSize(
|
||||
options.outWidth,
|
||||
options.outHeight
|
||||
)
|
||||
options.inJustDecodeBounds = false
|
||||
}
|
||||
cr.openInputStream(uri)?.use {
|
||||
BitmapFactory.decodeStream(it, null, options)
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
null
|
||||
}
|
||||
|
||||
private fun calculateInSampleSize(
|
||||
width: Int,
|
||||
height: Int,
|
||||
reqWidth: Int = 1024,
|
||||
reqHeight: Int = 1024
|
||||
): Int {
|
||||
var inSampleSize = 1
|
||||
if (height > reqHeight || width > reqWidth) {
|
||||
val halfHeight = height / 2
|
||||
val halfWidth = width / 2
|
||||
while (
|
||||
halfHeight / inSampleSize >= reqHeight ||
|
||||
halfWidth / inSampleSize >= reqWidth
|
||||
) {
|
||||
inSampleSize *= 2
|
||||
}
|
||||
}
|
||||
return inSampleSize
|
||||
}
|
||||
|
||||
fun crop(bitmap: Bitmap, rect: RectF, rotation: Float) = try {
|
||||
val erected = erect(bitmap, rotation)
|
||||
val w = erected.width
|
||||
val h = erected.height
|
||||
Bitmap.createBitmap(
|
||||
erected,
|
||||
(rect.left * w).roundToInt(),
|
||||
(rect.top * h).roundToInt(),
|
||||
(rect.width() * w).roundToInt(),
|
||||
(rect.height() * h).roundToInt()
|
||||
)
|
||||
} catch (e: OutOfMemoryError) {
|
||||
null
|
||||
} catch (e: IllegalArgumentException) {
|
||||
null
|
||||
}
|
||||
|
||||
private fun erect(bitmap: Bitmap, rotation: Float): Bitmap = if (
|
||||
rotation % 360f != 0f
|
||||
) {
|
||||
val matrix = Matrix()
|
||||
matrix.setRotate(rotation)
|
||||
Bitmap.createBitmap(
|
||||
bitmap,
|
||||
0,
|
||||
0,
|
||||
bitmap.width,
|
||||
bitmap.height,
|
||||
matrix,
|
||||
true
|
||||
)
|
||||
} else {
|
||||
bitmap
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package de.markusfisch.android.binaryeye.graphics
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.DashPathEffect
|
||||
import android.graphics.Paint
|
||||
import android.support.v4.content.ContextCompat
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
|
||||
fun Context.getDashedBorderPaint() = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
val dp = resources.displayMetrics.density
|
||||
color = ContextCompat.getColor(applicationContext, R.color.crop_bound)
|
||||
style = Paint.Style.STROKE
|
||||
strokeWidth = dp * 2f
|
||||
pathEffect = DashPathEffect(floatArrayOf(10f * dp, 10f * dp), 0f)
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package de.markusfisch.android.binaryeye.graphics
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Paint
|
||||
import android.graphics.Point
|
||||
import android.support.v4.content.ContextCompat
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
|
||||
class Dots(context: Context) {
|
||||
private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
|
||||
private val radius = 8f * context.resources.displayMetrics.density
|
||||
|
||||
init {
|
||||
paint.color = ContextCompat.getColor(context, R.color.dot)
|
||||
paint.style = Paint.Style.FILL
|
||||
}
|
||||
|
||||
fun draw(canvas: Canvas, points: List<Point>) {
|
||||
for (point in points) {
|
||||
canvas.drawCircle(
|
||||
point.x.toFloat(),
|
||||
point.y.toFloat(),
|
||||
radius,
|
||||
paint
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package de.markusfisch.android.binaryeye.graphics
|
||||
|
||||
import android.content.res.Resources
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.drawable.BitmapDrawable
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.os.Build
|
||||
|
||||
fun Resources.getBitmapFromDrawable(
|
||||
resId: Int
|
||||
): Bitmap = getBitmapFromDrawable(getDrawableCompat(resId))
|
||||
|
||||
fun Resources.getDrawableCompat(
|
||||
resId: Int
|
||||
): Drawable = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
getDrawable(resId, null)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
getDrawable(resId)
|
||||
}
|
||||
|
||||
private fun getBitmapFromDrawable(drawable: Drawable): Bitmap {
|
||||
if (drawable is BitmapDrawable) {
|
||||
return drawable.bitmap
|
||||
}
|
||||
val bitmap = Bitmap.createBitmap(
|
||||
drawable.intrinsicWidth,
|
||||
drawable.intrinsicHeight,
|
||||
Bitmap.Config.ARGB_8888
|
||||
)
|
||||
val canvas = Canvas(bitmap)
|
||||
drawable.setBounds(0, 0, canvas.width, canvas.height)
|
||||
drawable.draw(canvas)
|
||||
return bitmap
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package de.markusfisch.android.binaryeye.graphics
|
||||
|
||||
import android.graphics.Point
|
||||
import android.graphics.Rect
|
||||
import com.google.zxing.Result
|
||||
import com.google.zxing.ResultPoint
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
fun mapResult(
|
||||
frameWidth: Int,
|
||||
frameHeight: Int,
|
||||
frameOrientation: Int,
|
||||
viewRect: Rect,
|
||||
result: Result
|
||||
): List<Point> = frameToView(
|
||||
frameWidth,
|
||||
frameHeight,
|
||||
frameOrientation,
|
||||
viewRect
|
||||
).map(
|
||||
result.resultPoints
|
||||
)
|
||||
|
||||
fun frameToView(
|
||||
frameWidth: Int,
|
||||
frameHeight: Int,
|
||||
frameOrientation: Int,
|
||||
viewRect: Rect
|
||||
) = Mapping(
|
||||
frameWidth,
|
||||
frameHeight,
|
||||
frameOrientation,
|
||||
viewRect.width().toFloat(),
|
||||
viewRect.height().toFloat(),
|
||||
viewRect.left,
|
||||
viewRect.top
|
||||
)
|
||||
|
||||
fun isPortrait(orientation: Int) = orientation == 90 || orientation == 270
|
||||
|
||||
data class Mapping(
|
||||
val frameWidth: Int,
|
||||
val frameHeight: Int,
|
||||
val frameOrientation: Int,
|
||||
val viewWidth: Float,
|
||||
val viewHeight: Float,
|
||||
val offsetX: Int,
|
||||
val offsetY: Int
|
||||
) {
|
||||
fun map(resultPoint: ResultPoint): Point {
|
||||
val point = Point(
|
||||
resultPoint.x.roundToInt(),
|
||||
resultPoint.y.roundToInt()
|
||||
)
|
||||
rotate(point)
|
||||
val w: Int
|
||||
val h: Int
|
||||
if (isPortrait(frameOrientation)) {
|
||||
w = frameHeight
|
||||
h = frameWidth
|
||||
} else {
|
||||
w = frameWidth
|
||||
h = frameHeight
|
||||
}
|
||||
val ratioX = viewWidth / w.toFloat()
|
||||
val ratioY = viewHeight / h.toFloat()
|
||||
point.set(
|
||||
(point.x * ratioX).roundToInt() + offsetX,
|
||||
(point.y * ratioY).roundToInt() + offsetY
|
||||
)
|
||||
return point
|
||||
}
|
||||
|
||||
fun map(points: Array<ResultPoint?>): List<Point> =
|
||||
// because ZXing apparently returns null in this array sometimes
|
||||
points.filterNotNull().map { map(it) }
|
||||
|
||||
private fun rotate(point: Point) = when (frameOrientation) {
|
||||
90 -> point.set(frameHeight - point.y, point.x)
|
||||
180 -> point.set(frameWidth - point.x, frameHeight - point.y)
|
||||
270 -> point.set(point.y, frameWidth - point.x)
|
||||
else -> {
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,14 +7,90 @@ import android.preference.PreferenceManager
|
||||
class Preferences {
|
||||
lateinit var preferences: SharedPreferences
|
||||
|
||||
var cropHandleX = -1
|
||||
set(value) {
|
||||
apply(CROP_HANDLE_X, value)
|
||||
field = value
|
||||
}
|
||||
var cropHandleY = -1
|
||||
set(value) {
|
||||
apply(CROP_HANDLE_Y, value)
|
||||
field = value
|
||||
}
|
||||
var cropHandleOrientation = 0
|
||||
set(value) {
|
||||
apply(CROP_HANDLE_ORIENTATION, value)
|
||||
field = value
|
||||
}
|
||||
var showCropHandle = true
|
||||
set(value) {
|
||||
apply(SHOW_CROP_HANDLE, value)
|
||||
field = value
|
||||
}
|
||||
var zoomBySwiping = true
|
||||
set(value) {
|
||||
apply(ZOOM_BY_SWIPING, value)
|
||||
field = value
|
||||
}
|
||||
var autoRotate = false
|
||||
set(value) {
|
||||
// immediately save this setting before it shouldn't change
|
||||
// on the fly while scanning
|
||||
commit(AUTO_ROTATE, value)
|
||||
field = value
|
||||
}
|
||||
var tryHarder = false
|
||||
set(value) {
|
||||
// immediately save this setting because it's only ever read
|
||||
// before the camera is opened
|
||||
commit(TRY_HARDER, value)
|
||||
field = value
|
||||
}
|
||||
var vibrate = true
|
||||
set(value) {
|
||||
apply(VIBRATE, value)
|
||||
field = value
|
||||
}
|
||||
var useHistory = false
|
||||
set(value) {
|
||||
setBoolean(USE_HISTORY, value)
|
||||
apply(USE_HISTORY, value)
|
||||
field = value
|
||||
}
|
||||
var ignoreConsecutiveDuplicates = true
|
||||
set(value) {
|
||||
apply(IGNORE_CONSECUTIVE_DUPLICATES, value)
|
||||
field = value
|
||||
}
|
||||
var openImmediately = false
|
||||
set(value) {
|
||||
apply(OPEN_IMMEDIATELY, value)
|
||||
field = value
|
||||
}
|
||||
var showMetaData = true
|
||||
set(value) {
|
||||
apply(SHOW_META_DATA, value)
|
||||
field = value
|
||||
}
|
||||
var showHexDump = true
|
||||
set(value) {
|
||||
apply(SHOW_HEX_DUMP, value)
|
||||
field = value
|
||||
}
|
||||
var openWithUrl: String = ""
|
||||
set(value) {
|
||||
setString(OPEN_WITH_URL, value)
|
||||
apply(OPEN_WITH_URL, value)
|
||||
field = value
|
||||
}
|
||||
var indexOfLastSelectedFormat: Int = 0
|
||||
set(value) {
|
||||
apply(INDEX_OF_LAST_SELECTED_FORMAT, value)
|
||||
field = value
|
||||
}
|
||||
var forceCompat: Boolean = false
|
||||
set(value) {
|
||||
// since the app may be about to crash when forceCompat is set,
|
||||
// it's necessary to `commit()` this synchronously
|
||||
commit(FORCE_COMPAT, value)
|
||||
field = value
|
||||
}
|
||||
|
||||
@@ -24,24 +100,73 @@ class Preferences {
|
||||
}
|
||||
|
||||
fun update() {
|
||||
cropHandleX = preferences.getInt(CROP_HANDLE_X, cropHandleX)
|
||||
cropHandleY = preferences.getInt(CROP_HANDLE_Y, cropHandleY)
|
||||
cropHandleOrientation = preferences.getInt(
|
||||
CROP_HANDLE_ORIENTATION,
|
||||
cropHandleOrientation
|
||||
)
|
||||
showCropHandle = preferences.getBoolean(SHOW_CROP_HANDLE, showCropHandle)
|
||||
zoomBySwiping = preferences.getBoolean(ZOOM_BY_SWIPING, zoomBySwiping)
|
||||
autoRotate = preferences.getBoolean(AUTO_ROTATE, autoRotate)
|
||||
tryHarder = preferences.getBoolean(TRY_HARDER, tryHarder)
|
||||
vibrate = preferences.getBoolean(VIBRATE, vibrate)
|
||||
useHistory = preferences.getBoolean(USE_HISTORY, useHistory)
|
||||
openWithUrl = preferences.getString(OPEN_WITH_URL, openWithUrl)
|
||||
ignoreConsecutiveDuplicates = preferences.getBoolean(
|
||||
IGNORE_CONSECUTIVE_DUPLICATES,
|
||||
ignoreConsecutiveDuplicates
|
||||
)
|
||||
openImmediately = preferences.getBoolean(
|
||||
OPEN_IMMEDIATELY,
|
||||
openImmediately
|
||||
)
|
||||
showMetaData = preferences.getBoolean(SHOW_META_DATA, showMetaData)
|
||||
showHexDump = preferences.getBoolean(SHOW_HEX_DUMP, showHexDump)
|
||||
preferences.getString(OPEN_WITH_URL, openWithUrl)?.also {
|
||||
openWithUrl = it
|
||||
}
|
||||
indexOfLastSelectedFormat = preferences.getInt(
|
||||
INDEX_OF_LAST_SELECTED_FORMAT,
|
||||
indexOfLastSelectedFormat
|
||||
)
|
||||
forceCompat = preferences.getBoolean(FORCE_COMPAT, forceCompat)
|
||||
}
|
||||
|
||||
private fun setBoolean(label: String, value: Boolean) {
|
||||
val editor = preferences.edit()
|
||||
editor.putBoolean(label, value)
|
||||
editor.apply()
|
||||
private fun put(label: String, value: Boolean) =
|
||||
preferences.edit().putBoolean(label, value)
|
||||
|
||||
private fun commit(label: String, value: Boolean) {
|
||||
put(label, value).commit()
|
||||
}
|
||||
|
||||
private fun setString(label: String, value: String) {
|
||||
val editor = preferences.edit()
|
||||
editor.putString(label, value)
|
||||
editor.apply()
|
||||
private fun apply(label: String, value: Boolean) {
|
||||
put(label, value).apply()
|
||||
}
|
||||
|
||||
private fun apply(label: String, value: String) {
|
||||
preferences.edit().putString(label, value).apply()
|
||||
}
|
||||
|
||||
private fun apply(label: String, value: Int) {
|
||||
preferences.edit().putInt(label, value).apply()
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val CROP_HANDLE_X = "crop_handle_x"
|
||||
const val CROP_HANDLE_Y = "crop_handle_y"
|
||||
const val CROP_HANDLE_ORIENTATION = "crop_handle_orientation"
|
||||
const val SHOW_CROP_HANDLE = "show_crop_handle"
|
||||
const val ZOOM_BY_SWIPING = "zoom_by_swiping"
|
||||
const val AUTO_ROTATE = "auto_rotate"
|
||||
const val TRY_HARDER = "try_harder"
|
||||
const val VIBRATE = "vibrate"
|
||||
const val USE_HISTORY = "use_history"
|
||||
const val IGNORE_CONSECUTIVE_DUPLICATES = "ignore_consecutive_duplicates"
|
||||
const val OPEN_IMMEDIATELY = "open_immediately"
|
||||
const val SHOW_HEX_DUMP = "show_hex_dump"
|
||||
const val SHOW_META_DATA = "show_meta_data"
|
||||
const val OPEN_WITH_URL = "open_with_url"
|
||||
const val INDEX_OF_LAST_SELECTED_FORMAT = "index_of_last_selected_format"
|
||||
const val FORCE_COMPAT = "force_compat"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,29 +1,30 @@
|
||||
package de.markusfisch.android.binaryeye.rs
|
||||
|
||||
import de.markusfisch.android.binaryeye.renderscript.ScriptC_rotator
|
||||
|
||||
import android.content.Context
|
||||
import android.support.v8.renderscript.Allocation
|
||||
import android.support.v8.renderscript.Element
|
||||
import android.support.v8.renderscript.RenderScript
|
||||
import android.support.v8.renderscript.ScriptIntrinsicResize
|
||||
import android.support.v8.renderscript.Type
|
||||
import android.graphics.Rect
|
||||
import android.support.v8.renderscript.*
|
||||
import kotlin.math.max
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
private const val SCALE_FACTOR = .75f
|
||||
|
||||
class Preprocessor(
|
||||
context: Context,
|
||||
width: Int,
|
||||
height: Int,
|
||||
private val orientation: Int
|
||||
private val roi: Rect?
|
||||
) {
|
||||
var outWidth = 0
|
||||
var outHeight = 0
|
||||
|
||||
private val rs = RenderScript.create(context)
|
||||
private val resizeScript = ScriptIntrinsicResize.create(rs)
|
||||
private val rotatorScript = ScriptC_rotator(rs)
|
||||
private val rotateScript = ScriptC_rotate(rs)
|
||||
|
||||
private var yuvType: Type? = null
|
||||
private var yuvAlloc: Allocation? = null
|
||||
private var roiType: Type? = null
|
||||
private var roiAlloc: Allocation? = null
|
||||
private var resizedType: Type? = null
|
||||
private var resizedAlloc: Allocation? = null
|
||||
private var rotatedType: Type? = null
|
||||
@@ -42,15 +43,42 @@ class Preprocessor(
|
||||
Allocation.USAGE_SCRIPT
|
||||
)
|
||||
|
||||
val f = .75f
|
||||
var w = Math.round(width * f)
|
||||
var h = Math.round(height * f)
|
||||
if (roi != null) {
|
||||
val roiWidth = roi.width()
|
||||
val roiHeight = roi.height()
|
||||
|
||||
roiType = Type.createXY(
|
||||
rs,
|
||||
Element.U8(rs),
|
||||
roiWidth,
|
||||
roiHeight
|
||||
)
|
||||
roiAlloc = Allocation.createTyped(
|
||||
rs,
|
||||
roiType,
|
||||
Allocation.USAGE_SCRIPT
|
||||
)
|
||||
|
||||
outWidth = (roiWidth * SCALE_FACTOR).roundToInt()
|
||||
outHeight = (roiHeight * SCALE_FACTOR).roundToInt()
|
||||
|
||||
// make sure the dimensions are always a multiple of 4
|
||||
outWidth -= outWidth % 4
|
||||
outHeight -= outHeight % 4
|
||||
|
||||
// make sure the dimensions are always greater than 4
|
||||
outWidth = max(4, outWidth)
|
||||
outHeight = max(4, outHeight)
|
||||
} else {
|
||||
outWidth = (width * SCALE_FACTOR).roundToInt()
|
||||
outHeight = (height * SCALE_FACTOR).roundToInt()
|
||||
}
|
||||
|
||||
resizedType = Type.createXY(
|
||||
rs,
|
||||
Element.U8(rs),
|
||||
w,
|
||||
h
|
||||
outWidth,
|
||||
outHeight
|
||||
)
|
||||
resizedAlloc = Allocation.createTyped(
|
||||
rs,
|
||||
@@ -58,28 +86,17 @@ class Preprocessor(
|
||||
Allocation.USAGE_SCRIPT
|
||||
)
|
||||
|
||||
if (orientation == 90 || orientation == 270) {
|
||||
val tmp = w
|
||||
w = h
|
||||
h = tmp
|
||||
}
|
||||
|
||||
outWidth = w
|
||||
outHeight = h
|
||||
|
||||
if (orientation != 0) {
|
||||
rotatedType = Type.createXY(
|
||||
rs,
|
||||
Element.U8(rs),
|
||||
w,
|
||||
h
|
||||
)
|
||||
rotatedAlloc = Allocation.createTyped(
|
||||
rs,
|
||||
rotatedType,
|
||||
Allocation.USAGE_SCRIPT
|
||||
)
|
||||
}
|
||||
rotatedType = Type.createXY(
|
||||
rs,
|
||||
Element.U8(rs),
|
||||
outHeight,
|
||||
outWidth
|
||||
)
|
||||
rotatedAlloc = Allocation.createTyped(
|
||||
rs,
|
||||
rotatedType,
|
||||
Allocation.USAGE_SCRIPT
|
||||
)
|
||||
}
|
||||
|
||||
fun destroy() {
|
||||
@@ -87,6 +104,10 @@ class Preprocessor(
|
||||
yuvType = null
|
||||
yuvAlloc?.destroy()
|
||||
yuvAlloc = null
|
||||
roiType?.destroy()
|
||||
roiType = null
|
||||
roiAlloc?.destroy()
|
||||
roiAlloc = null
|
||||
resizedType?.destroy()
|
||||
resizedType = null
|
||||
resizedAlloc?.destroy()
|
||||
@@ -96,38 +117,43 @@ class Preprocessor(
|
||||
rotatedAlloc?.destroy()
|
||||
rotatedAlloc = null
|
||||
resizeScript.destroy()
|
||||
rotatorScript.destroy()
|
||||
rotateScript.destroy()
|
||||
rs.destroy()
|
||||
}
|
||||
|
||||
fun process(frame: ByteArray) {
|
||||
fun resizeOnly(frame: ByteArray) {
|
||||
resize(frame)
|
||||
resizedAlloc?.copyTo(frame)
|
||||
}
|
||||
|
||||
fun resizeAndRotate(frame: ByteArray) {
|
||||
resize(frame)
|
||||
val t = resizedType ?: return
|
||||
rotateScript._inImage = resizedAlloc
|
||||
rotateScript._inWidth = t.x
|
||||
rotateScript._inHeight = t.y
|
||||
rotateScript.forEach_rotate90(
|
||||
rotatedAlloc, // ignored in kernel, just to satisfy forEach
|
||||
rotatedAlloc
|
||||
)
|
||||
rotatedAlloc?.copyTo(frame)
|
||||
}
|
||||
|
||||
private fun resize(frame: ByteArray) {
|
||||
yuvAlloc?.copyFrom(frame)
|
||||
|
||||
resizeScript.setInput(yuvAlloc)
|
||||
resizeScript.forEach_bicubic(resizedAlloc)
|
||||
|
||||
val t = resizedType
|
||||
if (t == null || orientation == 0) {
|
||||
resizedAlloc?.copyTo(frame)
|
||||
} else {
|
||||
rotatorScript._inImage = resizedAlloc
|
||||
rotatorScript._inWidth = t.x
|
||||
rotatorScript._inHeight = t.y
|
||||
when (orientation) {
|
||||
90 -> rotatorScript.forEach_rotate90(
|
||||
rotatedAlloc,
|
||||
rotatedAlloc
|
||||
)
|
||||
180 -> rotatorScript.forEach_rotate180(
|
||||
rotatedAlloc,
|
||||
rotatedAlloc
|
||||
)
|
||||
270 -> rotatorScript.forEach_rotate270(
|
||||
rotatedAlloc,
|
||||
rotatedAlloc
|
||||
resizeScript.setInput(
|
||||
if (roi != null) {
|
||||
roiAlloc?.copy2DRangeFrom(
|
||||
0, 0,
|
||||
roi.width(), roi.height(),
|
||||
yuvAlloc,
|
||||
roi.left, roi.top
|
||||
)
|
||||
roiAlloc
|
||||
} else {
|
||||
yuvAlloc
|
||||
}
|
||||
rotatedAlloc?.copyTo(frame)
|
||||
}
|
||||
)
|
||||
resizeScript.forEach_bicubic(resizedAlloc)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
package de.markusfisch.android.binaryeye.view
|
||||
|
||||
import android.graphics.Rect
|
||||
import android.view.View
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
suspend inline fun View.useVisibility(
|
||||
whileExecuting: Int = View.VISIBLE,
|
||||
otherwise: Int = View.GONE,
|
||||
crossinline block: suspend () -> Unit
|
||||
) {
|
||||
if (visibility == whileExecuting) {
|
||||
return
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
visibility = whileExecuting
|
||||
}
|
||||
try {
|
||||
block()
|
||||
} finally {
|
||||
withContext(Dispatchers.Main) {
|
||||
visibility = otherwise
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun View.setPadding(rect: Rect) {
|
||||
this.setPadding(
|
||||
rect.left,
|
||||
rect.top,
|
||||
rect.right,
|
||||
rect.bottom
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package de.markusfisch.android.binaryeye.view
|
||||
|
||||
import android.graphics.Rect
|
||||
import android.os.Build
|
||||
import android.support.annotation.RequiresApi
|
||||
import android.support.v4.view.ViewCompat
|
||||
import android.support.v4.view.WindowInsetsCompat
|
||||
import android.support.v7.widget.Toolbar
|
||||
import android.view.View
|
||||
|
||||
private var toolbarHeight = 0
|
||||
fun recordToolbarHeight(toolbar: Toolbar) {
|
||||
toolbarHeight = toolbar.layoutParams.height
|
||||
}
|
||||
|
||||
fun View.setPaddingFromWindowInsets() {
|
||||
doOnApplyWindowInsets { v, insets -> v.setPadding(insets) }
|
||||
}
|
||||
|
||||
// A slight variation of the idea from Google's Chris Banes to avoid
|
||||
// adding the toolbar height for every view that needs to be inset.
|
||||
// For the original post see here:
|
||||
// https://medium.com/androiddevelopers/windowinsets-listeners-to-layouts-8f9ccc8fa4d1
|
||||
fun View.doOnApplyWindowInsets(f: (View, Rect) -> Unit) {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
|
||||
f(this, insetsWithToolbar())
|
||||
} else {
|
||||
ViewCompat.setOnApplyWindowInsetsListener(this) { v, insets ->
|
||||
f(v, insetsWithToolbar(insets))
|
||||
insets
|
||||
}
|
||||
// it's important to explicitly request the insets (again) in
|
||||
// case the view was created in Fragment.onCreateView() because
|
||||
// setOnApplyWindowInsetsListener() won't fire when the view
|
||||
// isn't attached
|
||||
requestApplyInsetsWhenAttached()
|
||||
}
|
||||
}
|
||||
|
||||
private fun insetsWithToolbar(insets: WindowInsetsCompat? = null) = Rect(
|
||||
insets?.systemWindowInsetLeft ?: 0,
|
||||
(insets?.systemWindowInsetTop ?: 0) + toolbarHeight,
|
||||
insets?.systemWindowInsetRight ?: 0,
|
||||
insets?.systemWindowInsetBottom ?: 0
|
||||
)
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.KITKAT)
|
||||
private fun View.requestApplyInsetsWhenAttached() {
|
||||
if (isAttachedToWindow) {
|
||||
ViewCompat.requestApplyInsets(this)
|
||||
} else {
|
||||
addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener {
|
||||
override fun onViewAttachedToWindow(v: View) {
|
||||
v.removeOnAttachStateChangeListener(this)
|
||||
ViewCompat.requestApplyInsets(v)
|
||||
}
|
||||
|
||||
override fun onViewDetachedFromWindow(v: View) = Unit
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package de.markusfisch.android.binaryeye.widget
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Rect
|
||||
import android.util.AttributeSet
|
||||
import de.markusfisch.android.binaryeye.app.colorSystemAndToolBars
|
||||
import de.markusfisch.android.scalingimageview.widget.ScalingImageView
|
||||
|
||||
open class ConfinedScalingImageView : ScalingImageView {
|
||||
val insets = Rect()
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet, defStyle: Int) :
|
||||
super(context, attrs, defStyle)
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet) :
|
||||
this(context, attrs, 0)
|
||||
|
||||
override fun onDraw(canvas: Canvas) {
|
||||
super.onDraw(canvas)
|
||||
colorSystemAndToolBars(context, !bounds.contains(mappedRect))
|
||||
}
|
||||
|
||||
override fun layoutImage(
|
||||
changed: Boolean,
|
||||
left: Int,
|
||||
top: Int,
|
||||
right: Int,
|
||||
bottom: Int
|
||||
) {
|
||||
if (changed) {
|
||||
setBounds(
|
||||
(left + insets.left).toFloat(),
|
||||
(top + insets.top).toFloat(),
|
||||
(right - insets.right).toFloat(),
|
||||
(bottom - insets.bottom).toFloat()
|
||||
)
|
||||
}
|
||||
centerRemap()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package de.markusfisch.android.binaryeye.widget
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.widget.ScrollView
|
||||
import de.markusfisch.android.binaryeye.app.colorSystemAndToolBars
|
||||
|
||||
class ConfinedScrollView : ScrollView {
|
||||
private var scrollable = false
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet, defStyle: Int) :
|
||||
super(context, attrs, defStyle)
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet) :
|
||||
this(context, attrs, 0)
|
||||
|
||||
override fun onLayout(
|
||||
changed: Boolean,
|
||||
left: Int,
|
||||
top: Int,
|
||||
right: Int,
|
||||
bottom: Int
|
||||
) {
|
||||
super.onLayout(changed, left, top, right, bottom)
|
||||
if (changed) {
|
||||
// give Android some time to settle down before running this,
|
||||
// not putting it on the queue makes it only work sometimes
|
||||
post {
|
||||
getChildAt(0)?.also { child ->
|
||||
scrollable = height < child.height + paddingTop + paddingBottom
|
||||
colorSystemAndToolBars(
|
||||
context,
|
||||
scrollY > 0,
|
||||
scrollable
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onScrollChanged(x: Int, y: Int, oldx: Int, oldy: Int) {
|
||||
super.onScrollChanged(x, y, oldx, oldy)
|
||||
colorSystemAndToolBars(context, y > 0, scrollable)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
package de.markusfisch.android.binaryeye.widget
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Point
|
||||
import android.graphics.Rect
|
||||
import android.graphics.RectF
|
||||
import android.util.AttributeSet
|
||||
import de.markusfisch.android.binaryeye.graphics.Dots
|
||||
import de.markusfisch.android.binaryeye.graphics.getDashedBorderPaint
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
class CropImageView(context: Context, attr: AttributeSet) :
|
||||
ConfinedScalingImageView(context, attr) {
|
||||
val windowInsets = Rect()
|
||||
|
||||
var onScan: (() -> Unit)? = null
|
||||
|
||||
private val dots = Dots(context)
|
||||
private val boundsPaint = context.getDashedBorderPaint()
|
||||
private val lastMappedRect = RectF()
|
||||
private val padding: Int = (24f * context.resources.displayMetrics.density).roundToInt()
|
||||
private val onScanRunnable = Runnable { onScan?.invoke() }
|
||||
|
||||
private var resultPoints: List<Point>? = null
|
||||
|
||||
init {
|
||||
scaleType = ScaleType.CENTER_CROP
|
||||
}
|
||||
|
||||
fun updateResultPoints(points: List<Point>) {
|
||||
resultPoints = points
|
||||
invalidate()
|
||||
}
|
||||
|
||||
override fun onLayout(
|
||||
changed: Boolean,
|
||||
left: Int,
|
||||
top: Int,
|
||||
right: Int,
|
||||
bottom: Int
|
||||
) {
|
||||
super.onLayout(changed, left, top, right, bottom)
|
||||
setBoundsWithPadding(
|
||||
left + windowInsets.left,
|
||||
top + windowInsets.top,
|
||||
right - windowInsets.right,
|
||||
bottom - windowInsets.bottom
|
||||
)
|
||||
}
|
||||
|
||||
fun getBoundsRect() = Rect(
|
||||
bounds.left.roundToInt(),
|
||||
bounds.top.roundToInt(),
|
||||
bounds.right.roundToInt(),
|
||||
bounds.bottom.roundToInt()
|
||||
)
|
||||
|
||||
private fun setBoundsWithPadding(
|
||||
left: Int,
|
||||
top: Int,
|
||||
right: Int,
|
||||
bottom: Int
|
||||
) {
|
||||
val width = right - left
|
||||
val height = bottom - top
|
||||
val size = if (width < height)
|
||||
width - padding * 2
|
||||
else
|
||||
height - padding * 2
|
||||
val hpad = (width - size) / 2
|
||||
val vpad = (height - size) / 2
|
||||
setBounds(
|
||||
left.toFloat() + hpad,
|
||||
top.toFloat() + vpad,
|
||||
right.toFloat() - hpad,
|
||||
bottom.toFloat() - vpad
|
||||
)
|
||||
center(bounds)
|
||||
}
|
||||
|
||||
override fun onDraw(canvas: Canvas) {
|
||||
super.onDraw(canvas)
|
||||
canvas.drawRect(bounds, boundsPaint)
|
||||
resultPoints?.let {
|
||||
dots.draw(canvas, it)
|
||||
}
|
||||
resultPoints = null
|
||||
val mr = mappedRect ?: return
|
||||
if (mr != lastMappedRect) {
|
||||
removeCallbacks(onScanRunnable)
|
||||
postDelayed(onScanRunnable, 500)
|
||||
lastMappedRect.set(mr)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,367 @@
|
||||
package de.markusfisch.android.binaryeye.widget
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.graphics.*
|
||||
import android.os.Build
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
import android.util.AttributeSet
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.app.prefs
|
||||
import de.markusfisch.android.binaryeye.graphics.Dots
|
||||
import de.markusfisch.android.binaryeye.graphics.getBitmapFromDrawable
|
||||
import de.markusfisch.android.binaryeye.graphics.getDashedBorderPaint
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
class DetectorView : View {
|
||||
val currentOrientation = resources.configuration.orientation
|
||||
val roi = Rect()
|
||||
|
||||
var onRoiChange: (() -> Unit)? = null
|
||||
var onRoiChanged: (() -> Unit)? = null
|
||||
|
||||
private val orientation = resources.configuration.orientation
|
||||
private val dots = Dots(context)
|
||||
private val invalidateRunnable: Runnable = Runnable {
|
||||
marks = null
|
||||
invalidate()
|
||||
}
|
||||
private val roiPaint = context.getDashedBorderPaint()
|
||||
private val handleBitmap = resources.getBitmapFromDrawable(
|
||||
R.drawable.button_crop
|
||||
)
|
||||
private val handleXRadius = handleBitmap.width / 2
|
||||
private val handleYRadius = handleBitmap.height / 2
|
||||
private val handlePos = Point(-1, -1)
|
||||
private val handleHome = Point()
|
||||
private val center = Point()
|
||||
private val touchDown = Point()
|
||||
private val distToFull: Int
|
||||
private val minMoveThresholdSq: Int
|
||||
private val cornerRadius: Int
|
||||
private val fabHeight: Int
|
||||
private val padding: Int
|
||||
|
||||
private var marks: List<Point>? = null
|
||||
private var handleGrabbed = false
|
||||
private var handleActive = false
|
||||
private var minY = 0
|
||||
private var maxY = 0
|
||||
private var minDist = 0
|
||||
private var shadeColor = 0
|
||||
|
||||
init {
|
||||
val dp = context.resources.displayMetrics.density
|
||||
distToFull = (24f * dp).roundToInt()
|
||||
val minMoveThreshold = (8f * dp).roundToInt()
|
||||
minMoveThresholdSq = minMoveThreshold * minMoveThreshold
|
||||
cornerRadius = (8f * dp).roundToInt()
|
||||
fabHeight = (92f * dp).roundToInt()
|
||||
padding = (20f * dp).roundToInt()
|
||||
isSaveEnabled = true
|
||||
}
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet) :
|
||||
super(context, attrs)
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) :
|
||||
super(context, attrs, defStyleAttr)
|
||||
|
||||
fun setCropHandlePos(x: Int, y: Int, orientation: Int) {
|
||||
if (orientation == currentOrientation) {
|
||||
handlePos.set(x, y)
|
||||
} else {
|
||||
handlePos.set(y, x)
|
||||
}
|
||||
if (x > -1) {
|
||||
handleActive = true
|
||||
}
|
||||
}
|
||||
|
||||
fun getCropHandlePos() = if (handleActive) {
|
||||
handlePos
|
||||
} else {
|
||||
Point(-1, -1)
|
||||
}
|
||||
|
||||
fun mark(points: List<Point>) {
|
||||
marks = points
|
||||
invalidate()
|
||||
removeCallbacks(invalidateRunnable)
|
||||
postDelayed(invalidateRunnable, 500)
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(): Parcelable? {
|
||||
if (!handleActive) {
|
||||
return super.onSaveInstanceState()
|
||||
}
|
||||
return SavedState(super.onSaveInstanceState()).apply {
|
||||
savedHandlePos.set(getCropHandlePos())
|
||||
savedOrientation = currentOrientation
|
||||
}
|
||||
}
|
||||
|
||||
override fun onRestoreInstanceState(state: Parcelable) {
|
||||
super.onRestoreInstanceState(
|
||||
if (state is SavedState) {
|
||||
setCropHandlePos(
|
||||
state.savedHandlePos.x,
|
||||
state.savedHandlePos.y,
|
||||
state.savedOrientation
|
||||
)
|
||||
state.superState
|
||||
} else {
|
||||
state
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
override fun onTouchEvent(event: MotionEvent?): Boolean {
|
||||
event ?: return super.onTouchEvent(event)
|
||||
val x = event.x.roundToInt()
|
||||
val y = event.y.roundToInt()
|
||||
return when (event.actionMasked) {
|
||||
MotionEvent.ACTION_DOWN -> {
|
||||
if (prefs.showCropHandle) {
|
||||
touchDown.set(x, y)
|
||||
handleGrabbed = abs(x - handlePos.x) < handleXRadius &&
|
||||
abs(y - handlePos.y) < handleYRadius
|
||||
if (handleGrabbed) {
|
||||
onRoiChange?.invoke()
|
||||
}
|
||||
handleGrabbed
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
MotionEvent.ACTION_MOVE -> {
|
||||
if (handleGrabbed) {
|
||||
handlePos.set(x, y)
|
||||
if (distSq(handlePos, touchDown) > minMoveThresholdSq) {
|
||||
handleActive = true
|
||||
}
|
||||
updateClipRect()
|
||||
invalidate()
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
MotionEvent.ACTION_CANCEL -> {
|
||||
if (handleGrabbed) {
|
||||
snap(x, y)
|
||||
handleGrabbed = false
|
||||
}
|
||||
false
|
||||
}
|
||||
MotionEvent.ACTION_UP -> {
|
||||
if (handleGrabbed) {
|
||||
if (!handleActive) {
|
||||
val mn = min(center.x, center.y) * .8f
|
||||
handlePos.set(
|
||||
(center.x + mn).roundToInt(),
|
||||
(center.y + mn).roundToInt()
|
||||
)
|
||||
handleActive = true
|
||||
invalidate()
|
||||
} else {
|
||||
snap(x, y)
|
||||
}
|
||||
updateClipRect()
|
||||
onRoiChanged?.invoke()
|
||||
handleGrabbed = false
|
||||
}
|
||||
false
|
||||
}
|
||||
else -> super.onTouchEvent(event)
|
||||
}
|
||||
}
|
||||
|
||||
private fun snap(x: Int, y: Int) {
|
||||
if (abs(x - center.x) < distToFull ||
|
||||
abs(y - center.y) < distToFull
|
||||
) {
|
||||
reset()
|
||||
invalidate()
|
||||
}
|
||||
}
|
||||
|
||||
private fun reset() {
|
||||
handlePos.set(handleHome)
|
||||
handleActive = false
|
||||
roi.set(0, 0, 0, 0)
|
||||
}
|
||||
|
||||
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
|
||||
super.onLayout(changed, left, top, right, bottom)
|
||||
if (!changed) {
|
||||
return
|
||||
}
|
||||
val width = right - left
|
||||
val height = bottom - top
|
||||
center.set(
|
||||
left + (width / 2),
|
||||
top + (height / 2)
|
||||
)
|
||||
minY = padding * 2
|
||||
maxY = height - minY
|
||||
handleHome.set(
|
||||
width - handleXRadius - paddingRight - padding,
|
||||
height - handleYRadius - paddingBottom - fabHeight
|
||||
)
|
||||
if (handleActive) {
|
||||
updateClipRect()
|
||||
} else {
|
||||
reset()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDraw(canvas: Canvas) {
|
||||
canvas.drawColor(0, PorterDuff.Mode.CLEAR)
|
||||
if (handleActive) {
|
||||
drawClip(canvas)
|
||||
}
|
||||
marks?.let {
|
||||
dots.draw(canvas, it)
|
||||
}
|
||||
if (prefs.showCropHandle) {
|
||||
canvas.drawBitmap(
|
||||
handleBitmap,
|
||||
(handlePos.x - handleXRadius).toFloat(),
|
||||
(handlePos.y - handleYRadius).toFloat(),
|
||||
null
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun drawClip(canvas: Canvas) {
|
||||
if (minDist < 1) {
|
||||
return
|
||||
}
|
||||
// canvas.clipRect() doesn't work reliably below KITKAT
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||
val radius = min(minDist / 2, cornerRadius).toFloat()
|
||||
canvas.save()
|
||||
canvas.clipOutPathCompat(
|
||||
calculateRoundedRectPath(
|
||||
roi.left.toFloat(),
|
||||
roi.top.toFloat(),
|
||||
roi.right.toFloat(),
|
||||
roi.bottom.toFloat(),
|
||||
radius,
|
||||
radius
|
||||
)
|
||||
)
|
||||
canvas.drawColor(shadeColor, PorterDuff.Mode.SRC)
|
||||
canvas.restore()
|
||||
} else {
|
||||
canvas.drawRect(roi, roiPaint)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateClipRect() {
|
||||
clampHandlePos()
|
||||
val dx = abs(handlePos.x - center.x)
|
||||
val dy = abs(handlePos.y - center.y)
|
||||
minDist = min(dx, dy)
|
||||
shadeColor = (min(
|
||||
1f,
|
||||
minDist.toFloat() / distToFull.toFloat()
|
||||
) * 128f).toInt() shl 24
|
||||
roi.set(
|
||||
center.x - dx,
|
||||
center.y - dy,
|
||||
center.x + dx,
|
||||
center.y + dy
|
||||
)
|
||||
}
|
||||
|
||||
private fun clampHandlePos() {
|
||||
handlePos.x = min(center.x * 2, max(0, handlePos.x))
|
||||
handlePos.y = min(maxY, max(minY, handlePos.y))
|
||||
}
|
||||
|
||||
internal class SavedState : BaseSavedState {
|
||||
val savedHandlePos = Point()
|
||||
|
||||
var savedOrientation = 0
|
||||
|
||||
constructor(superState: Parcelable?) : super(superState)
|
||||
|
||||
private constructor(parcel: Parcel) : super(parcel) {
|
||||
savedHandlePos.set(
|
||||
parcel.readInt(),
|
||||
parcel.readInt()
|
||||
)
|
||||
savedOrientation = parcel.readInt()
|
||||
}
|
||||
|
||||
override fun writeToParcel(out: Parcel, flags: Int) {
|
||||
super.writeToParcel(out, flags)
|
||||
out.writeInt(savedHandlePos.x)
|
||||
out.writeInt(savedHandlePos.y)
|
||||
out.writeInt(savedOrientation)
|
||||
}
|
||||
|
||||
companion object {
|
||||
@JvmField
|
||||
val CREATOR = object : Parcelable.Creator<SavedState> {
|
||||
override fun createFromParcel(source: Parcel) = SavedState(source)
|
||||
override fun newArray(size: Int): Array<SavedState?> = arrayOfNulls(size)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun distSq(a: Point, b: Point): Int {
|
||||
val dx = a.x - b.x
|
||||
val dy = a.y - b.y
|
||||
return dx * dx + dy * dy
|
||||
}
|
||||
|
||||
private fun Point.set(point: Point) {
|
||||
x = point.x
|
||||
y = point.y
|
||||
}
|
||||
|
||||
private fun Canvas.clipOutPathCompat(path: Path) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
clipOutPath(path)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
clipPath(path, Region.Op.DIFFERENCE)
|
||||
}
|
||||
}
|
||||
|
||||
private fun calculateRoundedRectPath(
|
||||
left: Float,
|
||||
top: Float,
|
||||
right: Float,
|
||||
bottom: Float,
|
||||
rx: Float,
|
||||
ry: Float
|
||||
): Path {
|
||||
val width = right - left
|
||||
val height = bottom - top
|
||||
val widthMinusCorners = width - 2 * rx
|
||||
val heightMinusCorners = height - 2 * ry
|
||||
return Path().apply {
|
||||
moveTo(right, top + ry)
|
||||
rQuadTo(0f, -ry, -rx, -ry)
|
||||
rLineTo(-widthMinusCorners, 0f)
|
||||
rQuadTo(-rx, 0f, -rx, ry)
|
||||
rLineTo(0f, heightMinusCorners)
|
||||
rQuadTo(0f, ry, rx, ry)
|
||||
rLineTo(widthMinusCorners, 0f)
|
||||
rQuadTo(rx, 0f, rx, -ry)
|
||||
rLineTo(0f, -heightMinusCorners)
|
||||
close()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package de.markusfisch.android.binaryeye.widget
|
||||
|
||||
import android.content.Context
|
||||
import android.widget.Toast
|
||||
|
||||
fun Context.toast(message: Int) = Toast.makeText(
|
||||
this,
|
||||
message,
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
|
||||
fun Context.toast(message: String) = Toast.makeText(
|
||||
this,
|
||||
message,
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
@@ -1,26 +1,13 @@
|
||||
package de.markusfisch.android.binaryeye.zxing
|
||||
|
||||
import com.google.zxing.BarcodeFormat
|
||||
import com.google.zxing.BinaryBitmap
|
||||
import com.google.zxing.DecodeHintType
|
||||
import com.google.zxing.EncodeHintType
|
||||
import com.google.zxing.LuminanceSource
|
||||
import com.google.zxing.MultiFormatReader
|
||||
import com.google.zxing.MultiFormatWriter
|
||||
import com.google.zxing.PlanarYUVLuminanceSource
|
||||
import com.google.zxing.RGBLuminanceSource
|
||||
import com.google.zxing.ReaderException
|
||||
import com.google.zxing.Result
|
||||
import com.google.zxing.common.HybridBinarizer
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import com.google.zxing.*
|
||||
import com.google.zxing.common.HybridBinarizer
|
||||
import java.util.*
|
||||
|
||||
import java.util.Arrays
|
||||
import java.util.EnumMap
|
||||
import java.util.EnumSet
|
||||
|
||||
class Zxing {
|
||||
private val multiFormatReader: MultiFormatReader = MultiFormatReader()
|
||||
class Zxing(possibleResultPoint: ResultPointCallback? = null) {
|
||||
private val hints = EnumMap<DecodeHintType, Any>(DecodeHintType::class.java)
|
||||
private val multiFormatReader = MultiFormatReader()
|
||||
|
||||
init {
|
||||
val decodeFormats = EnumSet.noneOf<BarcodeFormat>(
|
||||
@@ -28,7 +15,7 @@ class Zxing {
|
||||
)
|
||||
decodeFormats.addAll(
|
||||
EnumSet.copyOf(
|
||||
Arrays.asList(
|
||||
listOf(
|
||||
BarcodeFormat.AZTEC,
|
||||
BarcodeFormat.CODABAR,
|
||||
BarcodeFormat.CODE_39,
|
||||
@@ -49,11 +36,18 @@ class Zxing {
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
val hints = EnumMap<DecodeHintType, Any>(DecodeHintType::class.java)
|
||||
hints[DecodeHintType.POSSIBLE_FORMATS] = decodeFormats
|
||||
possibleResultPoint?.let {
|
||||
hints[DecodeHintType.NEED_RESULT_POINT_CALLBACK] = it
|
||||
}
|
||||
}
|
||||
|
||||
multiFormatReader.setHints(hints)
|
||||
fun updateHints(tryHarder: Boolean) {
|
||||
if (tryHarder) {
|
||||
hints[DecodeHintType.TRY_HARDER] = java.lang.Boolean.TRUE
|
||||
} else {
|
||||
hints.remove(DecodeHintType.TRY_HARDER)
|
||||
}
|
||||
}
|
||||
|
||||
fun decode(
|
||||
@@ -75,20 +69,15 @@ class Zxing {
|
||||
return decodeLuminanceSource(source, invert)
|
||||
}
|
||||
|
||||
fun decodePositiveNegative(bitmap: Bitmap): Result? {
|
||||
val result = decode(bitmap, false)
|
||||
if (result != null) {
|
||||
return result
|
||||
}
|
||||
return decode(bitmap, true)
|
||||
}
|
||||
fun decodePositiveNegative(bitmap: Bitmap): Result? =
|
||||
decode(bitmap, false) ?: decode(bitmap, true)
|
||||
|
||||
fun decode(bitmap: Bitmap, invert: Boolean = false): Result? {
|
||||
private fun decode(bitmap: Bitmap, invert: Boolean = false): Result? {
|
||||
val pixels = IntArray(bitmap.width * bitmap.height)
|
||||
return decode(pixels, bitmap, invert)
|
||||
}
|
||||
|
||||
fun decode(
|
||||
private fun decode(
|
||||
pixels: IntArray,
|
||||
bitmap: Bitmap,
|
||||
invert: Boolean = false
|
||||
@@ -100,8 +89,10 @@ class Zxing {
|
||||
} else {
|
||||
bitmap
|
||||
}.getPixels(pixels, 0, width, 0, 0, width, height)
|
||||
val source = RGBLuminanceSource(width, height, pixels)
|
||||
return decodeLuminanceSource(source, invert)
|
||||
return decodeLuminanceSource(
|
||||
RGBLuminanceSource(width, height, pixels),
|
||||
invert
|
||||
)
|
||||
}
|
||||
|
||||
private fun decodeLuminanceSource(
|
||||
@@ -120,7 +111,7 @@ class Zxing {
|
||||
private fun decodeLuminanceSource(source: LuminanceSource): Result? {
|
||||
val bitmap = BinaryBitmap(HybridBinarizer(source))
|
||||
return try {
|
||||
multiFormatReader.decodeWithState(bitmap)
|
||||
multiFormatReader.decode(bitmap, hints)
|
||||
} catch (e: ReaderException) {
|
||||
null
|
||||
} finally {
|
||||
@@ -165,5 +156,42 @@ class Zxing {
|
||||
bitmap.setPixels(pixels, 0, w, 0, 0, w, h)
|
||||
return bitmap
|
||||
}
|
||||
|
||||
fun encodeAsSvg(
|
||||
text: String,
|
||||
format: BarcodeFormat,
|
||||
width: Int,
|
||||
height: Int
|
||||
): String {
|
||||
val hints = EnumMap<EncodeHintType, Any>(EncodeHintType::class.java)
|
||||
hints[EncodeHintType.CHARACTER_SET] = "utf-8"
|
||||
val result = MultiFormatWriter().encode(
|
||||
text,
|
||||
format,
|
||||
0,
|
||||
0,
|
||||
hints
|
||||
)
|
||||
val sb = StringBuilder()
|
||||
sb.append("<svg width=\"$width\" height=\"$height\"")
|
||||
sb.append(" viewBox=\"0 0 $width $height\"")
|
||||
sb.append(" xmlns=\"http://www.w3.org/2000/svg\">\n")
|
||||
val w = result.width
|
||||
val h = result.height
|
||||
val xf = width.toFloat() / w
|
||||
val yf = height.toFloat() / h
|
||||
for (y in 0 until h) {
|
||||
for (x in 0 until w) {
|
||||
if (result.get(x, y)) {
|
||||
val ox = x * xf
|
||||
val oy = y * yf
|
||||
sb.append("<rect x=\"$ox\" y=\"$oy\"")
|
||||
sb.append(" width=\"$xf\" height=\"$yf\"/>\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
sb.append("</svg>\n")
|
||||
return sb.toString()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 2.4 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 899 B |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 3.3 KiB |
|
After Width: | Height: | Size: 4.6 KiB |
|
After Width: | Height: | Size: 3.3 KiB |
|
After Width: | Height: | Size: 4.9 KiB |
|
After Width: | Height: | Size: 6.2 KiB |
|
After Width: | Height: | Size: 4.4 KiB |