Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
e75d658092 | ||
|
|
c840462fa2 | ||
|
|
c3475a37d2 | ||
|
|
183fe2c6f2 | ||
|
|
056f5e263e | ||
|
|
386c7a6056 | ||
|
|
80c876f35d | ||
|
|
30ae51d945 | ||
|
|
95b54ed4ab | ||
|
|
3650bda107 | ||
|
|
2bdf13761e | ||
|
|
ee4ee1f778 | ||
|
|
dbb72747e9 | ||
|
|
8e577c0924 | ||
|
|
6ce160447c | ||
|
|
b94a47ccb4 | ||
|
|
ec34b33ce7 | ||
|
|
024b95c12e | ||
|
|
cf4dc2aa4a | ||
|
|
1919ce2935 | ||
|
|
1e3dfb64ae | ||
|
|
f87eaa82dd | ||
|
|
13b90186d3 | ||
|
|
2c4e329663 | ||
|
|
624e129ef7 | ||
|
|
2f19bd7fc9 | ||
|
|
da48539585 | ||
|
|
a1d263837a | ||
|
|
188824ecaa | ||
|
|
f5376290ab | ||
|
|
b888e37556 | ||
|
|
cb43160902 | ||
|
|
a3ad6aa4c5 | ||
|
|
d0f0bedae2 | ||
|
|
3f58349ce8 | ||
|
|
e90d7d5f0e | ||
|
|
b39e5d2721 | ||
|
|
3ede2713b6 | ||
|
|
dc2fc292ce | ||
|
|
3dc50d2d7a | ||
|
|
3d15425460 | ||
|
|
412e27ee81 | ||
|
|
d73ea341c0 | ||
|
|
694ebf83ed | ||
|
|
78bb03fabf | ||
|
|
902215f064 | ||
|
|
eaf38729bd | ||
|
|
bdd89ca539 | ||
|
|
39ecd31455 | ||
|
|
c96a2cf3ec | ||
|
|
ea9e29b98c | ||
|
|
96e1ef5a52 | ||
|
|
ed4b0a75dd | ||
|
|
37354338cb | ||
|
|
3cd2eecc22 | ||
|
|
0e40d3f7c3 | ||
|
|
c8fa01116d | ||
|
|
04539b4c37 | ||
|
|
b9e8f4cfca | ||
|
|
ad06c2bf9e | ||
|
|
2c7ec74b1b | ||
|
|
b34f29c95f | ||
|
|
e11518c3f5 | ||
|
|
57c4fad307 | ||
|
|
856b8e37e1 | ||
|
|
6b8dd8e864 | ||
|
|
c993cd53f1 | ||
|
|
934983f668 | ||
|
|
ccf425faad | ||
|
|
a244763109 | ||
|
|
7ab712e040 | ||
|
|
cb7219b290 | ||
|
|
7f4ba50ee4 | ||
|
|
b1f00f0592 | ||
|
|
80f8f908d2 | ||
|
|
506308ece9 | ||
|
|
baecded9d5 | ||
|
|
ceefee397f | ||
|
|
e45804be31 | ||
|
|
edef1acbfb | ||
|
|
742939182c | ||
|
|
a21bfcdea6 | ||
|
|
bd281e449b | ||
|
|
0e1464a15f | ||
|
|
8a6441ddf3 | ||
|
|
94874c0a9e | ||
|
|
b9a6410959 | ||
|
|
512f5e00dd | ||
|
|
91c2364958 | ||
|
|
1e3f1c553c | ||
|
|
80c65a48a0 | ||
|
|
4c04ffb7db | ||
|
|
cc3949fa71 | ||
|
|
904a0ecbdd | ||
|
|
9155dd026d | ||
|
|
7286a06985 | ||
|
|
a71552a406 | ||
|
|
5857e8c394 | ||
|
|
429af04714 | ||
|
|
f134987572 | ||
|
|
d8a9043f94 | ||
|
|
3782fa8360 | ||
|
|
023e32f796 | ||
|
|
82f3abdc86 | ||
|
|
1345d31a6d | ||
|
|
c56fa39c15 | ||
|
|
7c8997ae9a | ||
|
|
584343c4e9 | ||
|
|
5754f8eb7c | ||
|
|
15a076026c | ||
|
|
1c90451cb9 | ||
|
|
a4939f2f95 | ||
|
|
b19d2b1894 | ||
|
|
242421a765 | ||
|
|
4cf12b71ac | ||
|
|
b8cf3a6fe9 | ||
|
|
333e163624 | ||
|
|
dfe1518676 | ||
|
|
8097c685e8 | ||
|
|
810754d833 | ||
|
|
a0377cb53a | ||
|
|
6ddcc31a8a | ||
|
|
b61fc06688 | ||
|
|
f29f9e134f | ||
|
|
a2b264708d | ||
|
|
2ae859511f | ||
|
|
b554e7f47c | ||
|
|
1085b63d25 | ||
|
|
ac900ec855 | ||
|
|
8f68e66047 | ||
|
|
faf300db3e | ||
|
|
2fbdd03225 | ||
|
|
9f825f9d5d | ||
|
|
5ff1904148 | ||
|
|
16e80ff80e | ||
|
|
1dcd1845a7 | ||
|
|
955f507023 | ||
|
|
47a3224b27 | ||
|
|
d2bf09d321 | ||
|
|
deee39b367 | ||
|
|
4829433725 | ||
|
|
9efd43e496 | ||
|
|
5bd904ba4f | ||
|
|
3244f0775f | ||
|
|
f4d625a048 | ||
|
|
c838486ca1 | ||
|
|
d4a9ea1a4b | ||
|
|
4123905853 | ||
|
|
720811bf3b | ||
|
|
375fb4aa46 | ||
|
|
aa4b79d13d | ||
|
|
1e9c303646 | ||
|
|
d3ab28f2b3 | ||
|
|
7b67778a29 | ||
|
|
d2c8ef719b | ||
|
|
7bed192300 | ||
|
|
331f65f3a4 | ||
|
|
e17c732b6a | ||
|
|
2e60b2b997 | ||
|
|
70d9b8b99a | ||
|
|
d0cc7e0a68 | ||
|
|
264e7ad098 | ||
|
|
8c33e9bd57 | ||
|
|
a8fd37e4e4 | ||
|
|
17f11ea2de | ||
|
|
f6a582e4f7 | ||
|
|
6e645a567a | ||
|
|
e87dc35372 | ||
|
|
75f4f3eadc | ||
|
|
64e1d722d5 | ||
|
|
05a206384c | ||
|
|
04790e2b57 | ||
|
|
aa3c4a17e9 | ||
|
|
5968ae01eb | ||
|
|
57500b7700 | ||
|
|
4e17a21e5f | ||
|
|
605dc51d50 | ||
|
|
b17d29ba28 | ||
|
|
9afc7fcc62 | ||
|
|
d5543d0c39 | ||
|
|
96fea1f343 | ||
|
|
bc4d9f7148 | ||
|
|
4abacf030b | ||
|
|
279c988464 | ||
|
|
a6449b50e9 | ||
|
|
b4e6449dac | ||
|
|
c3dfadd9e6 | ||
|
|
bad5225024 | ||
|
|
aa9b4927b0 | ||
|
|
69d12910cd | ||
|
|
ee0f7c4529 | ||
|
|
1b296ff83e | ||
|
|
1a635af7b7 | ||
|
|
568530d470 |
+1
-1
@@ -1,8 +1,8 @@
|
||||
*.DS_Store
|
||||
*.swp
|
||||
.gradle
|
||||
.idea
|
||||
build
|
||||
local.properties
|
||||
gradle.properties
|
||||
infer-out
|
||||
*.iml
|
||||
|
||||
-18
@@ -1,18 +0,0 @@
|
||||
language: android
|
||||
android:
|
||||
components:
|
||||
- platform-tools
|
||||
- build-tools-26.0.0
|
||||
- android-26
|
||||
- extra-android-m2repository
|
||||
addons:
|
||||
sonarcloud:
|
||||
organization: "markusfisch-github"
|
||||
token:
|
||||
secure: "mTpn1JNKn/PzEFGTRvkmSfCj0c6nX8n/ef2IO0xPSFyveuM+cpNka5d4/nT1QxEqq1sLUj5HMHEShdk6XlER5Oro+DqekTSw8ONv8P24IZGjiM5noLJZ7uqv/73tM0SevROl3Ldee6AzyS7PzcextQms5UC8uyCgI2jbYwz3hpayaWPquBGRnlw7xGuYwCjJS2cMrhz8sEYRn3Egk3no1YJlxCdbIVaqtVmLweqBzLp176FWB5Svg8N3r+UzaZ3fhQn6LhlHPuObPTrCoVjy6W9TxdUsHfW3F61Zei9h1IEsCC0qsalY0OZC8ML5F2DTDp0spKHnPlXR4Q4hF9XIngk2vPVMtfJhu3JJRojjrxhtfOUZO/VS0AMpueZ5QiL1ZbHziKk9ztuRsB+OIoehWnBTq/r/igrWfqxfsq2VURfAYFldRnobBlT/svNkHBjHfZl9EqKprvwmmvw15ZD0twgsskweDn3GssHC5MGTH6+ujZI19t/SLbDMY2KVPNCrG9sHEXSqKT8wWxqGigUaIbVytD2SqGWep0qsB86FQLAXOmgr3+0upTx3B8YhMG5GGu/R45pJBwzXZbTqSx0goaxP+BBEkzcjQHNl0RvpF44m/FbPMYUBuNyHHEoEZC5G1raOV9jtI8G6whQP3OaT8lO2Aqbp1W/lTSEnCDcKYlU="
|
||||
script:
|
||||
- bash gradlew build
|
||||
- bash gradlew sonarqube
|
||||
branches:
|
||||
only:
|
||||
- master
|
||||
+151
@@ -1,5 +1,156 @@
|
||||
# Change Log
|
||||
|
||||
# 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
|
||||
* Downgrade ZXing version
|
||||
* Improve getting system UI metrics
|
||||
|
||||
# 1.11.1
|
||||
* Fix tap to focus on some devices
|
||||
|
||||
# 1.11.0
|
||||
* Add support for VCard and VEvent barcodes contents
|
||||
* Generated barcodes are now zoomable
|
||||
* Fix creating PDF417 barcodes
|
||||
|
||||
## 1.10.0
|
||||
* Add support for special barcode contents (WiFi, SMS, phone, E-mail)
|
||||
* Add Indonesian translation
|
||||
* Update Hungarian translation
|
||||
* Make system bars completely transparent
|
||||
|
||||
## 1.9.1
|
||||
* Pick search engine for content that is not an URL
|
||||
|
||||
## 1.9.0
|
||||
* Try Google when opening content as URL fails
|
||||
* Show generic error message if barcode generation fails
|
||||
|
||||
## 1.8.1
|
||||
* Distribute APK instead of App Bundle
|
||||
|
||||
## 1.8.0
|
||||
* Save binary data into external file
|
||||
* Fix storage of binary data
|
||||
* Remove horizontal orientation line from scanner
|
||||
|
||||
## 1.7.3
|
||||
* Fix crashes on Lineage and remove RenderScript fallback
|
||||
* Improve handling of binary data
|
||||
|
||||
## 1.7.2
|
||||
* Add a fallback for RenderScript
|
||||
* Add italian translation
|
||||
|
||||
## 1.7.1
|
||||
* Fix support for 64 bit architectures
|
||||
|
||||
## 1.7.0
|
||||
* Pick list separator on sharing history listing
|
||||
* Update Hungarian translation
|
||||
|
||||
## 1.6.0
|
||||
* Share history listing as plain text
|
||||
* Add separator line between history items
|
||||
* Add content description for floating action buttons
|
||||
|
||||
## 1.5.1
|
||||
* Fix generating hex dump
|
||||
|
||||
## 1.5.0
|
||||
* Show a hex dump of decoded contents
|
||||
* Fix processing text from share menu
|
||||
* Add French translation
|
||||
|
||||
## 1.4.3
|
||||
* Fix broken history listing
|
||||
|
||||
## 1.4.2
|
||||
* Show barcode type and content length after decoding
|
||||
|
||||
## 1.4.1
|
||||
* Toggle availability of torch mode button according to camera features
|
||||
|
||||
## 1.4.0
|
||||
* Save scanned codes and show a history listing
|
||||
* Share generated barcodes
|
||||
* Add scanning from a shared image
|
||||
* Add menu item to switch between camera front/back camera
|
||||
* Add dutch and hungarian translation
|
||||
|
||||
## 1.3.8
|
||||
* Qualify for F-Droid
|
||||
|
||||
## 1.3.7
|
||||
* Normalize URL schemes
|
||||
|
||||
## 1.3.6
|
||||
* Fix handling of camera scene modes
|
||||
|
||||
## 1.3.5
|
||||
* Add german translation
|
||||
* Fix handling of data intents
|
||||
* Respond to ZXing' SCAN intents
|
||||
|
||||
## 1.3.4
|
||||
* Fix pre-processing for Android 7+
|
||||
* Fix adaptive icons for Android 8+
|
||||
|
||||
## 1.3.3
|
||||
* Improved pre-processing
|
||||
* Added adaptive icon for Android 8+
|
||||
* Decrease default magnification to 10%
|
||||
|
||||
## 1.3.2
|
||||
* Fixed crash when going back very fast
|
||||
|
||||
## 1.3.1
|
||||
* Changed primary color
|
||||
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
# Contribution Guidelines
|
||||
|
||||
Please try to keep things in good shape and comply to what's there.
|
||||
|
||||
This project follows Android [best practices][android_best_practices]
|
||||
so please have a look if you've never heard of them.
|
||||
|
||||
The code is formatted according to Android Studio's standard, with the
|
||||
exception of indent being tabs instead of spaces.
|
||||
|
||||
Use the feature branch workflow to add new features and make sure
|
||||
to squash when merging into master:
|
||||
|
||||
$ git merge cool_feature --squash
|
||||
|
||||
Then write a [good commit messages][commit_messages] to keep the history
|
||||
meaningful and useful. One feature, one commit.
|
||||
|
||||
[android_best_practices]: https://developer.android.com/distribute/best-practices/develop/
|
||||
[commit_messages]: https://juffalow.com/other/write-good-git-commit-message
|
||||
@@ -1,5 +1,4 @@
|
||||
PACKAGE = de.markusfisch.android.binaryeye
|
||||
APK = app/build/outputs/apk/app-debug.apk
|
||||
|
||||
all: debug install start
|
||||
|
||||
@@ -7,23 +6,22 @@ debug:
|
||||
./gradlew assembleDebug
|
||||
|
||||
release: lint
|
||||
@./gradlew assembleRelease \
|
||||
-Pandroid.injected.signing.store.file=$(ANDROID_KEYFILE) \
|
||||
-Pandroid.injected.signing.store.password=$(ANDROID_STORE_PASSWORD) \
|
||||
-Pandroid.injected.signing.key.alias=$(ANDROID_KEY_ALIAS) \
|
||||
-Pandroid.injected.signing.key.password=$(ANDROID_KEY_PASSWORD)
|
||||
./gradlew assembleRelease
|
||||
|
||||
bundle: lint
|
||||
./gradlew bundleRelease
|
||||
|
||||
lint:
|
||||
./gradlew lintDebug
|
||||
|
||||
sonarqube:
|
||||
./gradlew sonarqube
|
||||
|
||||
infer: clean
|
||||
infer -- ./gradlew assembleDebug
|
||||
|
||||
test:
|
||||
./gradlew test cAT
|
||||
|
||||
install:
|
||||
adb $(TARGET) install -r $(APK)
|
||||
adb $(TARGET) install -r app/build/outputs/apk/debug/app-debug.apk
|
||||
|
||||
start:
|
||||
adb $(TARGET) shell 'am start -n \
|
||||
|
||||
+2
-1
@@ -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.
|
||||
|
||||
@@ -1,21 +1,33 @@
|
||||
# Binary Eye
|
||||
|
||||
<a href="https://travis-ci.org/markusfisch/BinaryEye"><img src="https://api.travis-ci.org/markusfisch/BinaryEye.svg" alt="Travis-ci"/></a>
|
||||
|
||||
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.
|
||||
|
||||
Unlike many others it works in portrait and landscape orientation, can read
|
||||
inverted codes, uses Material Design and doesn't do anything else but decoding
|
||||
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.
|
||||
|
||||
<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/apps/en-play-badge.png" height="45px"/></a>
|
||||
|
||||
And it's written in [Kotlin][kotlin] because I've wanted to explore that a
|
||||
And it's written in [Kotlin][kotlin] because I wanted to explore that a
|
||||
bit more.
|
||||
|
||||
## Screenshots
|
||||
|
||||
<img
|
||||
src="https://raw.githubusercontent.com/markusfisch/BinaryEye/gh-pages/screencap-scanning.png"
|
||||
alt="Screenshot Scanning" width="160"/>
|
||||
<img
|
||||
src="https://raw.githubusercontent.com/markusfisch/BinaryEye/gh-pages/screencap-decoded.png"
|
||||
alt="Screenshot Result" width="160"/>
|
||||
<img
|
||||
src="https://raw.githubusercontent.com/markusfisch/BinaryEye/gh-pages/screencap-compose-barcode.png"
|
||||
alt="Screenshot Compose Barcode" width="160"/>
|
||||
|
||||
## 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>
|
||||
|
||||
[play]: https://play.google.com/store/search?q=barcode%20scanner&c=apps
|
||||
[zxing]: https://github.com/zxing/zxing
|
||||
[kotlin]: http://kotlinlang.org/
|
||||
|
||||
+41
-15
@@ -1,10 +1,5 @@
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'net.ltgt.errorprone'
|
||||
|
||||
configurations.errorprone {
|
||||
resolutionStrategy.force 'com.google.errorprone:error_prone_core:2.0.19'
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdkVersion sdk_version
|
||||
@@ -14,11 +9,31 @@ android {
|
||||
minSdkVersion 9
|
||||
targetSdkVersion sdk_version
|
||||
|
||||
versionCode 7
|
||||
versionName '1.3.1'
|
||||
versionCode 46
|
||||
versionName '1.18.0'
|
||||
|
||||
// it's recommended to set this value to the lowest API level
|
||||
// able to provide all the functionality
|
||||
renderscriptTargetApi 24
|
||||
renderscriptSupportModeEnabled true
|
||||
|
||||
testInstrumentationRunner 'android.support.test.runner.AndroidJUnitRunner'
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
config {
|
||||
keyAlias System.getenv('ANDROID_KEY_ALIAS')
|
||||
keyPassword System.getenv('ANDROID_KEY_PASSWORD')
|
||||
storePassword System.getenv('ANDROID_STORE_PASSWORD')
|
||||
def filePath = System.getenv('ANDROID_KEYFILE')
|
||||
storeFile filePath ? file(filePath) : null
|
||||
}
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
main.java.srcDirs += 'src/main/kotlin'
|
||||
androidTest.java.srcDirs += 'src/androidTest/kotlin'
|
||||
test.java.srcDirs += 'src/test/kotlin'
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
@@ -27,11 +42,10 @@ android {
|
||||
}
|
||||
|
||||
release {
|
||||
proguardFiles \
|
||||
getDefaultProguardFile('proguard-android.txt'), \
|
||||
'proguard-rules.pro'
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||
minifyEnabled true
|
||||
shrinkResources true
|
||||
signingConfig signingConfigs.config
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,9 +58,21 @@ android {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compile fileTree(dir: 'libs', include: '*')
|
||||
compile "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version"
|
||||
compile "com.android.support:appcompat-v7:$support_version"
|
||||
compile "com.android.support:design:$support_version"
|
||||
compile project(':cameraview')
|
||||
androidTestImplementation ('com.android.support.test:runner:1.0.2') {
|
||||
exclude module: 'support-annotations'
|
||||
}
|
||||
androidTestImplementation ('com.android.support.test:rules:1.0.2') {
|
||||
exclude module: 'support-annotations'
|
||||
}
|
||||
testImplementation 'junit:junit:4.12'
|
||||
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
||||
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.4'
|
||||
}
|
||||
|
||||
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,51 @@
|
||||
package de.markusfisch.android.binaryeye
|
||||
|
||||
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.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
import java.util.regex.Pattern
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class PreprocessorTest {
|
||||
@Test
|
||||
fun processSamples() {
|
||||
val zxing = Zxing()
|
||||
val assets = InstrumentationRegistry.getInstrumentation()
|
||||
.context.assets
|
||||
val pattern = Pattern.compile(
|
||||
"[0-9]+-([0-9]+)x([0-9]+)-([0-9]+)deg.yuv"
|
||||
)
|
||||
val files = assets.list("yuv") ?: return
|
||||
for (file in files) {
|
||||
val m = pattern.matcher(file)
|
||||
if (!m.find() || m.groupCount() < 3) {
|
||||
continue
|
||||
}
|
||||
val frameWidth = m.group(1) ?: return
|
||||
val frameHeight = m.group(2) ?: return
|
||||
val frameOrientation = m.group(3) ?: return
|
||||
val frameData = assets.open("yuv/$file").readBytes()
|
||||
val preprocessor = Preprocessor(
|
||||
InstrumentationRegistry.getTargetContext(),
|
||||
frameWidth.toInt(),
|
||||
frameHeight.toInt(),
|
||||
frameOrientation.toInt()
|
||||
)
|
||||
preprocessor.process(frameData)
|
||||
val result = zxing.decode(
|
||||
frameData,
|
||||
preprocessor.outWidth,
|
||||
preprocessor.outHeight
|
||||
)
|
||||
preprocessor.destroy()
|
||||
|
||||
checkNotNull(result) { "no barcode found in $file" }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M52,-884.362h29v71h-29z"/>
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M52,-884.362h29v71h-29z"/>
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M52,-884.362h29v71h-29z"/>
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M32.002,-24.974h3.127v7.656h-3.127z"/>
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M52,-884.362h29v71h-29z"/>
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M52,-884.362h29v71h-29z"/>
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M52,-884.362h29v71h-29z"/>
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M52,-884.362h29v71h-29z"/>
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M52,-884.362h29v71h-29z"/>
|
||||
<path
|
||||
android:pathData="m54.826,27.893c-6.851,0 -13.701,2.581 -18.924,7.745 -10.445,10.328 -10.413,27.062 0,37.423l36.218,36.036 37.879,0.902 -0.07,-38.363 -36.18,-35.998c-5.207,-5.18 -12.073,-7.745 -18.924,-7.745z"
|
||||
android:strokeAlpha="1"
|
||||
android:strokeLineJoin="miter"
|
||||
android:strokeWidth="8.60725021"
|
||||
android:fillColor="#000000"
|
||||
android:strokeColor="#00000000"
|
||||
android:fillType="nonZero"
|
||||
android:fillAlpha="0.1254902"
|
||||
android:strokeLineCap="butt"/>
|
||||
<path
|
||||
android:pathData="M28.001,54.118a26.525,26.227 0,1 0,53.049 0a26.525,26.227 0,1 0,-53.049 0z"
|
||||
android:strokeAlpha="1"
|
||||
android:strokeLineJoin="miter"
|
||||
android:strokeWidth="4"
|
||||
android:fillColor="#ffffff"
|
||||
android:strokeColor="#00000000"
|
||||
android:fillAlpha="1"
|
||||
android:strokeLineCap="butt"/>
|
||||
<path
|
||||
android:pathData="M39.26,54.988a14.509,14.346 0,1 0,29.019 0a14.509,14.346 0,1 0,-29.019 0z"
|
||||
android:strokeAlpha="1"
|
||||
android:strokeLineJoin="miter"
|
||||
android:strokeWidth="4"
|
||||
android:fillColor="#000000"
|
||||
android:strokeColor="#00000000"
|
||||
android:fillAlpha="1"
|
||||
android:strokeLineCap="butt"/>
|
||||
<path
|
||||
android:pathData="M54.034,44.513a9.535,9.428 0,1 0,19.07 0a9.535,9.428 0,1 0,-19.07 0z"
|
||||
android:strokeAlpha="1"
|
||||
android:strokeLineJoin="miter"
|
||||
android:strokeWidth="10"
|
||||
android:fillColor="#ffffff"
|
||||
android:strokeColor="#00000000"
|
||||
android:fillAlpha="1"
|
||||
android:strokeLineCap="butt"/>
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M52,-884.362h29v71h-29z"/>
|
||||
<path
|
||||
android:pathData="m62.745,24.783c-2.055,1.011 -2.87,3.415 -1.847,5.446 0.588,1.168 1.642,1.916 2.839,2.168 -0.564,0.884 -0.835,1.972 -0.925,3.156 -1.782,-0.45 -3.735,-0.191 -5.25,0.639a0.799,0.808 90,0 0,-0.132 0.172,0.799 0.808,90 0,0 -0.132,0.172 0.799,0.808 90,0 0,-0.132 0.39,0.799 0.808,90 0,0 0.044,0.304 0.799,0.808 90,0 0,0.437 0.433,0.799 0.808,90 0,0 0.131,0.043 0.799,0.808 90,0 0,0.131 0.043,0.799 0.808,90 0,0 0.218,-0 0.799,0.808 90,0 0,0.263 -0.129c1.29,-0.707 2.97,-0.892 4.375,-0.425 0.096,0.882 0.341,1.782 0.695,2.683 -0.456,0.072 -0.936,0.177 -1.357,0.343 -1.207,0.48 -2.245,1.302 -3.022,2.244a0.799,0.808 90,0 0,-0.044 0.129,0.799 0.808,90 0,0 -0.088,0.258 0.799,0.808 90,0 0,0.044 0.304,0.799 0.808,90 0,0 0.393,0.563 0.799,0.808 90,0 0,0.394 0.131,0.799 0.808,90 0,0 0.175,-0.086 0.799,0.808 90,0 0,0.175 -0.086,0.799 0.808,90 0,0 0.219,-0.215c0.922,-1.115 2.409,-1.925 3.897,-2.025 0.494,0.845 1.016,1.575 1.658,2.209 -1.232,1.149 -2.043,2.664 -2.239,4.234a0.799,0.808 90,0 0,-0.001 0.215,0.799 0.808,90 0,0 0.174,0.346 0.799,0.808 90,0 0,0.043 0.086,0.799 0.808,90 0,0 0.219,0.217 0.799,0.808 90,0 0,0.131 0.043,0.799 0.808,90 0,0 0.131,0.043 0.799,0.808 90,0 0,0.218 -0,0.799 0.808,90 0,0 0.218,-0 0.799,0.808 90,0 0,0.132 -0.172,0.799 0.808,90 0,0 0.175,-0.086 0.799,0.808 90,0 0,0.044 -0.129,0.799 0.808,90 0,0 0.088,-0.258 0.799,0.808 90,0 0,0.044 -0.129c0.159,-1.272 0.816,-2.493 1.799,-3.371 1.966,1.394 4.252,1.797 6.122,0.876 1.892,-0.931 2.903,-3.018 2.941,-5.444 1.327,-0.259 2.813,0.014 3.936,0.656a0.799,0.808 90,0 0,0.174 0.129,0.799 0.808,90 0,0 0.131,0.043 0.799,0.808 90,0 0,0.088 -0.043,0.799 0.808,90 0,0 0.218,-0 0.799,0.808 90,0 0,0.087 -0.043,0.799 0.808,90 0,0 0.175,-0.086 0.799,0.808 90,0 0,0.132 -0.172,0.799 0.808,90 0,0 0.132,-0.172 0.799,0.808 90,0 0,0.045 -0.562,0.799 0.808,90 0,0 -0.043,-0.086 0.799,0.808 90,0 0,-0.262 -0.303,0.799 0.808,90 0,0 -0.131,-0.043c-1.386,-0.792 -3.097,-1.066 -4.766,-0.787 -0.125,-0.89 -0.309,-1.783 -0.695,-2.683 0.982,-1.073 2.47,-1.756 3.896,-1.81a0.799,0.808 90,0 0,0.131 0.043,0.799 0.808,90 0,0 0.087,-0.043 0.799,0.808 90,0 0,0.482 -0.345,0.799 0.808,90 0,0 0.044,-0.129 0.799,0.808 90,0 0,0.088 -0.476,0.799 0.808,90 0,0 -0.043,-0.086 0.799,0.808 90,0 0,-0.262 -0.303,0.799 0.808,90 0,0 -0.043,-0.086 0.799,0.808 90,0 0,-0.394 -0.131,0.799 0.808,90 0,0 -0.131,-0.043c-1.227,0.048 -2.518,0.369 -3.633,1.031 -0.388,0.233 -0.762,0.548 -1.095,0.864 -0.513,-0.821 -1.093,-1.559 -1.746,-2.166 0.47,-1.39 1.632,-2.605 2.98,-3.195a0.799,0.808 90,0 0,0.175 -0.086,0.799 0.808,90 0,0 0.132,-0.172 0.799,0.808 90,0 0,0.132 -0.172,0.799 0.808,90 0,0 0.132,-0.39 0.799,0.808 90,0 0,-0.043 -0.086,0.799 0.808,90 0,0 -0.174,-0.346 0.799,0.808 90,0 0,-0.043 -0.086,0.799 0.808,90 0,0 -0.437,-0.217 0.799,0.808 90,0 0,-0.175 0.086,0.799 0.808,90 0,0 -0.131,-0.043 0.799,0.808 90,0 0,-0.087 0.043,0.799 0.808,90 0,0 -0.131,-0.043c-1.584,0.694 -2.97,2.079 -3.681,3.756 -1.012,-0.644 -2.048,-1.087 -3.104,-1.173 0.53,-1.101 0.642,-2.414 0.05,-3.59 -1.023,-2.031 -3.541,-2.795 -5.596,-1.783z"
|
||||
android:fillColor="#ff2a2a"
|
||||
android:strokeColor="#00000000"
|
||||
android:fillAlpha="1"/>
|
||||
<path
|
||||
android:pathData="m66.624,32.489c0.281,-0.138 1.896,2.465 3.62,5.889 1.724,3.424 2.893,6.354 2.613,6.492 -0.281,0.138 -1.94,-2.551 -3.663,-5.975 -1.724,-3.424 -2.85,-6.268 -2.569,-6.406z"
|
||||
android:fillColor="#d40000"
|
||||
android:strokeColor="#00000000"
|
||||
android:fillAlpha="1"/>
|
||||
</vector>
|
||||
@@ -3,18 +3,18 @@
|
||||
package="de.markusfisch.android.binaryeye"
|
||||
android:installLocation="auto">
|
||||
<supports-screens
|
||||
android:anyDensity="true"
|
||||
android:largeScreens="true"
|
||||
android:normalScreens="true"
|
||||
android:resizeable="true"
|
||||
android:smallScreens="true"
|
||||
android:xlargeScreens="true"/>
|
||||
<uses-permission android:name="android.permission.CAMERA"/>
|
||||
<uses-permission android:name="android.permission.VIBRATE"/>
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
||||
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
|
||||
<uses-feature android:name="android.hardware.camera"/>
|
||||
<uses-feature android:name="android.hardware.camera.autofocus"/>
|
||||
<application
|
||||
tools:ignore="UnusedAttribute"
|
||||
android:name=".app.BinaryEyeApp"
|
||||
android:allowBackup="true"
|
||||
android:fullBackupContent="true"
|
||||
android:supportsRtl="true"
|
||||
@@ -33,11 +33,15 @@
|
||||
<activity
|
||||
android:name=".activity.CameraActivity"
|
||||
android:label="@string/scan_code">
|
||||
<intent-filter>
|
||||
<intent-filter android:label="@string/compose_barcode">
|
||||
<action android:name="android.intent.action.SEND"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<data android:mimeType="text/plain"/>
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="com.google.zxing.client.android.SCAN"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
@@ -48,8 +52,31 @@
|
||||
android:scheme="http"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".activity.PickActivity"
|
||||
android:label="@string/pick_code_to_scan">
|
||||
<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/*" android:scheme="file"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".activity.MainActivity"
|
||||
android:label="@string/app_name"/>
|
||||
<provider
|
||||
android:name="android.support.v4.content.FileProvider"
|
||||
android:authorities="${applicationId}.provider"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true">
|
||||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/file_paths"/>
|
||||
</provider>
|
||||
</application>
|
||||
</manifest>
|
||||
|
||||
@@ -0,0 +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 DEFAULT_ACTION: IAction = OpenOrSearchAction
|
||||
|
||||
val REGISTRY: Set<IAction> = setOf(
|
||||
MailAction,
|
||||
OtpauthAction,
|
||||
SmsAction,
|
||||
TelAction,
|
||||
VCardAction,
|
||||
VEventAction,
|
||||
WebAction,
|
||||
WifiAction
|
||||
)
|
||||
|
||||
fun getAction(data: ByteArray): IAction = REGISTRY.find {
|
||||
it.canExecuteOn(data)
|
||||
} ?: DEFAULT_ACTION
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package de.markusfisch.android.binaryeye.actions
|
||||
|
||||
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
|
||||
|
||||
interface IAction {
|
||||
val iconResId: Int
|
||||
val titleResId: Int
|
||||
|
||||
fun canExecuteOn(data: ByteArray): Boolean
|
||||
suspend fun execute(context: Context, data: ByteArray)
|
||||
}
|
||||
|
||||
abstract class IntentAction : IAction {
|
||||
abstract val errorMsg: Int
|
||||
|
||||
final override suspend fun execute(context: Context, data: ByteArray) {
|
||||
val intent =
|
||||
createIntent(context, data) ?: return Toast.makeText(
|
||||
context,
|
||||
errorMsg,
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
execShareIntent(context, intent)
|
||||
}
|
||||
|
||||
abstract suspend fun createIntent(context: Context, data: ByteArray): Intent?
|
||||
}
|
||||
|
||||
abstract class SchemeAction : IAction {
|
||||
abstract val scheme: String
|
||||
open val intentAction: String = Intent.ACTION_VIEW
|
||||
open val buildRegex: Boolean = false
|
||||
|
||||
final override fun canExecuteOn(data: ByteArray): Boolean {
|
||||
val content = String(data)
|
||||
return if (buildRegex) {
|
||||
content.matches("""^$scheme://[\w\W]+$""".toRegex(RegexOption.IGNORE_CASE))
|
||||
} 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))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package de.markusfisch.android.binaryeye.actions.mail
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.actions.IntentAction
|
||||
|
||||
object MailAction : IntentAction() {
|
||||
private val mailRegex =
|
||||
"""^mail(?:to)?:([\w.%+-]+@[A-Za-z\d.-]+\.[A-Za-z]{2,6}(?:[?&](?:subject|body)=[\S\s]*?){0,2})$""".toRegex()
|
||||
|
||||
override val iconResId: Int = R.drawable.ic_action_mail
|
||||
override val titleResId: Int = R.string.mail_send
|
||||
override val errorMsg: Int = R.string.mail_error
|
||||
|
||||
override fun canExecuteOn(data: ByteArray): Boolean {
|
||||
return String(data).matches(mailRegex)
|
||||
}
|
||||
|
||||
override suspend fun createIntent(context: Context, data: ByteArray): Intent? {
|
||||
val mailWithMessage = mailRegex.matchEntire(
|
||||
String(data)
|
||||
)?.groupValues?.get(1) ?: return null
|
||||
return Intent(
|
||||
Intent.ACTION_SENDTO,
|
||||
Uri.parse("mailto:$mailWithMessage")
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
package de.markusfisch.android.binaryeye.actions.search
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.widget.Toast
|
||||
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 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 -> Toast.makeText(
|
||||
context,
|
||||
R.string.cannot_resolve_action,
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package de.markusfisch.android.binaryeye.actions.sms
|
||||
|
||||
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 : 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
|
||||
override val errorMsg: Int = R.string.sms_error
|
||||
|
||||
override fun canExecuteOn(data: ByteArray): Boolean {
|
||||
return String(data).matches(smsRegex)
|
||||
}
|
||||
|
||||
override suspend fun createIntent(context: Context, data: ByteArray): Intent? {
|
||||
val (number: String, message: String) = smsRegex.matchEntire(
|
||||
String(data)
|
||||
)?.let {
|
||||
it.groupValues[1] to it.groupValues[2]
|
||||
} ?: return null
|
||||
return Intent(
|
||||
Intent.ACTION_SENDTO,
|
||||
Uri.parse("smsto:$number")
|
||||
).apply {
|
||||
if (message.isNotEmpty()) putExtra("sms_body", message)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package de.markusfisch.android.binaryeye.actions.tel
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.actions.IntentAction
|
||||
import de.markusfisch.android.binaryeye.app.parseAndNormalizeUri
|
||||
|
||||
object TelAction : IntentAction() {
|
||||
private val telRegex = """^tel:(\+?[0-9]+)$""".toRegex(RegexOption.IGNORE_CASE)
|
||||
|
||||
override val iconResId: Int = R.drawable.ic_action_tel
|
||||
override val titleResId: Int = R.string.tel_dial
|
||||
override val errorMsg: Int = R.string.tel_error
|
||||
|
||||
override fun canExecuteOn(data: ByteArray): Boolean {
|
||||
return String(data).matches(telRegex)
|
||||
}
|
||||
|
||||
override suspend fun createIntent(context: Context, data: ByteArray): Intent {
|
||||
return Intent(Intent.ACTION_DIAL, parseAndNormalizeUri(String(data)))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
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)(?:$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(Locale.US)
|
||||
|
||||
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])
|
||||
}
|
||||
}.groupBy({ it.first.toUpperCase(Locale.US) }) { it.second }
|
||||
}
|
||||
|
||||
data class VTypeProperty(
|
||||
val info: List<String>,
|
||||
val value: String
|
||||
) {
|
||||
val firstTypeOrFirstInfo: String?
|
||||
get() = this.info.find { it.startsWith("TYPE=", true) }?.let {
|
||||
typeRegex.matchEntire(it)?.groupValues?.get(1)
|
||||
} ?: this.info.firstOrNull()
|
||||
|
||||
companion object {
|
||||
private val typeRegex = """^(?:TYPE=)(.+?)[:;]?.*$""".toRegex(RegexOption.IGNORE_CASE)
|
||||
}
|
||||
}
|
||||
+183
@@ -0,0 +1,183 @@
|
||||
package de.markusfisch.android.binaryeye.actions.vtype.vcard
|
||||
|
||||
import android.content.ContentValues
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.provider.ContactsContract
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.actions.IntentAction
|
||||
import de.markusfisch.android.binaryeye.actions.vtype.VTypeParser
|
||||
import java.util.*
|
||||
import kotlin.collections.ArrayList
|
||||
|
||||
object VCardAction : IntentAction() {
|
||||
override val iconResId: Int
|
||||
get() = R.drawable.ic_action_vcard
|
||||
override val titleResId: Int
|
||||
get() = R.string.vcard_add
|
||||
override val errorMsg: Int
|
||||
get() = R.string.vcard_failed
|
||||
|
||||
override fun canExecuteOn(data: ByteArray): Boolean {
|
||||
return VTypeParser.parseVType(String(data)) == "VCARD"
|
||||
}
|
||||
|
||||
override suspend fun createIntent(context: Context, data: ByteArray): Intent? {
|
||||
val info = VTypeParser.parseMap(String(data))
|
||||
|
||||
return Intent(Intent.ACTION_INSERT_OR_EDIT).apply {
|
||||
type = ContactsContract.Contacts.CONTENT_ITEM_TYPE
|
||||
info["FN"]?.singleOrNull()?.also { name ->
|
||||
putExtra(ContactsContract.Intents.Insert.NAME, name.value)
|
||||
} ?: info["N"]?.singleOrNull()?.also { name ->
|
||||
putExtra(ContactsContract.Intents.Insert.NAME, name.value.nameFormat)
|
||||
}
|
||||
info["TEL"]?.filter { it.value.isNotEmpty() }?.forEachIndexed { index, phoneProperty ->
|
||||
val (extraPhoneType, extraPhone) = when (index) {
|
||||
0 -> ContactsContract.Intents.Insert.PHONE_TYPE to
|
||||
ContactsContract.Intents.Insert.PHONE
|
||||
1 -> ContactsContract.Intents.Insert.SECONDARY_PHONE_TYPE to
|
||||
ContactsContract.Intents.Insert.SECONDARY_PHONE
|
||||
2 -> ContactsContract.Intents.Insert.TERTIARY_PHONE_TYPE to
|
||||
ContactsContract.Intents.Insert.TERTIARY_PHONE_TYPE
|
||||
else -> return@forEachIndexed
|
||||
}
|
||||
phoneProperty.firstTypeOrFirstInfo?.also {
|
||||
it.phoneType?.also { phoneType ->
|
||||
putExtra(extraPhoneType, phoneType)
|
||||
} ?: putExtra(extraPhoneType, it)
|
||||
}
|
||||
putExtra(
|
||||
extraPhone,
|
||||
phoneProperty.value.toLowerCase(Locale.US).removePrefix("tel:")
|
||||
)
|
||||
}
|
||||
info["EMAIL"]?.forEachIndexed { index, mailProperty ->
|
||||
val (extraMailType, extraMail) = when (index) {
|
||||
0 -> ContactsContract.Intents.Insert.EMAIL_TYPE to
|
||||
ContactsContract.Intents.Insert.EMAIL
|
||||
1 -> ContactsContract.Intents.Insert.SECONDARY_EMAIL_TYPE to
|
||||
ContactsContract.Intents.Insert.SECONDARY_EMAIL
|
||||
2 -> ContactsContract.Intents.Insert.TERTIARY_EMAIL_TYPE to
|
||||
ContactsContract.Intents.Insert.TERTIARY_EMAIL
|
||||
else -> return@forEachIndexed
|
||||
}
|
||||
mailProperty.firstTypeOrFirstInfo?.also {
|
||||
it.mailType?.also { mailType ->
|
||||
putExtra(extraMailType, mailType)
|
||||
} ?: putExtra(extraMailType, it)
|
||||
}
|
||||
putExtra(extraMail, mailProperty.value)
|
||||
}
|
||||
info["ORG"]?.singleOrNull()?.also { organization ->
|
||||
putExtra(ContactsContract.Intents.Insert.COMPANY, organization.value)
|
||||
}
|
||||
info["TITLE"]?.singleOrNull()?.also { jobTitle ->
|
||||
putExtra(ContactsContract.Intents.Insert.JOB_TITLE, jobTitle.value)
|
||||
}
|
||||
info["ADR"]?.forEach { addr ->
|
||||
addr.firstTypeOrFirstInfo?.also {
|
||||
it.addressType?.also { addressType ->
|
||||
putExtra(ContactsContract.Intents.Insert.POSTAL_TYPE, addressType)
|
||||
} ?: putExtra(ContactsContract.Intents.Insert.POSTAL_TYPE, it)
|
||||
}
|
||||
putExtra(ContactsContract.Intents.Insert.POSTAL, addr.value.locationFormat)
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
|
||||
val urls = info["URL"]?.mapNotNull {
|
||||
it.value.takeIf { url -> url.isNotBlank() }?.let { url ->
|
||||
ContentValues(2).apply {
|
||||
put(
|
||||
ContactsContract.Data.MIMETYPE,
|
||||
ContactsContract.CommonDataKinds.Website.CONTENT_ITEM_TYPE
|
||||
)
|
||||
put(ContactsContract.CommonDataKinds.Website.URL, url)
|
||||
}
|
||||
}
|
||||
}
|
||||
val bDays = info["BDAY"]?.mapNotNull {
|
||||
it.value.takeIf { day -> day.isNotBlank() }?.let { day ->
|
||||
ContentValues(3).apply {
|
||||
put(
|
||||
ContactsContract.Data.MIMETYPE,
|
||||
ContactsContract.CommonDataKinds.Event.CONTENT_ITEM_TYPE
|
||||
)
|
||||
put(
|
||||
ContactsContract.CommonDataKinds.Event.TYPE,
|
||||
ContactsContract.CommonDataKinds.Event.TYPE_BIRTHDAY
|
||||
)
|
||||
put(ContactsContract.CommonDataKinds.Event.START_DATE, day)
|
||||
}
|
||||
}
|
||||
}
|
||||
putParcelableArrayListExtra(
|
||||
ContactsContract.Intents.Insert.DATA,
|
||||
ArrayList(listOfNotNull(urls, bDays).flatten())
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
private val String.mailType: Int?
|
||||
get() = when (this.toUpperCase(Locale.US)) {
|
||||
"HOME" -> ContactsContract.CommonDataKinds.Email.TYPE_HOME
|
||||
"WORK" -> ContactsContract.CommonDataKinds.Email.TYPE_WORK
|
||||
"OTHER" -> ContactsContract.CommonDataKinds.Email.TYPE_OTHER
|
||||
"MOBILE" -> ContactsContract.CommonDataKinds.Email.TYPE_MOBILE
|
||||
else -> null
|
||||
}
|
||||
|
||||
private val String.phoneType: Int?
|
||||
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
|
||||
"FAX_WORK" -> ContactsContract.CommonDataKinds.Phone.TYPE_FAX_WORK
|
||||
"FAX_HOME" -> ContactsContract.CommonDataKinds.Phone.TYPE_FAX_HOME
|
||||
"PAGER" -> ContactsContract.CommonDataKinds.Phone.TYPE_PAGER
|
||||
"OTHER" -> ContactsContract.CommonDataKinds.Phone.TYPE_OTHER
|
||||
"CALLBACK" -> ContactsContract.CommonDataKinds.Phone.TYPE_CALLBACK
|
||||
"CAR" -> ContactsContract.CommonDataKinds.Phone.TYPE_CAR
|
||||
"COMPANY_MAIN" -> ContactsContract.CommonDataKinds.Phone.TYPE_COMPANY_MAIN
|
||||
"ISDN" -> ContactsContract.CommonDataKinds.Phone.TYPE_ISDN
|
||||
"MAIN" -> ContactsContract.CommonDataKinds.Phone.TYPE_MAIN
|
||||
"OTHER_FAX" -> ContactsContract.CommonDataKinds.Phone.TYPE_OTHER_FAX
|
||||
"RADIO" -> ContactsContract.CommonDataKinds.Phone.TYPE_RADIO
|
||||
"TELEX" -> ContactsContract.CommonDataKinds.Phone.TYPE_TELEX
|
||||
"TTY_TDD" -> ContactsContract.CommonDataKinds.Phone.TYPE_TTY_TDD
|
||||
"WORK_MOBILE" -> ContactsContract.CommonDataKinds.Phone.TYPE_WORK_MOBILE
|
||||
"WORK_PAGER" -> ContactsContract.CommonDataKinds.Phone.TYPE_WORK_PAGER
|
||||
"ASSISTANT" -> ContactsContract.CommonDataKinds.Phone.TYPE_ASSISTANT
|
||||
"MMS" -> ContactsContract.CommonDataKinds.Phone.TYPE_MMS
|
||||
else -> null
|
||||
}
|
||||
|
||||
private val String.addressType: Int?
|
||||
get() = when (this.toUpperCase(Locale.US)) {
|
||||
"HOME" -> ContactsContract.CommonDataKinds.StructuredPostal.TYPE_HOME
|
||||
"WORK" -> ContactsContract.CommonDataKinds.StructuredPostal.TYPE_WORK
|
||||
"OTHER" -> ContactsContract.CommonDataKinds.StructuredPostal.TYPE_OTHER
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
package de.markusfisch.android.binaryeye.actions.vtype.vevent
|
||||
|
||||
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.*
|
||||
|
||||
object VEventAction : IntentAction() {
|
||||
override val iconResId: Int
|
||||
get() = R.drawable.ic_action_vevent
|
||||
override val titleResId: Int
|
||||
get() = R.string.vevent_add
|
||||
override val errorMsg: Int
|
||||
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"
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
|
||||
override suspend fun createIntent(context: Context, data: ByteArray): Intent? {
|
||||
val info = VTypeParser.parseMap(String(data))
|
||||
|
||||
return Intent(Intent.ACTION_EDIT).apply {
|
||||
type = "vnd.android.cursor.item/event"
|
||||
info["SUMMARY"]?.singleOrNull()?.also { title ->
|
||||
putExtra(CalendarContract.Events.TITLE, title.value)
|
||||
}
|
||||
info["DESCRIPTION"]?.singleOrNull()?.also { description ->
|
||||
putExtra(CalendarContract.Events.DESCRIPTION, description.value)
|
||||
}
|
||||
info["LOCATION"]?.singleOrNull()?.also { location ->
|
||||
putExtra(CalendarContract.Events.EVENT_LOCATION, location.value)
|
||||
}
|
||||
info["DTSTART"]?.singleOrNull()?.also { eventStart ->
|
||||
dateFormats.simpleFindParse(eventStart.value)?.also {
|
||||
putExtra(CalendarContract.EXTRA_EVENT_BEGIN_TIME, it.time)
|
||||
}
|
||||
}
|
||||
info["DTEND"]?.singleOrNull()?.also { eventEnd ->
|
||||
dateFormats.simpleFindParse(eventEnd.value)?.also {
|
||||
putExtra(CalendarContract.EXTRA_EVENT_END_TIME, it.time)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("SimpleDateFormat") // we definitely don't wan't the local format
|
||||
private val dateFormats = listOf(
|
||||
SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"),
|
||||
SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss"),
|
||||
SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'"),
|
||||
SimpleDateFormat("yyyyMMdd'T'HHmmss"),
|
||||
SimpleDateFormat("yyyy-MM-dd"),
|
||||
SimpleDateFormat("yyyyMMdd")
|
||||
)
|
||||
|
||||
private fun List<SimpleDateFormat>.simpleFindParse(data: String): Date? {
|
||||
for (simpleDataFormat in this) {
|
||||
return simpleDataFormat.simpleParse(data) ?: continue
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun SimpleDateFormat.simpleParse(data: String): Date? = try {
|
||||
this.parse(data)
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
package de.markusfisch.android.binaryeye.actions.wifi
|
||||
|
||||
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 de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.actions.IAction
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.coroutines.withTimeoutOrNull
|
||||
|
||||
object WifiAction : IAction {
|
||||
override val iconResId = R.drawable.ic_action_wifi
|
||||
override val titleResId = R.string.connect_to_wifi
|
||||
|
||||
override fun canExecuteOn(data: ByteArray): Boolean =
|
||||
WifiConfigurationFactory.parse(String(data)) != null
|
||||
|
||||
override suspend fun execute(context: Context, data: ByteArray) = withContext(Dispatchers.IO) {
|
||||
val wifiConfig = WifiConfigurationFactory.parse(
|
||||
String(data)
|
||||
) ?: return@withContext
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun WifiManager.enableWifi(context: Context): Boolean {
|
||||
if (!isWifiEnabled) {
|
||||
if (!setWifiEnabled(true)) {
|
||||
withContext(Dispatchers.Main) {
|
||||
Toast.makeText(
|
||||
context,
|
||||
R.string.wifi_config_failed,
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
}
|
||||
return false
|
||||
}
|
||||
// wait for wifi to really be enabled, takes some time
|
||||
withTimeoutOrNull(10000) {
|
||||
while (!isWifiEnabled) delay(500)
|
||||
} ?: if (!isWifiEnabled) {
|
||||
withContext(Dispatchers.Main) {
|
||||
Toast.makeText(
|
||||
context,
|
||||
R.string.wifi_config_failed,
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
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()
|
||||
}
|
||||
}
|
||||
+212
@@ -0,0 +1,212 @@
|
||||
package de.markusfisch.android.binaryeye.actions.wifi
|
||||
|
||||
import android.net.wifi.WifiConfiguration
|
||||
import android.net.wifi.WifiEnterpriseConfig
|
||||
import android.os.Build
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Normal:
|
||||
* WIFI:S:[network SSID];T:<WPA|WEP|nopass|>;P:[network password];H:<true|false|>;;
|
||||
*
|
||||
* WPA2 enterprise (EAP):
|
||||
* WIFI:S:[network SSID];T:WPA2-EAP;H:<true|false|nopass|>;E:[EAP method];PH2:[Phase 2 method];AI:[anonymous identity];I:[identity];P:[password];;
|
||||
*
|
||||
* EPA methods:
|
||||
* "AKA", "AKA_PRIME",
|
||||
* "NONE", "PEAP",
|
||||
* "PWD", "SIM",
|
||||
* "TLS", "TTLS",
|
||||
* "UNAUTH_TLS"
|
||||
* https://developer.android.com/reference/android/net/wifi/WifiEnterpriseConfig.Eap.html
|
||||
*
|
||||
* Phase 2 methods:
|
||||
* "AKA", "AKA_PRIME",
|
||||
* "GTC", "MSCHAP",
|
||||
* "MSCHAPV2", "NONE",
|
||||
* "PAP", "SIM"
|
||||
* https://developer.android.com/reference/android/net/wifi/WifiEnterpriseConfig.Phase2.html
|
||||
*
|
||||
* The fields can appear in any order. Only "S:" is required.
|
||||
*/
|
||||
object WifiConfigurationFactory {
|
||||
fun parse(input: String): WifiConfiguration? {
|
||||
val inputMap = parseMap(input) ?: return null
|
||||
val parsedData = SimpleDataAccessor.of(inputMap) ?: return null
|
||||
return WifiConfiguration().apply(parsedData)
|
||||
}
|
||||
|
||||
internal fun parseMap(string: String): Map<String, String>? {
|
||||
// normally those codes should have the last semicolon, but many
|
||||
// generators don't add it
|
||||
val wifiRegex = """^WIFI:((?:.+?:(?:[^\\;]|\\.)*;)+);?$""".toRegex()
|
||||
// should be: ^(.+):((?:[^\\;,":]|\\.)*);$ but allows unescaped , "
|
||||
// and : because many qr creator doesn't escape
|
||||
val pairRegex = """(.+?):((?:[^\\;]|\\.)*);""".toRegex()
|
||||
|
||||
return wifiRegex.matchEntire(
|
||||
string
|
||||
)?.groupValues?.get(1)?.let { pairs ->
|
||||
pairRegex.findAll(pairs).map { pair ->
|
||||
pair.groupValues[1].toUpperCase(Locale.US) to pair.groupValues[2]
|
||||
}.toMap()
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
internal val securityType: String
|
||||
get() = inputMap["T"]?.unescaped ?: ""
|
||||
internal val password: String?
|
||||
get() = inputMap["P"]?.quotedUnlessHex?.unescaped
|
||||
internal val hidden: Boolean
|
||||
get() = inputMap["H"]?.unescaped == "true"
|
||||
internal val anonymousIdentity: String
|
||||
get() = inputMap["AI"]?.unescaped ?: ""
|
||||
internal val identity: String
|
||||
get() = inputMap["I"]?.unescaped ?: ""
|
||||
internal val eapMethod: Int?
|
||||
get() = if (inputMap["E"].isNullOrEmpty()) {
|
||||
requireSdk(Build.VERSION_CODES.JELLY_BEAN_MR2) {
|
||||
WifiEnterpriseConfig.Eap.NONE
|
||||
}
|
||||
} else when (inputMap["E"]) {
|
||||
"AKA" -> requireSdk(Build.VERSION_CODES.LOLLIPOP) { WifiEnterpriseConfig.Eap.AKA }
|
||||
"AKA_PRIME" -> requireSdk(Build.VERSION_CODES.M) { WifiEnterpriseConfig.Eap.AKA_PRIME }
|
||||
"NONE" -> requireSdk(Build.VERSION_CODES.JELLY_BEAN_MR2) { WifiEnterpriseConfig.Eap.NONE }
|
||||
"PEAP" -> requireSdk(Build.VERSION_CODES.JELLY_BEAN_MR2) { WifiEnterpriseConfig.Eap.PEAP }
|
||||
"PWD" -> requireSdk(Build.VERSION_CODES.JELLY_BEAN_MR2) { WifiEnterpriseConfig.Eap.PWD }
|
||||
"SIM" -> requireSdk(Build.VERSION_CODES.LOLLIPOP) { WifiEnterpriseConfig.Eap.SIM }
|
||||
"TLS" -> requireSdk(Build.VERSION_CODES.JELLY_BEAN_MR2) { WifiEnterpriseConfig.Eap.TLS }
|
||||
"TTLS" -> requireSdk(Build.VERSION_CODES.JELLY_BEAN_MR2) { WifiEnterpriseConfig.Eap.TTLS }
|
||||
"UNAUTH_TLS" -> requireSdk(Build.VERSION_CODES.N) { WifiEnterpriseConfig.Eap.UNAUTH_TLS }
|
||||
else -> null
|
||||
}
|
||||
internal val phase2Method: Int?
|
||||
get() = if (inputMap["PH2"].isNullOrEmpty()) {
|
||||
requireSdk(Build.VERSION_CODES.JELLY_BEAN_MR2) {
|
||||
WifiEnterpriseConfig.Phase2.NONE
|
||||
}
|
||||
} else when (inputMap["PH2"]) {
|
||||
"AKA" -> requireSdk(Build.VERSION_CODES.O) { WifiEnterpriseConfig.Phase2.AKA }
|
||||
"AKA_PRIME" -> requireSdk(Build.VERSION_CODES.O) { WifiEnterpriseConfig.Phase2.AKA_PRIME }
|
||||
"GTC" -> requireSdk(Build.VERSION_CODES.JELLY_BEAN_MR2) { WifiEnterpriseConfig.Phase2.GTC }
|
||||
"MSCHAP" -> requireSdk(Build.VERSION_CODES.JELLY_BEAN_MR2) { WifiEnterpriseConfig.Phase2.MSCHAP }
|
||||
"MSCHAPV2" -> requireSdk(Build.VERSION_CODES.JELLY_BEAN_MR2) { WifiEnterpriseConfig.Phase2.MSCHAPV2 }
|
||||
"NONE" -> requireSdk(Build.VERSION_CODES.JELLY_BEAN_MR2) { WifiEnterpriseConfig.Phase2.NONE }
|
||||
"PAP" -> requireSdk(Build.VERSION_CODES.JELLY_BEAN_MR2) { WifiEnterpriseConfig.Phase2.PAP }
|
||||
"SIM" -> requireSdk(Build.VERSION_CODES.O) { WifiEnterpriseConfig.Phase2.SIM }
|
||||
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 {
|
||||
inputMap["S"].isNullOrEmpty()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun WifiConfiguration.apply(
|
||||
data: SimpleDataAccessor
|
||||
): WifiConfiguration? {
|
||||
fun WifiConfiguration.applyCommon(
|
||||
data: SimpleDataAccessor
|
||||
): WifiConfiguration? {
|
||||
allowedAuthAlgorithms.clear()
|
||||
allowedGroupCiphers.clear()
|
||||
allowedKeyManagement.clear()
|
||||
allowedPairwiseCiphers.clear()
|
||||
allowedProtocols.clear()
|
||||
|
||||
SSID = data.ssid
|
||||
hiddenSSID = data.hidden
|
||||
return this
|
||||
}
|
||||
|
||||
fun WifiConfiguration.applySecurity(
|
||||
data: SimpleDataAccessor
|
||||
): WifiConfiguration? {
|
||||
when (data.securityType) {
|
||||
"", "nopass" -> allowedKeyManagement.set(WifiConfiguration.KeyMgmt.NONE)
|
||||
"WEP" -> @Suppress("DEPRECATION") /* WEP as insecure */ {
|
||||
data.password?.also {
|
||||
wepKeys[0] = it
|
||||
} ?: return null
|
||||
wepTxKeyIndex = 0
|
||||
allowedAuthAlgorithms.set(WifiConfiguration.AuthAlgorithm.SHARED)
|
||||
allowedKeyManagement.set(WifiConfiguration.KeyMgmt.NONE)
|
||||
allowedGroupCiphers.set(WifiConfiguration.GroupCipher.TKIP)
|
||||
allowedGroupCiphers.set(WifiConfiguration.GroupCipher.CCMP)
|
||||
allowedGroupCiphers.set(WifiConfiguration.GroupCipher.WEP40)
|
||||
allowedGroupCiphers.set(WifiConfiguration.GroupCipher.WEP104)
|
||||
}
|
||||
"WPA" -> {
|
||||
data.password?.also {
|
||||
preSharedKey = it
|
||||
} ?: return null
|
||||
allowedAuthAlgorithms.set(WifiConfiguration.AuthAlgorithm.OPEN)
|
||||
@Suppress("DEPRECATION") // WPA 1 is insecure and has bad performance
|
||||
allowedProtocols.set(WifiConfiguration.Protocol.WPA) // WPA
|
||||
allowedProtocols.set(WifiConfiguration.Protocol.RSN) // WPA2
|
||||
allowedKeyManagement.set(WifiConfiguration.KeyMgmt.WPA_PSK)
|
||||
allowedKeyManagement.set(WifiConfiguration.KeyMgmt.WPA_EAP)
|
||||
@Suppress("DEPRECATION") // TKIP is insecure and has bad performance
|
||||
allowedPairwiseCiphers.set(WifiConfiguration.PairwiseCipher.TKIP)
|
||||
allowedPairwiseCiphers.set(WifiConfiguration.PairwiseCipher.CCMP)
|
||||
allowedGroupCiphers.set(WifiConfiguration.GroupCipher.TKIP)
|
||||
allowedGroupCiphers.set(WifiConfiguration.GroupCipher.CCMP)
|
||||
}
|
||||
"WPA2-EAP" -> requireSdk(Build.VERSION_CODES.JELLY_BEAN_MR2) {
|
||||
data.password?.also {
|
||||
preSharedKey = it
|
||||
} ?: return null
|
||||
allowedAuthAlgorithms.set(WifiConfiguration.AuthAlgorithm.OPEN)
|
||||
allowedProtocols.set(WifiConfiguration.Protocol.RSN) // WPA2
|
||||
allowedKeyManagement.set(WifiConfiguration.KeyMgmt.WPA_EAP)
|
||||
@Suppress("DEPRECATION") // TKIP is insecure and has bad performance
|
||||
allowedPairwiseCiphers.set(WifiConfiguration.PairwiseCipher.TKIP)
|
||||
allowedPairwiseCiphers.set(WifiConfiguration.PairwiseCipher.CCMP)
|
||||
allowedGroupCiphers.set(WifiConfiguration.GroupCipher.TKIP)
|
||||
allowedGroupCiphers.set(WifiConfiguration.GroupCipher.CCMP)
|
||||
|
||||
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
|
||||
} ?: return null // api isn't high enough
|
||||
}
|
||||
return this
|
||||
}
|
||||
return this.applyCommon(data)?.applySecurity(data)
|
||||
}
|
||||
|
||||
private inline fun <T> requireSdk(version: Int, block: () -> T): T? {
|
||||
return if (Build.VERSION.SDK_INT >= version) return block() else null
|
||||
}
|
||||
}
|
||||
@@ -1,78 +1,79 @@
|
||||
package de.markusfisch.android.binaryeye.activity
|
||||
|
||||
import com.google.zxing.Result
|
||||
|
||||
import de.markusfisch.android.cameraview.widget.CameraView
|
||||
|
||||
import de.markusfisch.android.binaryeye.app.initSystemBars
|
||||
import de.markusfisch.android.binaryeye.rs.YuvToGray
|
||||
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.content.SharedPreferences
|
||||
import android.hardware.Camera
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.os.Vibrator
|
||||
import android.preference.PreferenceManager
|
||||
import android.support.v4.app.ActivityCompat
|
||||
import android.support.v4.content.ContextCompat
|
||||
import android.support.v7.app.AppCompatActivity
|
||||
import android.support.v7.widget.Toolbar
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.widget.SeekBar
|
||||
import android.widget.Toast
|
||||
import com.google.zxing.Result
|
||||
import com.google.zxing.ResultMetadataType
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
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.cameraview.widget.CameraView
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class CameraActivity : AppCompatActivity() {
|
||||
companion object {
|
||||
private val REQUEST_CAMERA = 1
|
||||
private val ZOOM_MAX = "zoom_max"
|
||||
private val ZOOM_LEVEL = "zoom_level"
|
||||
}
|
||||
|
||||
private val zxing = Zxing()
|
||||
private val decodingRunnable = Runnable {
|
||||
while (decoding) {
|
||||
val result = decodeFrame()
|
||||
if (result != null) {
|
||||
cameraView.post { found(result) }
|
||||
decoding = false
|
||||
break
|
||||
|
||||
private lateinit var vibrator: Vibrator
|
||||
private lateinit var cameraView: CameraView
|
||||
private lateinit var zoomBar: SeekBar
|
||||
private lateinit var flashFab: View
|
||||
|
||||
private var preprocessor: Preprocessor? = null
|
||||
private var invert = false
|
||||
private var flash = false
|
||||
private var frontFacing = false
|
||||
private var fallbackBuffer: IntArray? = null
|
||||
|
||||
override fun onRequestPermissionsResult(
|
||||
requestCode: Int,
|
||||
permissions: Array<String>,
|
||||
grantResults: IntArray
|
||||
) {
|
||||
when (requestCode) {
|
||||
REQUEST_CAMERA -> if (grantResults.isNotEmpty() &&
|
||||
grantResults[0] != PackageManager.PERMISSION_GRANTED
|
||||
) {
|
||||
Toast.makeText(
|
||||
this,
|
||||
R.string.no_camera_no_fun,
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private lateinit var preferences: SharedPreferences
|
||||
private lateinit var vibrator: Vibrator
|
||||
private lateinit var cameraView: CameraView
|
||||
private lateinit var zoomBar: SeekBar
|
||||
private lateinit var yuvToGray: YuvToGray
|
||||
|
||||
private var decoding = false
|
||||
private var decodingThread: Thread? = null
|
||||
private var frameData: ByteArray? = null
|
||||
private var frameWidth: Int = 0
|
||||
private var frameHeight: Int = 0
|
||||
private var frameOrientation: Int = 0
|
||||
private var odd = false
|
||||
private var flash = false
|
||||
|
||||
override fun onRequestPermissionsResult(
|
||||
requestCode: Int,
|
||||
permissions: Array<String>,
|
||||
grantResults: IntArray) {
|
||||
override fun onActivityResult(
|
||||
requestCode: Int,
|
||||
resultCode: Int,
|
||||
resultData: Intent?
|
||||
) {
|
||||
when (requestCode) {
|
||||
REQUEST_CAMERA -> if (grantResults.size > 0 &&
|
||||
grantResults[0] != PackageManager.PERMISSION_GRANTED) {
|
||||
Toast.makeText(this, R.string.no_camera_no_fun,
|
||||
Toast.LENGTH_SHORT).show()
|
||||
finish()
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -84,44 +85,57 @@ class CameraActivity : AppCompatActivity() {
|
||||
initSystemBars(this)
|
||||
setSupportActionBar(findViewById(R.id.toolbar) as Toolbar)
|
||||
|
||||
preferences = PreferenceManager.getDefaultSharedPreferences(this)
|
||||
vibrator = getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
|
||||
yuvToGray = YuvToGray(this)
|
||||
|
||||
cameraView = findViewById(R.id.camera_view) as CameraView
|
||||
zoomBar = findViewById(R.id.zoom) as SeekBar
|
||||
flashFab = findViewById(R.id.flash)
|
||||
flashFab.setOnClickListener { toggleTorchMode() }
|
||||
|
||||
initCameraView()
|
||||
setTapToFocus()
|
||||
initZoomBar()
|
||||
restoreZoom()
|
||||
initFlashFab(findViewById(R.id.flash))
|
||||
|
||||
if (intent?.action == Intent.ACTION_SEND &&
|
||||
intent.type == "text/plain"
|
||||
) {
|
||||
handleSendText(intent)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
yuvToGray.destroy()
|
||||
preprocessor?.destroy()
|
||||
fallbackBuffer = null
|
||||
saveZoom()
|
||||
}
|
||||
|
||||
override fun onNewIntent(intent: Intent) {
|
||||
super.onNewIntent(intent)
|
||||
handleSendText(intent)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
System.gc()
|
||||
if (hasCameraPermission()) {
|
||||
cameraView.openAsync(CameraView.findCameraId(
|
||||
Camera.CameraInfo.CAMERA_FACING_BACK))
|
||||
startDecoding()
|
||||
if (hasCameraPermission(this, REQUEST_CAMERA)) {
|
||||
openCamera()
|
||||
}
|
||||
}
|
||||
|
||||
private fun openCamera() {
|
||||
cameraView.openAsync(
|
||||
CameraView.findCameraId(
|
||||
if (frontFacing) {
|
||||
Camera.CameraInfo.CAMERA_FACING_FRONT
|
||||
} else {
|
||||
Camera.CameraInfo.CAMERA_FACING_BACK
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
cancelDecoding()
|
||||
closeCamera()
|
||||
}
|
||||
|
||||
private fun closeCamera() {
|
||||
cameraView.close()
|
||||
}
|
||||
|
||||
@@ -148,6 +162,30 @@ class CameraActivity : AppCompatActivity() {
|
||||
startActivity(MainActivity.getEncodeIntent(this))
|
||||
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.preferences -> {
|
||||
startActivity(MainActivity.getPreferencesIntent(this))
|
||||
true
|
||||
}
|
||||
R.id.info -> {
|
||||
openReadme()
|
||||
true
|
||||
@@ -156,76 +194,95 @@ class CameraActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun switchCamera() {
|
||||
closeCamera()
|
||||
frontFacing = frontFacing xor true
|
||||
openCamera()
|
||||
}
|
||||
|
||||
private fun openReadme() {
|
||||
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(
|
||||
"https://github.com/markusfisch/BinaryEye/blob/master/README.md")))
|
||||
startActivity(
|
||||
Intent(
|
||||
Intent.ACTION_VIEW, Uri.parse(
|
||||
"https://github.com/markusfisch/BinaryEye/blob/master/README.md"
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun handleSendText(intent: Intent) {
|
||||
if (!Intent.ACTION_SEND.equals(intent.getAction()) ||
|
||||
!"text/plain".equals(intent.getType())) {
|
||||
return
|
||||
val text = intent.getStringExtra(Intent.EXTRA_TEXT)
|
||||
if (text?.isEmpty() == false) {
|
||||
startActivity(MainActivity.getEncodeIntent(this, text, true))
|
||||
finish()
|
||||
}
|
||||
|
||||
var text = intent.getStringExtra(Intent.EXTRA_TEXT)
|
||||
if (text == null || text.isEmpty()) {
|
||||
return
|
||||
}
|
||||
|
||||
// consume this intent
|
||||
intent.setAction(null)
|
||||
|
||||
startActivity(MainActivity.getEncodeIntent(this, text))
|
||||
}
|
||||
|
||||
private fun hasCameraPermission(): Boolean {
|
||||
val permission = android.Manifest.permission.CAMERA
|
||||
|
||||
if (ContextCompat.checkSelfPermission(this, permission) !=
|
||||
PackageManager.PERMISSION_GRANTED) {
|
||||
ActivityCompat.requestPermissions(this, arrayOf(permission),
|
||||
REQUEST_CAMERA)
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
private fun initCameraView() {
|
||||
cameraView.setUseOrientationListener(true)
|
||||
cameraView.setTapToFocus()
|
||||
cameraView.setOnCameraListener(object : CameraView.OnCameraListener {
|
||||
override fun onConfigureParameters(
|
||||
parameters: Camera.Parameters) {
|
||||
if (parameters.isZoomSupported()) {
|
||||
val max = parameters.getMaxZoom()
|
||||
parameters: Camera.Parameters
|
||||
) {
|
||||
if (parameters.isZoomSupported) {
|
||||
val max = parameters.maxZoom
|
||||
if (zoomBar.max != max) {
|
||||
zoomBar.max = max
|
||||
zoomBar.progress = max / 3
|
||||
zoomBar.progress = max / 10
|
||||
saveZoom()
|
||||
}
|
||||
parameters.setZoom(zoomBar.progress)
|
||||
parameters.zoom = zoomBar.progress
|
||||
} else {
|
||||
zoomBar.visibility = View.GONE
|
||||
}
|
||||
for (mode in parameters.getSupportedSceneModes()) {
|
||||
if (mode.equals(Camera.Parameters.SCENE_MODE_BARCODE)) {
|
||||
parameters.setSceneMode(mode)
|
||||
break
|
||||
val sceneModes = parameters.supportedSceneModes
|
||||
sceneModes?.let {
|
||||
for (mode in sceneModes) {
|
||||
if (mode == Camera.Parameters.SCENE_MODE_BARCODE) {
|
||||
parameters.sceneMode = mode
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
CameraView.setAutoFocus(parameters)
|
||||
updateFlashFab(parameters.flashMode == null)
|
||||
}
|
||||
|
||||
override fun onCameraError() {
|
||||
Toast.makeText(this@CameraActivity, R.string.camera_error,
|
||||
Toast.LENGTH_SHORT).show()
|
||||
Toast.makeText(
|
||||
this@CameraActivity,
|
||||
R.string.camera_error,
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
finish()
|
||||
}
|
||||
|
||||
override fun onCameraStarted(camera: Camera) {
|
||||
frameWidth = cameraView.frameWidth
|
||||
frameHeight = cameraView.frameHeight
|
||||
frameOrientation = cameraView.frameOrientation
|
||||
camera.setPreviewCallback { data, _ -> frameData = data }
|
||||
override fun onCameraReady(camera: Camera) {
|
||||
val frameWidth = cameraView.frameWidth
|
||||
val frameHeight = cameraView.frameHeight
|
||||
val frameOrientation = cameraView.frameOrientation
|
||||
var decoding = true
|
||||
camera.setPreviewCallback { frameData, _ ->
|
||||
if (decoding) {
|
||||
val result = decodeFrame(
|
||||
frameData,
|
||||
frameWidth,
|
||||
frameHeight,
|
||||
frameOrientation
|
||||
)
|
||||
result?.let {
|
||||
cameraView.post {
|
||||
vibrator.vibrate(100)
|
||||
showResult(this@CameraActivity, result)
|
||||
}
|
||||
decoding = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPreviewStarted(camera: Camera) {
|
||||
}
|
||||
|
||||
override fun onCameraStopping(camera: Camera) {
|
||||
@@ -234,29 +291,14 @@ class CameraActivity : AppCompatActivity() {
|
||||
})
|
||||
}
|
||||
|
||||
private fun setTapToFocus() {
|
||||
val runnable = Runnable {
|
||||
cameraView.setFocusArea(null)
|
||||
}
|
||||
cameraView.setOnTouchListener({ v: View, event: MotionEvent ->
|
||||
val camera = cameraView.getCamera()
|
||||
camera?.cancelAutoFocus()
|
||||
cameraView.setFocusArea(cameraView.calculateFocusRect(
|
||||
event.getX(), event.getY(), 100))
|
||||
camera?.autoFocus({ _: Boolean, _: Camera ->
|
||||
cameraView.removeCallbacks(runnable)
|
||||
cameraView.postDelayed(runnable, 3000)
|
||||
})
|
||||
v.performClick()
|
||||
true
|
||||
})
|
||||
}
|
||||
|
||||
private fun initZoomBar() {
|
||||
zoomBar.setOnSeekBarChangeListener(object :
|
||||
SeekBar.OnSeekBarChangeListener {
|
||||
override fun onProgressChanged(seekBar: SeekBar, progress: Int,
|
||||
fromUser: Boolean) {
|
||||
SeekBar.OnSeekBarChangeListener {
|
||||
override fun onProgressChanged(
|
||||
seekBar: SeekBar,
|
||||
progress: Int,
|
||||
fromUser: Boolean
|
||||
) {
|
||||
setZoom(progress)
|
||||
}
|
||||
|
||||
@@ -267,12 +309,12 @@ class CameraActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
private fun setZoom(zoom: Int) {
|
||||
val camera: Camera? = cameraView.getCamera()
|
||||
val camera: Camera? = cameraView.camera
|
||||
camera?.let {
|
||||
try {
|
||||
val params = camera.getParameters()
|
||||
params.setZoom(zoom)
|
||||
camera.setParameters(params)
|
||||
val params = camera.parameters
|
||||
params.zoom = zoom
|
||||
camera.parameters = params
|
||||
} catch (e: RuntimeException) {
|
||||
// ignore; there's nothing we can do
|
||||
}
|
||||
@@ -280,72 +322,164 @@ class CameraActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
private fun saveZoom() {
|
||||
val editor = preferences.edit()
|
||||
val editor = prefs.preferences.edit()
|
||||
editor.putInt(ZOOM_MAX, zoomBar.max)
|
||||
editor.putInt(ZOOM_LEVEL, zoomBar.progress)
|
||||
editor.apply()
|
||||
}
|
||||
|
||||
private fun restoreZoom() {
|
||||
zoomBar.max = preferences.getInt(ZOOM_MAX, zoomBar.max)
|
||||
zoomBar.progress = preferences.getInt(ZOOM_LEVEL, zoomBar.progress)
|
||||
zoomBar.max = prefs.preferences.getInt(ZOOM_MAX, zoomBar.max)
|
||||
zoomBar.progress = prefs.preferences.getInt(
|
||||
ZOOM_LEVEL,
|
||||
zoomBar.progress
|
||||
)
|
||||
}
|
||||
|
||||
private fun initFlashFab(fab: View) {
|
||||
if (!packageManager.hasSystemFeature(
|
||||
PackageManager.FEATURE_CAMERA_FLASH)) {
|
||||
fab.visibility = View.GONE
|
||||
private fun updateFlashFab(available: Boolean) {
|
||||
flashFab.visibility = if (available) {
|
||||
View.GONE
|
||||
} else {
|
||||
fab.setOnClickListener { _ ->
|
||||
toggleTorchMode()
|
||||
}
|
||||
View.VISIBLE
|
||||
}
|
||||
}
|
||||
|
||||
private fun toggleTorchMode() {
|
||||
val camera = cameraView.getCamera()
|
||||
val parameters = camera?.getParameters()
|
||||
parameters?.setFlashMode(if (flash)
|
||||
val camera = cameraView.camera
|
||||
val parameters = camera?.parameters
|
||||
parameters?.flashMode = if (flash) {
|
||||
Camera.Parameters.FLASH_MODE_OFF
|
||||
else
|
||||
Camera.Parameters.FLASH_MODE_TORCH)
|
||||
flash = flash xor true
|
||||
parameters?.let { camera.setParameters(parameters) }
|
||||
}
|
||||
|
||||
private fun startDecoding() {
|
||||
frameData = null
|
||||
decoding = true
|
||||
decodingThread = Thread(decodingRunnable)
|
||||
decodingThread?.start()
|
||||
}
|
||||
|
||||
private fun cancelDecoding() {
|
||||
decoding = false
|
||||
if (decodingThread != null) {
|
||||
var retry = 100
|
||||
while (retry-- > 0) {
|
||||
try {
|
||||
decodingThread?.join()
|
||||
break
|
||||
} catch (e: InterruptedException) {
|
||||
decodingThread?.interrupt()
|
||||
}
|
||||
} else {
|
||||
Camera.Parameters.FLASH_MODE_TORCH
|
||||
}
|
||||
parameters?.let {
|
||||
try {
|
||||
camera.parameters = parameters
|
||||
flash = flash xor true
|
||||
} catch (e: RuntimeException) {
|
||||
// ignore; there's nothing we can do
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun decodeFrame(): Result? {
|
||||
private fun decodeFrame(
|
||||
frameData: ByteArray?,
|
||||
frameWidth: Int,
|
||||
frameHeight: Int,
|
||||
frameOrientation: Int
|
||||
): Result? {
|
||||
frameData ?: return null
|
||||
return zxing.decodeBitmap(yuvToGray.convert(
|
||||
frameData!!, frameWidth, frameHeight, frameOrientation))
|
||||
invert = invert xor true
|
||||
val pp = preprocessor ?: Preprocessor(
|
||||
this,
|
||||
frameWidth,
|
||||
frameHeight,
|
||||
frameOrientation
|
||||
)
|
||||
pp.process(frameData)
|
||||
preprocessor = pp
|
||||
return zxing.decode(
|
||||
frameData,
|
||||
pp.outWidth,
|
||||
pp.outHeight,
|
||||
invert
|
||||
)
|
||||
}
|
||||
|
||||
private fun found(result: Result) {
|
||||
cancelDecoding()
|
||||
vibrator.vibrate(100)
|
||||
|
||||
startActivity(MainActivity.getDecodeIntent(this, result.text,
|
||||
result.getBarcodeFormat()))
|
||||
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"
|
||||
}
|
||||
}
|
||||
|
||||
fun showResult(
|
||||
activity: Activity,
|
||||
result: Result,
|
||||
isResult: Boolean = false
|
||||
) {
|
||||
val rawBytes = getRawBytes(result)
|
||||
if (prefs.useHistory) {
|
||||
GlobalScope.launch {
|
||||
db.insertScan(
|
||||
System.currentTimeMillis(),
|
||||
result.text,
|
||||
rawBytes,
|
||||
result.barcodeFormat.toString()
|
||||
)
|
||||
}
|
||||
}
|
||||
if (isResult) {
|
||||
activity.setResult(Activity.RESULT_OK, getReturnIntent(result))
|
||||
activity.finish()
|
||||
} else {
|
||||
activity.startActivity(
|
||||
MainActivity.getDecodeIntent(
|
||||
activity,
|
||||
result.text,
|
||||
result.barcodeFormat,
|
||||
rawBytes
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
val metadata = result.resultMetadata
|
||||
metadata?.let {
|
||||
val orientation = metadata[ResultMetadataType.UPC_EAN_EXTENSION]
|
||||
orientation?.let {
|
||||
intent.putExtra(
|
||||
"SCAN_RESULT_ORIENTATION",
|
||||
orientation.toString()
|
||||
)
|
||||
}
|
||||
val ecLevel = metadata[ResultMetadataType.ERROR_CORRECTION_LEVEL]
|
||||
ecLevel?.let {
|
||||
intent.putExtra(
|
||||
"SCAN_RESULT_ERROR_CORRECTION_LEVEL",
|
||||
ecLevel.toString()
|
||||
)
|
||||
}
|
||||
val upcEanExtension = metadata[ResultMetadataType.UPC_EAN_EXTENSION]
|
||||
upcEanExtension?.let {
|
||||
intent.putExtra(
|
||||
"SCAN_RESULT_UPC_EAN_EXTENSION",
|
||||
upcEanExtension.toString()
|
||||
)
|
||||
}
|
||||
val segments = metadata[ResultMetadataType.BYTE_SEGMENTS]
|
||||
segments?.let {
|
||||
var i = 0
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
for (seg in segments as Iterable<ByteArray>) {
|
||||
intent.putExtra("SCAN_RESULT_BYTE_SEGMENTS_$i", seg)
|
||||
++i
|
||||
}
|
||||
}
|
||||
}
|
||||
return intent
|
||||
}
|
||||
|
||||
@@ -1,41 +1,22 @@
|
||||
package de.markusfisch.android.binaryeye.activity
|
||||
|
||||
import com.google.zxing.BarcodeFormat
|
||||
|
||||
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.R
|
||||
|
||||
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 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.app.setSystemAndToolBarTransparency
|
||||
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
|
||||
|
||||
class MainActivity : AppCompatActivity() {
|
||||
companion object {
|
||||
private val ENCODE = "encode"
|
||||
private val DECODE = "decode"
|
||||
private val DECODE_FORMAT = "decode_format"
|
||||
|
||||
fun getEncodeIntent(context: Context, text: String? = ""): Intent {
|
||||
val intent = Intent(context, MainActivity::class.java)
|
||||
intent.putExtra(ENCODE, text)
|
||||
return intent
|
||||
}
|
||||
|
||||
fun getDecodeIntent(context: Context, text: String,
|
||||
format: BarcodeFormat): Intent {
|
||||
val intent = Intent(context, MainActivity::class.java)
|
||||
intent.putExtra(DECODE, text)
|
||||
intent.putExtra(DECODE_FORMAT, format)
|
||||
return intent
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSupportNavigateUp(): Boolean {
|
||||
val fm = supportFragmentManager
|
||||
if (fm != null && fm.backStackEntryCount > 0) {
|
||||
@@ -54,21 +35,89 @@ class MainActivity : AppCompatActivity() {
|
||||
setSupportActionBar(findViewById(R.id.toolbar) as Toolbar)
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
|
||||
supportFragmentManager.addOnBackStackChangedListener {
|
||||
setSystemAndToolBarTransparency(this@MainActivity)
|
||||
}
|
||||
|
||||
if (state == null) {
|
||||
val intent = getIntent()
|
||||
var fragment: Fragment
|
||||
if (intent != null && intent.hasExtra(ENCODE)) {
|
||||
fragment = EncodeFragment.newInstance(
|
||||
intent.getStringExtra(ENCODE))
|
||||
} else if (intent != null && intent.hasExtra(DECODE)) {
|
||||
fragment = DecodeFragment.newInstance(
|
||||
intent.getStringExtra(DECODE),
|
||||
intent.getSerializableExtra(
|
||||
DECODE_FORMAT) as BarcodeFormat)
|
||||
} else {
|
||||
fragment = DecodeFragment()
|
||||
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()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
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"
|
||||
|
||||
fun getPreferencesIntent(context: Context): Intent {
|
||||
val intent = Intent(context, MainActivity::class.java)
|
||||
intent.putExtra(PREFERENCES, true)
|
||||
return intent
|
||||
}
|
||||
|
||||
fun getHistoryIntent(context: Context): Intent {
|
||||
val intent = Intent(context, MainActivity::class.java)
|
||||
intent.putExtra(HISTORY, true)
|
||||
return intent
|
||||
}
|
||||
|
||||
fun getEncodeIntent(
|
||||
context: Context,
|
||||
text: String? = "",
|
||||
isExternal: Boolean = false
|
||||
): Intent {
|
||||
val intent = Intent(context, MainActivity::class.java)
|
||||
intent.putExtra(ENCODE, text)
|
||||
if (isExternal) {
|
||||
val flagActivityClearTask =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
|
||||
Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||
} else 0
|
||||
intent.addFlags(
|
||||
Intent.FLAG_ACTIVITY_NO_HISTORY or
|
||||
flagActivityClearTask or
|
||||
Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
)
|
||||
}
|
||||
setFragment(supportFragmentManager, fragment)
|
||||
return intent
|
||||
}
|
||||
|
||||
fun getDecodeIntent(
|
||||
context: Context,
|
||||
text: String,
|
||||
format: BarcodeFormat,
|
||||
raw: ByteArray? = null
|
||||
): 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)
|
||||
}
|
||||
return intent
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
package de.markusfisch.android.binaryeye.activity
|
||||
|
||||
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.provider.MediaStore
|
||||
import android.support.v7.app.AppCompatActivity
|
||||
import android.support.v7.widget.Toolbar
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.widget.Toast
|
||||
import com.google.zxing.Result
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.app.initSystemBars
|
||||
import de.markusfisch.android.binaryeye.app.setSystemAndToolBarTransparency
|
||||
import de.markusfisch.android.binaryeye.graphics.crop
|
||||
import de.markusfisch.android.binaryeye.graphics.downsizeIfBigger
|
||||
import de.markusfisch.android.binaryeye.widget.CropImageView
|
||||
import de.markusfisch.android.binaryeye.zxing.Zxing
|
||||
import java.io.IOException
|
||||
import kotlin.math.min
|
||||
import kotlin.math.max
|
||||
|
||||
class PickActivity : AppCompatActivity() {
|
||||
private val zxing = Zxing()
|
||||
|
||||
private lateinit var cropImageView: CropImageView
|
||||
|
||||
private var returnResult = false
|
||||
|
||||
override fun onCreate(state: Bundle?) {
|
||||
super.onCreate(state)
|
||||
setContentView(R.layout.activity_pick)
|
||||
initSystemBars(this)
|
||||
|
||||
setSupportActionBar(findViewById(R.id.toolbar) as Toolbar)
|
||||
|
||||
supportFragmentManager.addOnBackStackChangedListener {
|
||||
setSystemAndToolBarTransparency(this@PickActivity)
|
||||
}
|
||||
|
||||
returnResult = intent?.action == "com.google.zxing.client.android.SCAN"
|
||||
|
||||
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) {
|
||||
Toast.makeText(
|
||||
applicationContext,
|
||||
R.string.error_no_content,
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
finish()
|
||||
return
|
||||
}
|
||||
|
||||
val scannedRect = Rect()
|
||||
val scanBounds: () -> Result? = {
|
||||
var result: Result? = null
|
||||
crop(
|
||||
bitmap,
|
||||
cropImageView.normalizedRectInBounds,
|
||||
cropImageView.imageRotation
|
||||
)?.also {
|
||||
scannedRect.set(0, 0, it.width, it.height)
|
||||
result = zxing.decodePositiveNegative(it)
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
cropImageView = findViewById(R.id.image) as CropImageView
|
||||
cropImageView.setImageBitmap(bitmap)
|
||||
cropImageView.onScan = {
|
||||
val rect = Rect()
|
||||
scanBounds()?.also {
|
||||
rect.left = Int.MAX_VALUE
|
||||
rect.top = Int.MAX_VALUE
|
||||
for (rp in it.resultPoints) {
|
||||
val x = rp.x.toInt()
|
||||
val y = rp.y.toInt()
|
||||
rect.left = min(rect.left, x)
|
||||
rect.right = max(rect.right, x)
|
||||
rect.top = min(rect.top, y)
|
||||
rect.bottom = max(rect.bottom, y)
|
||||
}
|
||||
// map result points onto view
|
||||
val bounds = cropImageView.getBoundsRect()
|
||||
val fx = bounds.width() / scannedRect.width().toFloat()
|
||||
val fy = bounds.height() / scannedRect.height().toFloat()
|
||||
rect.set(
|
||||
(rect.left * fx).toInt(),
|
||||
(rect.top * fy).toInt(),
|
||||
(rect.right * fx).toInt(),
|
||||
(rect.bottom * fx).toInt()
|
||||
)
|
||||
rect.offset(bounds.left.toInt(), bounds.top.toInt())
|
||||
}
|
||||
rect
|
||||
}
|
||||
|
||||
findViewById(R.id.scan).setOnClickListener {
|
||||
scanImage(scanBounds())
|
||||
}
|
||||
}
|
||||
|
||||
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(uri)
|
||||
}
|
||||
|
||||
private fun loadImageToOpen(intent: Intent): Bitmap? {
|
||||
val uri = intent.data ?: return null
|
||||
return loadImageUri(uri)
|
||||
}
|
||||
|
||||
private fun loadImageUri(uri: Uri): Bitmap? = try {
|
||||
downsizeIfBigger(
|
||||
MediaStore.Images.Media.getBitmap(contentResolver, uri),
|
||||
1024
|
||||
)
|
||||
} catch (e: IOException) {
|
||||
null
|
||||
}
|
||||
|
||||
private fun scanImage(result: Result?) {
|
||||
if (result != null) {
|
||||
showResult(this, result, returnResult)
|
||||
finish()
|
||||
return
|
||||
}
|
||||
Toast.makeText(
|
||||
applicationContext,
|
||||
R.string.no_barcode_found,
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package de.markusfisch.android.binaryeye.adapter
|
||||
|
||||
import android.content.Context
|
||||
import android.database.Cursor
|
||||
import android.view.LayoutInflater
|
||||
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.data.Database
|
||||
|
||||
class ScansAdapter(context: Context, cursor: Cursor) :
|
||||
CursorAdapter(context, cursor, false) {
|
||||
private val timeIndex = cursor.getColumnIndex(Database.SCANS_DATETIME)
|
||||
private val contentIndex = cursor.getColumnIndex(Database.SCANS_CONTENT)
|
||||
private val formatIndex = cursor.getColumnIndex(Database.SCANS_FORMAT)
|
||||
|
||||
override fun newView(
|
||||
context: Context,
|
||||
cursor: Cursor,
|
||||
parent: ViewGroup
|
||||
): View {
|
||||
return LayoutInflater.from(parent.context).inflate(
|
||||
R.layout.row_scan, parent, false
|
||||
)
|
||||
}
|
||||
|
||||
override fun bindView(
|
||||
view: View,
|
||||
context: Context,
|
||||
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
|
||||
}
|
||||
holder.formatView.text = cursor.getString(formatIndex)
|
||||
}
|
||||
|
||||
private fun getViewHolder(view: View): ViewHolder {
|
||||
return view.tag as ViewHolder? ?: ViewHolder(
|
||||
view.findViewById(R.id.time),
|
||||
view.findViewById(R.id.content),
|
||||
view.findViewById(R.id.format)
|
||||
).also {
|
||||
view.tag = it
|
||||
}
|
||||
}
|
||||
|
||||
private data class ViewHolder(
|
||||
val timeView: TextView,
|
||||
val contentView: TextView,
|
||||
val formatView: TextView
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package de.markusfisch.android.binaryeye.app
|
||||
|
||||
import android.app.Application
|
||||
import android.support.v8.renderscript.RenderScript
|
||||
import de.markusfisch.android.binaryeye.preference.Preferences
|
||||
import de.markusfisch.android.binaryeye.repository.DatabaseRepository
|
||||
|
||||
val db = DatabaseRepository()
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package de.markusfisch.android.binaryeye.app
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.support.v4.app.Fragment
|
||||
import android.support.v4.app.FragmentManager
|
||||
import android.support.v7.app.AlertDialog
|
||||
import android.view.View
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
|
||||
fun setFragment(fm: FragmentManager?, fragment: Fragment) {
|
||||
fm?.let { getTransaction(fm, fragment).commit() }
|
||||
}
|
||||
|
||||
fun addFragment(fm: FragmentManager?, fragment: Fragment) {
|
||||
fm?.let { getTransaction(fm, fragment).addToBackStack(null).commit() }
|
||||
}
|
||||
|
||||
@SuppressLint("CommitTransaction")
|
||||
private fun getTransaction(
|
||||
fm: FragmentManager,
|
||||
fragment: Fragment
|
||||
) = fm.beginTransaction().replace(R.id.content_frame, fragment)
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package de.markusfisch.android.binaryeye.app
|
||||
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
|
||||
private val nonPrintable = "[\\x00-\\x08\\x0e-\\x1f]".toRegex()
|
||||
|
||||
fun hasNonPrintableCharacters(s: String) = nonPrintable.containsMatchIn(s)
|
||||
|
||||
fun parseAndNormalizeUri(input: String): Uri = Uri.parse(input).let {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) it.normalizeScheme() else it
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package de.markusfisch.android.binaryeye.app
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.os.Environment
|
||||
import android.support.annotation.MainThread
|
||||
import android.widget.EditText
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun Flow<ByteArray>.writeToFile(name: String): Int {
|
||||
return try {
|
||||
val file = File(
|
||||
Environment.getExternalStoragePublicDirectory(
|
||||
Environment.DIRECTORY_DOWNLOADS
|
||||
),
|
||||
name
|
||||
)
|
||||
if (file.exists()) {
|
||||
return R.string.error_file_exists
|
||||
}
|
||||
collect { bytes ->
|
||||
file.appendBytes(bytes)
|
||||
}
|
||||
R.string.saved_in_downloads
|
||||
} catch (e: IOException) {
|
||||
R.string.error_saving_binary_data
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package de.markusfisch.android.binaryeye.app
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.emitAll
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.scanReduce
|
||||
|
||||
fun Flow<ByteArray>.addDelimiter(delimiter: String) = addDelimiter(
|
||||
delimiter.toByteArray(),
|
||||
ByteArray::plus
|
||||
)
|
||||
|
||||
inline fun <T, R> Flow<T>.addDelimiter(
|
||||
delimiter: R,
|
||||
crossinline append: R.(T) -> T
|
||||
): Flow<T> = scanReduce { _, content ->
|
||||
delimiter.append(content)
|
||||
}
|
||||
|
||||
infix fun <T> T.asFlowWith(other: Flow<T>) = flow<T> {
|
||||
emit(this@asFlowWith)
|
||||
emitAll(other)
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
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
|
||||
|
||||
fun hasCameraPermission(activity: Activity, requestCode: Int): Boolean {
|
||||
return hasPermission(activity, Manifest.permission.CAMERA, requestCode)
|
||||
}
|
||||
|
||||
fun hasWritePermission(activity: Activity, requestCode: Int = 1): Boolean {
|
||||
return hasPermission(
|
||||
activity,
|
||||
Manifest.permission.WRITE_EXTERNAL_STORAGE,
|
||||
requestCode
|
||||
)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package de.markusfisch.android.binaryeye.app
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.support.v4.content.FileProvider
|
||||
import android.widget.Toast
|
||||
import de.markusfisch.android.binaryeye.BuildConfig
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import java.io.File
|
||||
|
||||
fun shareText(context: Context, text: String, type: String = "text/plain") {
|
||||
val intent = Intent(Intent.ACTION_SEND)
|
||||
intent.putExtra(Intent.EXTRA_TEXT, text)
|
||||
intent.type = type
|
||||
execShareIntent(context, intent)
|
||||
}
|
||||
|
||||
fun shareUri(context: Context, uri: Uri, type: String) {
|
||||
val intent = Intent(Intent.ACTION_SEND)
|
||||
intent.putExtra(Intent.EXTRA_STREAM, uri)
|
||||
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
intent.type = type
|
||||
execShareIntent(context, intent)
|
||||
}
|
||||
|
||||
fun execShareIntent(context: Context, intent: Intent) {
|
||||
if (intent.resolveActivity(context.packageManager) != null) {
|
||||
context.startActivity(intent)
|
||||
} else {
|
||||
Toast.makeText(
|
||||
context,
|
||||
R.string.cannot_resolve_action,
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
}
|
||||
|
||||
fun shareFile(context: Context, file: File, type: String) {
|
||||
shareUri(context, getUriForFile(context, file), type)
|
||||
}
|
||||
|
||||
fun getUriForFile(context: Context, file: File): Uri {
|
||||
return if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
|
||||
Uri.fromFile(file)
|
||||
} else {
|
||||
FileProvider.getUriForFile(
|
||||
context,
|
||||
BuildConfig.APPLICATION_ID + ".provider",
|
||||
file
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package de.markusfisch.android.binaryeye.app
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Rect
|
||||
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.widget.AbsListView
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
|
||||
val windowInsets = Rect()
|
||||
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 && view.getChildAt(0).top < 0)
|
||||
val scrollable = if (scrolled) true else totalItemCount > 0 &&
|
||||
view.getChildAt(view.lastVisiblePosition).bottom > view.height
|
||||
setSystemAndToolBarTransparency(view.context, scrolled, scrollable)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onScrollStateChanged(
|
||||
view: AbsListView,
|
||||
scrollState: Int
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
fun setSystemAndToolBarTransparency(
|
||||
context: Context,
|
||||
scrolled: Boolean = false,
|
||||
scrollable: Boolean = false
|
||||
) {
|
||||
val opaqueColor = ContextCompat.getColor(context, R.color.primary)
|
||||
val transparentColor = 0x00000000
|
||||
val topColor = if (scrolled) opaqueColor else transparentColor
|
||||
val bottomColor = if (scrolled || scrollable) opaqueColor else transparentColor
|
||||
val activity = context as AppCompatActivity
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
val window = activity.window
|
||||
window.statusBarColor = topColor
|
||||
window.navigationBarColor = bottomColor
|
||||
}
|
||||
activity.supportActionBar?.setBackgroundDrawable(ColorDrawable(topColor))
|
||||
}
|
||||
|
||||
fun initSystemBars(activity: AppCompatActivity) {
|
||||
activity.findViewById(R.id.main_layout)?.also { view ->
|
||||
setWindowInsetListener(view)
|
||||
}
|
||||
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
|
||||
}
|
||||
setSystemAndToolBarTransparency(activity)
|
||||
}
|
||||
|
||||
private fun setWindowInsetListener(view: View) {
|
||||
ViewCompat.setOnApplyWindowInsetsListener(view) { _, insets ->
|
||||
if (insets.hasSystemWindowInsets()) {
|
||||
val left = insets.systemWindowInsetLeft
|
||||
val top = insets.systemWindowInsetTop
|
||||
val right = insets.systemWindowInsetRight
|
||||
val bottom = insets.systemWindowInsetBottom
|
||||
view.setPadding(left, top, right, bottom)
|
||||
windowInsets.set(left, top, right, bottom)
|
||||
}
|
||||
insets.consumeSystemWindowInsets()
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
package de.markusfisch.android.binaryeye.app
|
||||
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
|
||||
import android.support.v4.app.Fragment
|
||||
import android.support.v4.app.FragmentManager
|
||||
import android.support.v4.app.FragmentTransaction
|
||||
|
||||
fun setFragment(fm: FragmentManager?, fragment: Fragment) {
|
||||
fm?.let { getTransaction(fm, fragment).commit() }
|
||||
}
|
||||
|
||||
fun addFragment(fm: FragmentManager?, fragment: Fragment) {
|
||||
fm?.let { getTransaction(fm, fragment).addToBackStack(null).commit() }
|
||||
}
|
||||
|
||||
private fun getTransaction(
|
||||
fm: FragmentManager,
|
||||
fragment: Fragment): FragmentTransaction {
|
||||
return fm.beginTransaction().replace(R.id.content_frame, fragment)
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
package de.markusfisch.android.binaryeye.app
|
||||
|
||||
import de.markusfisch.android.binaryeye.view.SystemBarMetrics
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
|
||||
import android.os.Build
|
||||
import android.support.v4.content.ContextCompat
|
||||
import android.support.v7.app.AppCompatActivity
|
||||
import android.view.View
|
||||
import android.view.Window
|
||||
|
||||
fun initSystemBars(activity: AppCompatActivity?) {
|
||||
val view = activity?.findViewById(R.id.main_layout)
|
||||
if (view != null && setSystemBarColor(activity.window,
|
||||
ContextCompat.getColor(activity,
|
||||
R.color.primary_dark_translucent))) {
|
||||
view.setPadding(
|
||||
0,
|
||||
SystemBarMetrics.getStatusBarHeight(
|
||||
activity.resources),
|
||||
0,
|
||||
0)
|
||||
}
|
||||
}
|
||||
|
||||
private 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
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
package de.markusfisch.android.binaryeye.data
|
||||
|
||||
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
|
||||
import de.markusfisch.android.binaryeye.app.hasNonPrintableCharacters
|
||||
|
||||
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 hasBinaryData(): Cursor? = db.rawQuery(
|
||||
"""SELECT
|
||||
1
|
||||
FROM $SCANS
|
||||
WHERE $SCANS_RAW IS NOT NULL
|
||||
LIMIT 1
|
||||
""", null
|
||||
)
|
||||
|
||||
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,80 @@
|
||||
package de.markusfisch.android.binaryeye.data.csv
|
||||
|
||||
import de.markusfisch.android.binaryeye.app.addDelimiter
|
||||
import de.markusfisch.android.binaryeye.app.asFlowWith
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
fun <T> csvBuilder(config: CSVBuilder<T>.() -> Unit): CSVBuilder<T> {
|
||||
return CSVBuilder<T>().apply(config)
|
||||
}
|
||||
|
||||
class CSVBuilder<T> {
|
||||
private val columns = mutableListOf<ColumnBuilder>()
|
||||
|
||||
fun column(config: ColumnBuilder.() -> Unit): ColumnBuilder {
|
||||
return ColumnBuilder().apply(config).also {
|
||||
columns.add(it)
|
||||
}
|
||||
}
|
||||
|
||||
inner class ColumnBuilder {
|
||||
var isBinary = false
|
||||
var name: String? = null
|
||||
internal var gettingBy: ((T) -> ByteArray)? = null
|
||||
|
||||
fun gettingBy(getter: (T) -> ByteArray) {
|
||||
gettingBy = getter
|
||||
}
|
||||
|
||||
fun gettingByString(getter: (T) -> String): Unit = gettingBy {
|
||||
getter(it).toByteArray()
|
||||
}
|
||||
}
|
||||
|
||||
fun buildWith(inputs: Flow<T>, delimiter: String): Flow<ByteArray> = flow {
|
||||
val columnsFlow = columns.asFlow()
|
||||
|
||||
val header = columnsFlow.buildHeader(delimiter)
|
||||
|
||||
val contentLines = inputs.buildContent(columnsFlow, delimiter)
|
||||
|
||||
emitAll(header)
|
||||
emit("\n".toByteArray())
|
||||
emitAll(contentLines)
|
||||
}
|
||||
|
||||
private fun Flow<ColumnBuilder>.buildHeader(delimiter: String) = map {
|
||||
it.name?.escaped(delimiter)?.toByteArray()
|
||||
?: throw IllegalStateException("You need to provide a name for csv column")
|
||||
}.addDelimiter(delimiter)
|
||||
|
||||
private fun Flow<T>.buildContent(
|
||||
columnsFlow: Flow<ColumnBuilder>,
|
||||
delimiter: String
|
||||
): Flow<ByteArray> = map { input ->
|
||||
input.getLine(columnsFlow, delimiter)
|
||||
}.addDelimiter("\n".toByteArray()) { other: Flow<ByteArray> ->
|
||||
// this: ByteArray
|
||||
this asFlowWith other
|
||||
}.flattenConcat()
|
||||
|
||||
private fun T.getLine(columnsFlow: Flow<ColumnBuilder>, delimiter: String): Flow<ByteArray> =
|
||||
columnsFlow.map { column ->
|
||||
column.gettingBy?.run {
|
||||
var result = invoke(this@getLine)
|
||||
if (!column.isBinary) {
|
||||
result = String(result).escaped(delimiter).toByteArray()
|
||||
}
|
||||
return@run result
|
||||
} ?: throw IllegalStateException("You need to provide a getter for the value")
|
||||
}.addDelimiter(delimiter)
|
||||
|
||||
private fun Any.escaped(delimiter: String): String {
|
||||
val any = this.toString()
|
||||
return when {
|
||||
any.run { contains('"') || contains('\n') || contains(delimiter) } ->
|
||||
"\"${any.replace("\"", "\"\"").replace(delimiter, "\"$delimiter")}\""
|
||||
else -> any
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,69 +1,195 @@
|
||||
package de.markusfisch.android.binaryeye.fragment
|
||||
|
||||
import com.google.zxing.BarcodeFormat
|
||||
|
||||
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.os.Environment
|
||||
import android.support.v4.app.Fragment
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.*
|
||||
import android.widget.EditText
|
||||
import android.widget.ImageView
|
||||
import android.widget.Toast
|
||||
import com.google.zxing.BarcodeFormat
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.app.addSuffixIfNotGiven
|
||||
import de.markusfisch.android.binaryeye.app.hasWritePermission
|
||||
import de.markusfisch.android.binaryeye.app.shareFile
|
||||
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
|
||||
|
||||
class BarcodeFragment : Fragment() {
|
||||
private var barcode: Bitmap? = 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 view = inflater.inflate(
|
||||
R.layout.fragment_barcode,
|
||||
container,
|
||||
false
|
||||
)
|
||||
|
||||
val args = arguments ?: return view
|
||||
val content = args.getString(CONTENT) ?: return view
|
||||
val format = args.getSerializable(FORMAT) as BarcodeFormat? ?: return view
|
||||
val size = args.getInt(SIZE)
|
||||
try {
|
||||
barcode = 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(barcode)
|
||||
view.findViewById<View>(R.id.share).setOnClickListener {
|
||||
val bitmap = barcode
|
||||
bitmap?.let {
|
||||
share(bitmap)
|
||||
}
|
||||
}
|
||||
|
||||
return view
|
||||
}
|
||||
|
||||
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.save -> {
|
||||
askForFileNameAndSave()
|
||||
true
|
||||
}
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
}
|
||||
|
||||
// dialogs do not have a parent view
|
||||
@SuppressLint("InflateParams")
|
||||
private fun askForFileNameAndSave() {
|
||||
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) { _, _ ->
|
||||
val bitmap = barcode
|
||||
bitmap?.let {
|
||||
saveAsFile(
|
||||
bitmap,
|
||||
addSuffixIfNotGiven(
|
||||
editText.text.toString(),
|
||||
".png"
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||
}
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun saveAsFile(bitmap: Bitmap, path: String) {
|
||||
val ac = activity ?: return
|
||||
if (!hasWritePermission(ac)) {
|
||||
return
|
||||
}
|
||||
GlobalScope.launch {
|
||||
val success = saveBitmap(
|
||||
bitmap,
|
||||
File(
|
||||
Environment.getExternalStoragePublicDirectory(
|
||||
Environment.DIRECTORY_DOWNLOADS
|
||||
),
|
||||
path
|
||||
)
|
||||
)
|
||||
GlobalScope.launch(Main) {
|
||||
Toast.makeText(
|
||||
ac,
|
||||
if (success) {
|
||||
R.string.saved_in_downloads
|
||||
} else {
|
||||
R.string.error_saving_binary_data
|
||||
},
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun share(bitmap: Bitmap) {
|
||||
GlobalScope.launch {
|
||||
val file = File(
|
||||
context.externalCacheDir,
|
||||
"shared_barcode.png"
|
||||
)
|
||||
val success = saveBitmap(bitmap, file)
|
||||
GlobalScope.launch(Main) {
|
||||
if (success) {
|
||||
shareFile(context, file, "image/png")
|
||||
} else {
|
||||
val ac = activity
|
||||
ac?.let {
|
||||
Toast.makeText(
|
||||
ac,
|
||||
R.string.error_saving_binary_data,
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val CONTENT = "content"
|
||||
private val FORMAT = "format"
|
||||
private val SIZE = "size"
|
||||
private const val CONTENT = "content"
|
||||
private const val FORMAT = "format"
|
||||
private const val SIZE = "size"
|
||||
|
||||
fun newInstance(
|
||||
content: String,
|
||||
format: BarcodeFormat,
|
||||
size: Int): Fragment {
|
||||
content: String,
|
||||
format: BarcodeFormat,
|
||||
size: Int
|
||||
): Fragment {
|
||||
val args = Bundle()
|
||||
args.putString(CONTENT, content)
|
||||
args.putSerializable(FORMAT, format)
|
||||
args.putInt(SIZE, size)
|
||||
val fragment = BarcodeFragment()
|
||||
fragment.setArguments(args)
|
||||
fragment.arguments = args
|
||||
return fragment
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
state: Bundle?): View? {
|
||||
activity.setTitle(R.string.view_barcode)
|
||||
|
||||
val view = inflater.inflate(
|
||||
R.layout.fragment_barcode,
|
||||
container,
|
||||
false)
|
||||
|
||||
val args = getArguments()
|
||||
args?.let {
|
||||
val size = args.getInt(SIZE)
|
||||
val bitmap: Bitmap?
|
||||
try {
|
||||
bitmap = Zxing.encodeAsBitmap(
|
||||
args.getString(CONTENT),
|
||||
args.getSerializable(FORMAT) as BarcodeFormat,
|
||||
size,
|
||||
size)
|
||||
} catch (e: Exception) {
|
||||
Toast.makeText(activity, e.message,
|
||||
Toast.LENGTH_SHORT).show()
|
||||
fragmentManager.popBackStack()
|
||||
return null
|
||||
}
|
||||
view.findViewById<ImageView>(R.id.barcode).setImageBitmap(bitmap)
|
||||
}
|
||||
|
||||
return view
|
||||
private fun saveBitmap(bitmap: Bitmap, file: File): Boolean {
|
||||
return try {
|
||||
val fos = FileOutputStream(file)
|
||||
bitmap.compress(Bitmap.CompressFormat.PNG, 90, fos)
|
||||
fos.close()
|
||||
true
|
||||
} catch (e: IOException) {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,42 +1,41 @@
|
||||
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.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.support.design.widget.FloatingActionButton
|
||||
import android.support.v4.app.Fragment
|
||||
import android.text.ClipboardManager
|
||||
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.text.Editable
|
||||
import android.text.TextWatcher
|
||||
import android.view.*
|
||||
import android.widget.EditText
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import com.google.zxing.BarcodeFormat
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.actions.ActionRegistry
|
||||
import de.markusfisch.android.binaryeye.app.*
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class DecodeFragment : Fragment() {
|
||||
companion object {
|
||||
private val CONTENT = "content"
|
||||
private val FORMAT = "format"
|
||||
|
||||
fun newInstance(content: String, format: BarcodeFormat): Fragment {
|
||||
val args = Bundle()
|
||||
args.putString(CONTENT, content)
|
||||
args.putSerializable(FORMAT, format)
|
||||
val fragment = DecodeFragment()
|
||||
fragment.arguments = args
|
||||
return fragment
|
||||
}
|
||||
}
|
||||
|
||||
private lateinit var contentView: EditText
|
||||
private lateinit var formatView: TextView
|
||||
private lateinit var hexView: TextView
|
||||
private lateinit var format: BarcodeFormat
|
||||
private lateinit var fab: FloatingActionButton
|
||||
|
||||
private var action = ActionRegistry.DEFAULT_ACTION
|
||||
private var isBinary = false
|
||||
private val content: String
|
||||
get() = contentView.text.toString()
|
||||
|
||||
private val parentJob = Job()
|
||||
private val scope: CoroutineScope = CoroutineScope(Dispatchers.Main + parentJob)
|
||||
|
||||
override fun onCreate(state: Bundle?) {
|
||||
super.onCreate(state)
|
||||
@@ -44,86 +43,229 @@ class DecodeFragment : Fragment() {
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
state: Bundle?): View {
|
||||
activity.setTitle(R.string.content)
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
state: Bundle?
|
||||
): View {
|
||||
activity?.setTitle(R.string.content)
|
||||
|
||||
val view = inflater.inflate(
|
||||
R.layout.fragment_decode,
|
||||
container,
|
||||
false)
|
||||
R.layout.fragment_decode,
|
||||
container,
|
||||
false
|
||||
)
|
||||
|
||||
val content = arguments?.getString(CONTENT) ?: ""
|
||||
format = arguments?.getSerializable(FORMAT) as BarcodeFormat? ?:
|
||||
BarcodeFormat.QR_CODE
|
||||
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
|
||||
|
||||
contentView = view.findViewById<EditText>(R.id.content)
|
||||
contentView.setText(content)
|
||||
contentView = view.findViewById(R.id.content)
|
||||
fab = view.findViewById(R.id.open)
|
||||
|
||||
view.findViewById<View>(R.id.share).setOnClickListener { _ ->
|
||||
share(getContent())
|
||||
if (!isBinary) {
|
||||
contentView.setText(inputContent)
|
||||
contentView.addTextChangedListener(object : TextWatcher {
|
||||
override fun afterTextChanged(s: Editable?) {
|
||||
updateViewsAndAction(content.toByteArray())
|
||||
}
|
||||
|
||||
override fun beforeTextChanged(
|
||||
s: CharSequence?,
|
||||
start: Int,
|
||||
count: Int,
|
||||
after: Int
|
||||
) {
|
||||
}
|
||||
|
||||
override fun onTextChanged(
|
||||
s: CharSequence?,
|
||||
start: Int,
|
||||
before: Int,
|
||||
count: Int
|
||||
) {
|
||||
}
|
||||
})
|
||||
fab.setOnClickListener {
|
||||
executeAction(content.toByteArray())
|
||||
}
|
||||
if (prefs.openImmediately) {
|
||||
executeAction(content.toByteArray())
|
||||
}
|
||||
} else {
|
||||
contentView.setText(R.string.binary_data)
|
||||
contentView.isEnabled = false
|
||||
fab.setImageResource(R.drawable.ic_action_save)
|
||||
fab.setOnClickListener {
|
||||
askForFileNameAndSave(raw)
|
||||
}
|
||||
}
|
||||
|
||||
formatView = view.findViewById(R.id.format)
|
||||
hexView = view.findViewById(R.id.hex)
|
||||
|
||||
updateViewsAndAction(raw)
|
||||
|
||||
return view
|
||||
}
|
||||
|
||||
private fun updateViewsAndAction(bytes: ByteArray) {
|
||||
val prevAction = action
|
||||
if (!prevAction.canExecuteOn(bytes)) {
|
||||
action = ActionRegistry.getAction(bytes)
|
||||
}
|
||||
formatView.text = resources.getQuantityString(
|
||||
R.plurals.barcode_info,
|
||||
bytes.size,
|
||||
format.toString(),
|
||||
bytes.size
|
||||
)
|
||||
hexView.text = hexDump(bytes, 33)
|
||||
if (prevAction !== action) {
|
||||
fab.setImageResource(action.iconResId)
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
||||
fab.setOnLongClickListener { v ->
|
||||
Toast.makeText(
|
||||
v.context,
|
||||
action.titleResId,
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
true
|
||||
}
|
||||
} else {
|
||||
fab.tooltipText = getString(action.titleResId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
inflater.inflate(R.menu.fragment_decode, menu)
|
||||
if (isBinary) {
|
||||
menu.findItem(R.id.copy_to_clipboard).isVisible = false
|
||||
menu.findItem(R.id.create).isVisible = false
|
||||
}
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
return when (item.itemId) {
|
||||
R.id.copy_to_clipboard -> {
|
||||
copyToClipboard(getContent())
|
||||
copyToClipboard(content)
|
||||
true
|
||||
}
|
||||
R.id.open_url -> {
|
||||
openUrl(getContent())
|
||||
R.id.share -> {
|
||||
context?.also { shareText(it, content) }
|
||||
true
|
||||
}
|
||||
R.id.create -> {
|
||||
addFragment(fragmentManager,
|
||||
EncodeFragment.newInstance(getContent(), format))
|
||||
addFragment(
|
||||
fragmentManager,
|
||||
EncodeFragment.newInstance(content, format)
|
||||
)
|
||||
true
|
||||
}
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getContent(): String {
|
||||
return contentView.getText().toString()
|
||||
}
|
||||
|
||||
private fun copyToClipboard(text: String) {
|
||||
activity ?: return
|
||||
|
||||
val cm = activity.getSystemService(
|
||||
Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||
cm.setText(text)
|
||||
Toast.makeText(activity,
|
||||
R.string.put_into_clipboard,
|
||||
Toast.LENGTH_SHORT).show()
|
||||
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) {
|
||||
if (activity == null || url.isEmpty()) {
|
||||
return
|
||||
}
|
||||
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
|
||||
if (intent.resolveActivity(activity.getPackageManager()) != null) {
|
||||
startActivity(intent)
|
||||
} else {
|
||||
Toast.makeText(activity,
|
||||
R.string.cannot_resolve_action,
|
||||
Toast.LENGTH_SHORT).show()
|
||||
private fun executeAction(content: ByteArray) {
|
||||
if (activity != null && content.isNotEmpty()) scope.launch {
|
||||
action.execute(activity, content)
|
||||
}
|
||||
}
|
||||
|
||||
private fun share(text: String) {
|
||||
val intent = Intent(Intent.ACTION_SEND)
|
||||
intent.putExtra(Intent.EXTRA_TEXT, text)
|
||||
intent.setType("text/plain")
|
||||
startActivity(intent)
|
||||
private fun askForFileNameAndSave(raw: ByteArray) {
|
||||
val ac = activity ?: return
|
||||
if (!hasWritePermission(ac)) return
|
||||
scope.launch(Dispatchers.Main) {
|
||||
val name = ac.askForFileName() ?: return@launch
|
||||
val message = flowOf(raw).writeToFile(name)
|
||||
Toast.makeText(
|
||||
ac,
|
||||
message,
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
parentJob.cancel()
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val CONTENT = "content"
|
||||
private const val FORMAT = "format"
|
||||
private const val RAW = "raw"
|
||||
|
||||
fun newInstance(
|
||||
content: String,
|
||||
format: BarcodeFormat,
|
||||
raw: ByteArray? = null
|
||||
): Fragment {
|
||||
val args = Bundle()
|
||||
args.putString(CONTENT, content)
|
||||
args.putSerializable(FORMAT, format)
|
||||
if (raw != null) {
|
||||
args.putByteArray(RAW, raw)
|
||||
}
|
||||
val fragment = DecodeFragment()
|
||||
fragment.arguments = args
|
||||
return fragment
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun hexDump(bytes: ByteArray, charsPerLine: Int): String {
|
||||
if (charsPerLine < 4 || bytes.isEmpty()) {
|
||||
return ""
|
||||
}
|
||||
val dump = StringBuilder()
|
||||
val hex = StringBuilder()
|
||||
val ascii = StringBuilder()
|
||||
val itemsPerLine = (charsPerLine - 1) / 4
|
||||
val len = bytes.size
|
||||
var i = 0
|
||||
while (true) {
|
||||
val ord = bytes[i]
|
||||
hex.append(String.format("%02X ", ord))
|
||||
ascii.append(if (ord > 31) ord.toChar() else " ")
|
||||
|
||||
++i
|
||||
|
||||
val posInLine = i % itemsPerLine
|
||||
val atEnd = i >= len
|
||||
var atLineEnd = posInLine == 0
|
||||
if (atEnd && !atLineEnd) {
|
||||
for (j in posInLine until itemsPerLine) {
|
||||
hex.append(" ")
|
||||
}
|
||||
atLineEnd = true
|
||||
}
|
||||
if (atLineEnd) {
|
||||
dump.append(hex.toString())
|
||||
dump.append(" ")
|
||||
dump.append(ascii.toString())
|
||||
dump.append("\n")
|
||||
hex.setLength(0)
|
||||
ascii.setLength(0)
|
||||
}
|
||||
if (atEnd) {
|
||||
break
|
||||
}
|
||||
}
|
||||
return dump.toString()
|
||||
}
|
||||
|
||||
@@ -1,101 +1,88 @@
|
||||
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.inputmethod.InputMethodManager
|
||||
import android.widget.*
|
||||
import com.google.zxing.BarcodeFormat
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.app.addFragment
|
||||
|
||||
class EncodeFragment : Fragment() {
|
||||
companion object {
|
||||
private val CONTENT = "content"
|
||||
private val FORMAT = "format"
|
||||
|
||||
fun newInstance(
|
||||
content: String,
|
||||
format: BarcodeFormat = BarcodeFormat.AZTEC): Fragment {
|
||||
val args = Bundle()
|
||||
args.putString(CONTENT, content)
|
||||
args.putSerializable(FORMAT, format)
|
||||
val fragment = EncodeFragment()
|
||||
fragment.setArguments(args)
|
||||
return fragment
|
||||
}
|
||||
}
|
||||
|
||||
private lateinit var formatView: Spinner
|
||||
private lateinit var sizeView: TextView
|
||||
private lateinit var sizeBarView: SeekBar
|
||||
private val writers = arrayListOf(
|
||||
BarcodeFormat.AZTEC,
|
||||
BarcodeFormat.CODABAR,
|
||||
BarcodeFormat.CODE_39,
|
||||
BarcodeFormat.CODE_128,
|
||||
BarcodeFormat.DATA_MATRIX,
|
||||
BarcodeFormat.EAN_8,
|
||||
BarcodeFormat.EAN_13,
|
||||
BarcodeFormat.ITF,
|
||||
BarcodeFormat.PDF_417,
|
||||
BarcodeFormat.QR_CODE,
|
||||
BarcodeFormat.UPC_A
|
||||
BarcodeFormat.AZTEC,
|
||||
BarcodeFormat.CODABAR,
|
||||
BarcodeFormat.CODE_39,
|
||||
BarcodeFormat.CODE_128,
|
||||
BarcodeFormat.DATA_MATRIX,
|
||||
BarcodeFormat.EAN_8,
|
||||
BarcodeFormat.EAN_13,
|
||||
BarcodeFormat.ITF,
|
||||
BarcodeFormat.PDF_417,
|
||||
BarcodeFormat.QR_CODE,
|
||||
BarcodeFormat.UPC_A
|
||||
)
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
state: Bundle?): View {
|
||||
activity.setTitle(R.string.compose_barcode)
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
state: Bundle?
|
||||
): View? {
|
||||
val ac = activity ?: return null
|
||||
ac.setTitle(R.string.compose_barcode)
|
||||
|
||||
val view = inflater.inflate(
|
||||
R.layout.fragment_encode,
|
||||
container,
|
||||
false)
|
||||
R.layout.fragment_encode,
|
||||
container,
|
||||
false
|
||||
)
|
||||
|
||||
formatView = view.findViewById<Spinner>(R.id.format)
|
||||
formatView.setAdapter(ArrayAdapter<String>(
|
||||
activity,
|
||||
android.R.layout.simple_list_item_1,
|
||||
writers.map { it -> it.name }))
|
||||
formatView = view.findViewById(R.id.format)
|
||||
formatView.adapter = ArrayAdapter<String>(
|
||||
ac,
|
||||
android.R.layout.simple_list_item_1,
|
||||
writers.map { it.name }
|
||||
)
|
||||
|
||||
sizeView = view.findViewById<TextView>(R.id.size_display)
|
||||
sizeBarView = view.findViewById<SeekBar>(R.id.size_bar)
|
||||
sizeView = view.findViewById(R.id.size_display)
|
||||
sizeBarView = view.findViewById(R.id.size_bar)
|
||||
initSizeBar()
|
||||
|
||||
val contentView = view.findViewById<EditText>(R.id.content)
|
||||
|
||||
val args = getArguments()
|
||||
val args = arguments
|
||||
args?.let {
|
||||
contentView.setText(args.getString(CONTENT))
|
||||
formatView.setSelection(writers.indexOf(
|
||||
args.getSerializable(FORMAT) as BarcodeFormat?))
|
||||
formatView.setSelection(
|
||||
writers.indexOf(
|
||||
args.getSerializable(FORMAT) as BarcodeFormat?
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
view.findViewById<View>(R.id.encode).setOnClickListener { v ->
|
||||
val format = writers.get(formatView.getSelectedItemPosition())
|
||||
var size = getSize(sizeBarView.getProgress())
|
||||
val content = contentView.getText().toString()
|
||||
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()
|
||||
Toast.makeText(
|
||||
v.context,
|
||||
R.string.error_no_content,
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
} else {
|
||||
hideSoftKeyboard(contentView)
|
||||
addFragment(fragmentManager, BarcodeFragment.newInstance(
|
||||
content,
|
||||
format,
|
||||
size))
|
||||
addFragment(
|
||||
fragmentManager,
|
||||
BarcodeFragment.newInstance(content, format, size)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,34 +90,53 @@ class EncodeFragment : Fragment() {
|
||||
}
|
||||
|
||||
private fun initSizeBar() {
|
||||
updateSize(sizeBarView.getProgress())
|
||||
updateSize(sizeBarView.progress)
|
||||
sizeBarView.setOnSeekBarChangeListener(
|
||||
object : SeekBar.OnSeekBarChangeListener {
|
||||
override fun onProgressChanged(
|
||||
object : SeekBar.OnSeekBarChangeListener {
|
||||
override fun onProgressChanged(
|
||||
seekBar: SeekBar,
|
||||
progressValue: Int,
|
||||
fromUser: Boolean) {
|
||||
updateSize(progressValue)
|
||||
}
|
||||
fromUser: Boolean
|
||||
) {
|
||||
updateSize(progressValue)
|
||||
}
|
||||
|
||||
override fun onStartTrackingTouch(seekBar: SeekBar) {}
|
||||
override fun onStartTrackingTouch(seekBar: SeekBar) {}
|
||||
|
||||
override fun onStopTrackingTouch(seekBar: SeekBar) {}
|
||||
})
|
||||
override fun onStopTrackingTouch(seekBar: SeekBar) {}
|
||||
})
|
||||
}
|
||||
|
||||
private fun updateSize(power: Int) {
|
||||
val size = getSize(power)
|
||||
sizeView.setText("${size}x${size}")
|
||||
sizeView.text = getString(R.string.width_by_height, size, size)
|
||||
}
|
||||
|
||||
private fun getSize(power: Int) = 128 * (power + 1)
|
||||
|
||||
private fun hideSoftKeyboard(view: View) {
|
||||
val im = activity?.getSystemService(
|
||||
Context.INPUT_METHOD_SERVICE) as InputMethodManager?
|
||||
Context.INPUT_METHOD_SERVICE
|
||||
) as InputMethodManager?
|
||||
im?.let {
|
||||
im.hideSoftInputFromWindow(view.getWindowToken(), 0)
|
||||
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
|
||||
): Fragment {
|
||||
val args = Bundle()
|
||||
args.putString(CONTENT, content)
|
||||
args.putSerializable(FORMAT, format)
|
||||
val fragment = EncodeFragment()
|
||||
fragment.arguments = args
|
||||
return fragment
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,284 @@
|
||||
package de.markusfisch.android.binaryeye.fragment
|
||||
|
||||
import android.app.AlertDialog
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.support.annotation.WorkerThread
|
||||
import android.support.v4.app.Fragment
|
||||
import android.support.v7.widget.SwitchCompat
|
||||
import android.view.*
|
||||
import android.widget.ListView
|
||||
import android.widget.Toast
|
||||
import com.google.zxing.BarcodeFormat
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.adapter.ScansAdapter
|
||||
import de.markusfisch.android.binaryeye.app.*
|
||||
import de.markusfisch.android.binaryeye.data.csv.csvBuilder
|
||||
import de.markusfisch.android.binaryeye.repository.DatabaseRepository
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.flow.mapNotNull
|
||||
|
||||
class HistoryFragment : Fragment() {
|
||||
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)
|
||||
|
||||
override fun onCreate(state: Bundle?) {
|
||||
super.onCreate(state)
|
||||
setHasOptionsMenu(true)
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
state: Bundle?
|
||||
): View {
|
||||
activity?.setTitle(R.string.history)
|
||||
|
||||
val view = inflater.inflate(
|
||||
R.layout.fragment_history,
|
||||
container,
|
||||
false
|
||||
)
|
||||
|
||||
val 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)
|
||||
true
|
||||
}
|
||||
listView.setOnScrollListener(systemBarScrollListener)
|
||||
|
||||
fab = view.findViewById(R.id.share)
|
||||
fab.setOnClickListener { v ->
|
||||
pickListSeparatorAndShare(v.context)
|
||||
}
|
||||
|
||||
progressView = view.findViewById(R.id.progress_view)
|
||||
|
||||
return view
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
(listView.adapter as ScansAdapter?)?.changeCursor(null)
|
||||
parentJob.cancel()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
update(context)
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
inflater.inflate(R.menu.fragment_history, menu)
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
return when (item.itemId) {
|
||||
R.id.clear -> {
|
||||
askToRemoveAllScans(context)
|
||||
true
|
||||
}
|
||||
R.id.export_history -> {
|
||||
askToExportToFile(context)
|
||||
true
|
||||
}
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
}
|
||||
|
||||
private fun initHistorySwitch(switchView: SwitchCompat) {
|
||||
switchView.setOnCheckedChangeListener { _, isChecked ->
|
||||
prefs.useHistory = isChecked
|
||||
}
|
||||
if (prefs.useHistory) {
|
||||
switchView.toggle()
|
||||
}
|
||||
}
|
||||
|
||||
private fun update(context: Context) {
|
||||
scope.launch {
|
||||
val cursor = db.getScansCursor()
|
||||
withContext(Dispatchers.Main) {
|
||||
fab.visibility = if (cursor != null && cursor.count > 0) {
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun showScan(id: Long) = db.getScan(id)?.also { scan ->
|
||||
try {
|
||||
addFragment(
|
||||
fragmentManager,
|
||||
DecodeFragment.newInstance(
|
||||
scan.content,
|
||||
BarcodeFormat.valueOf(scan.format),
|
||||
scan.raw
|
||||
)
|
||||
)
|
||||
} catch (e: IllegalArgumentException) {
|
||||
// shouldn't ever happen
|
||||
}
|
||||
}
|
||||
|
||||
private fun askToRemoveScan(context: Context, id: Long) {
|
||||
AlertDialog.Builder(context)
|
||||
.setMessage(R.string.really_remove_scan)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
db.removeScan(id)
|
||||
update(context)
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||
}
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun askToRemoveAllScans(context: Context) {
|
||||
AlertDialog.Builder(context)
|
||||
.setMessage(R.string.really_remove_all_scans)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
db.removeScans()
|
||||
update(context)
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||
}
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun askToExportToFile(context: Context) = scope.launch {
|
||||
progressView.useVisibility {
|
||||
if (!hasWritePermission(activity)) {
|
||||
return@useVisibility
|
||||
}
|
||||
val (getBinaries, scans) = getScans {
|
||||
alertDialog(context) { resume ->
|
||||
setTitle(R.string.csv_allow_binary)
|
||||
setMessage(R.string.csv_allow_binary_message)
|
||||
setPositiveButton(R.string.no) { _, _ ->
|
||||
resume(false)
|
||||
}
|
||||
setNegativeButton(android.R.string.yes) { _, _ ->
|
||||
resume(true)
|
||||
}
|
||||
setNeutralButton(android.R.string.cancel) { _, _ ->
|
||||
resume(null)
|
||||
}
|
||||
}
|
||||
} ?: return@useVisibility
|
||||
val delimiters = context.resources.getStringArray(
|
||||
R.array.csv_delimiters_values
|
||||
)
|
||||
val delimiter = alertDialog<String>(context) { resume ->
|
||||
setTitle(R.string.csv_delimiter)
|
||||
setItems(R.array.csv_delimiters_names) { _, which ->
|
||||
resume(delimiters[which])
|
||||
}
|
||||
} ?: return@useVisibility
|
||||
val name = withContext(Dispatchers.Main) {
|
||||
activity.askForFileName(suffix = "csv")
|
||||
} ?: return@useVisibility
|
||||
val csv = scans.toCSV(delimiter, getBinaries)
|
||||
val toastMessage = csv.writeToFile(name)
|
||||
withContext(Dispatchers.Main) {
|
||||
Toast.makeText(
|
||||
context,
|
||||
toastMessage,
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun Flow<DatabaseRepository.Scan>.toCSV(
|
||||
delimiter: String,
|
||||
allowBinary: Boolean
|
||||
): Flow<ByteArray> {
|
||||
return csvBuilder<DatabaseRepository.Scan> {
|
||||
column {
|
||||
name = "DATE"
|
||||
gettingByString { it.timestamp }
|
||||
}
|
||||
column {
|
||||
name = "TYPE"
|
||||
gettingByString { it.format }
|
||||
}
|
||||
column {
|
||||
name = "CONTENT"
|
||||
gettingByString { it.content }
|
||||
}
|
||||
if (allowBinary) column {
|
||||
isBinary = true
|
||||
name = "BINARY_CONTENT"
|
||||
gettingBy { it.raw ?: ByteArray(0) }
|
||||
}
|
||||
}.buildWith(this, delimiter)
|
||||
}
|
||||
|
||||
private fun pickListSeparatorAndShare(context: Context) {
|
||||
val separators = context.resources.getStringArray(
|
||||
R.array.list_separators_values
|
||||
)
|
||||
AlertDialog.Builder(context)
|
||||
.setTitle(R.string.pick_list_separator)
|
||||
.setItems(R.array.list_separators_names) { _, which ->
|
||||
shareScans(separators[which])
|
||||
}
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun shareScans(separator: String) {
|
||||
scope.launch {
|
||||
progressView.useVisibility {
|
||||
val sb = StringBuilder()
|
||||
getScans { false }?.second?.collect {
|
||||
sb.append(it.content)
|
||||
sb.append(separator)
|
||||
}
|
||||
val text = sb.toString()
|
||||
if (text.isNotEmpty()) withContext(Dispatchers.Main) {
|
||||
shareText(context, text)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private suspend inline fun getScans(
|
||||
crossinline binaryData: suspend () -> Boolean?
|
||||
): Pair<Boolean, Flow<DatabaseRepository.Scan>>? {
|
||||
val getBinaries: Boolean = db.hasBinaryData() && binaryData() ?: return null
|
||||
return getBinaries to db.getScans().mapNotNull {
|
||||
when {
|
||||
it.content.isNotEmpty() -> db.getScan(it.id)
|
||||
getBinaries -> db.getScan(it.id)
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package de.markusfisch.android.binaryeye.fragment
|
||||
|
||||
import android.os.Bundle
|
||||
import android.support.v4.app.Fragment
|
||||
import android.support.v7.widget.SwitchCompat
|
||||
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
|
||||
|
||||
class PreferencesFragment : Fragment() {
|
||||
private lateinit var openImmediatelySwitch: SwitchCompat
|
||||
private lateinit var useHistorySwitch: SwitchCompat
|
||||
private lateinit var openWithUrlInput: EditText
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
state: Bundle?
|
||||
): View? {
|
||||
activity?.setTitle(R.string.preferences)
|
||||
|
||||
val view = inflater.inflate(
|
||||
R.layout.fragment_preferences,
|
||||
container,
|
||||
false
|
||||
)
|
||||
|
||||
openImmediatelySwitch = view.findViewById<SwitchCompat>(R.id.open_immediately)
|
||||
if (prefs.openImmediately) {
|
||||
openImmediatelySwitch.toggle()
|
||||
}
|
||||
|
||||
useHistorySwitch = view.findViewById<SwitchCompat>(R.id.use_history)
|
||||
if (prefs.useHistory) {
|
||||
useHistorySwitch.toggle()
|
||||
}
|
||||
|
||||
openWithUrlInput = view.findViewById<EditText>(R.id.open_with_url)
|
||||
openWithUrlInput.setText(prefs.openWithUrl)
|
||||
|
||||
return view
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
prefs.openImmediately = openImmediatelySwitch.isChecked
|
||||
prefs.useHistory = useHistorySwitch.isChecked
|
||||
prefs.openWithUrl = openWithUrlInput.text.toString()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package de.markusfisch.android.binaryeye.graphics
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Matrix
|
||||
import android.graphics.RectF
|
||||
import kotlin.math.max
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
fun downsizeIfBigger(bitmap: Bitmap, maxSize: Int): Bitmap = if (
|
||||
max(bitmap.width, bitmap.height) > maxSize
|
||||
) {
|
||||
val srcWidth = bitmap.width
|
||||
val srcHeight = bitmap.height
|
||||
var newWidth = maxSize
|
||||
var newHeight = maxSize
|
||||
if (srcWidth > srcHeight) {
|
||||
newHeight = (newWidth.toFloat() / srcWidth.toFloat() *
|
||||
srcHeight.toFloat()).roundToInt()
|
||||
} else {
|
||||
newWidth = (newHeight.toFloat() / srcHeight.toFloat() *
|
||||
srcWidth.toFloat()).roundToInt()
|
||||
}
|
||||
Bitmap.createScaledBitmap(bitmap, newWidth, newHeight, true)
|
||||
} else {
|
||||
bitmap
|
||||
}
|
||||
|
||||
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,54 @@
|
||||
package de.markusfisch.android.binaryeye.preference
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import android.preference.PreferenceManager
|
||||
|
||||
class Preferences {
|
||||
lateinit var preferences: SharedPreferences
|
||||
|
||||
var openImmediately = false
|
||||
set(value) {
|
||||
setBoolean(OPEN_IMMEDIATELY, value)
|
||||
field = value
|
||||
}
|
||||
var useHistory = false
|
||||
set(value) {
|
||||
setBoolean(USE_HISTORY, value)
|
||||
field = value
|
||||
}
|
||||
var openWithUrl: String = ""
|
||||
set(value) {
|
||||
setString(OPEN_WITH_URL, value)
|
||||
field = value
|
||||
}
|
||||
|
||||
fun init(context: Context) {
|
||||
preferences = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
update()
|
||||
}
|
||||
|
||||
fun update() {
|
||||
openImmediately = preferences.getBoolean(OPEN_IMMEDIATELY, openImmediately)
|
||||
useHistory = preferences.getBoolean(USE_HISTORY, useHistory)
|
||||
preferences.getString(OPEN_WITH_URL, openWithUrl)?.also { openWithUrl = it }
|
||||
}
|
||||
|
||||
private fun setBoolean(label: String, value: Boolean) {
|
||||
val editor = preferences.edit()
|
||||
editor.putBoolean(label, value)
|
||||
editor.apply()
|
||||
}
|
||||
|
||||
private fun setString(label: String, value: String) {
|
||||
val editor = preferences.edit()
|
||||
editor.putString(label, value)
|
||||
editor.apply()
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val OPEN_IMMEDIATELY = "open_immediately"
|
||||
const val USE_HISTORY = "use_history"
|
||||
const val OPEN_WITH_URL = "open_with_url"
|
||||
}
|
||||
}
|
||||
+128
@@ -0,0 +1,128 @@
|
||||
package de.markusfisch.android.binaryeye.repository
|
||||
|
||||
import android.content.Context
|
||||
import android.database.Cursor
|
||||
import de.markusfisch.android.binaryeye.data.Database
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
class DatabaseRepository {
|
||||
private val db = Database()
|
||||
|
||||
fun open(applicationContext: Context) {
|
||||
db.open(applicationContext)
|
||||
}
|
||||
|
||||
fun getScan(id: Long): Scan? = getScan(id) { _, cursor ->
|
||||
val timeIndex = cursor.getColumnIndex(Database.SCANS_DATETIME)
|
||||
val contentIndex = cursor.getColumnIndex(Database.SCANS_CONTENT)
|
||||
val rawIndex = cursor.getColumnIndex(Database.SCANS_RAW)
|
||||
val formatIndex = cursor.getColumnIndex(Database.SCANS_FORMAT)
|
||||
Scan(
|
||||
id = id,
|
||||
timestamp = cursor.getString(timeIndex),
|
||||
content = cursor.getString(contentIndex),
|
||||
raw = cursor.getBlob(rawIndex),
|
||||
format = cursor.getString(formatIndex)
|
||||
)
|
||||
}
|
||||
|
||||
fun <T> getScan(id: Long, map: (id: Long, Cursor) -> T): T? =
|
||||
db.getScan(id)?.use { map(id, it.apply { moveToFirst() }) }
|
||||
|
||||
fun getScans(): Flow<SimpleScan> = getScans { cursor ->
|
||||
val idIndex = cursor.getColumnIndex(Database.SCANS_ID)
|
||||
val timeIndex = cursor.getColumnIndex(Database.SCANS_DATETIME)
|
||||
val contentIndex = cursor.getColumnIndex(Database.SCANS_CONTENT)
|
||||
val formatIndex = cursor.getColumnIndex(Database.SCANS_FORMAT)
|
||||
SimpleScan(
|
||||
id = cursor.getLong(idIndex),
|
||||
timestamp = cursor.getString(timeIndex),
|
||||
content = cursor.getString(contentIndex),
|
||||
format = cursor.getString(formatIndex)
|
||||
)
|
||||
}
|
||||
|
||||
fun <T> getScans(map: suspend (Cursor) -> T): Flow<T> = flow {
|
||||
db.getScans()?.use { results ->
|
||||
results.asIterable.forEach {
|
||||
emit(it)
|
||||
}
|
||||
}
|
||||
}.map(map).flowOn(Dispatchers.IO)
|
||||
|
||||
fun getScansCursor(): Cursor? = db.getScans()
|
||||
|
||||
fun hasBinaryData() = db.hasBinaryData()?.use { it.count > 0 } ?: false
|
||||
|
||||
fun insertScan(
|
||||
timestamp: Long,
|
||||
content: String,
|
||||
raw: ByteArray?,
|
||||
format: String
|
||||
): Long = db.insertScan(timestamp, content, raw, format)
|
||||
|
||||
fun removeScan(id: Long) = db.removeScan(id)
|
||||
|
||||
fun removeScans() = db.removeScans()
|
||||
|
||||
data class SimpleScan(
|
||||
val id: Long,
|
||||
val timestamp: String,
|
||||
val content: String,
|
||||
val format: String
|
||||
)
|
||||
|
||||
data class Scan(
|
||||
val id: Long,
|
||||
val timestamp: String,
|
||||
val content: String,
|
||||
val raw: ByteArray?,
|
||||
val format: String
|
||||
) {
|
||||
// Needed 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
|
||||
|
||||
if (id != other.id) return false
|
||||
if (timestamp != other.timestamp) return false
|
||||
if (content != other.content) return false
|
||||
if (raw != null) {
|
||||
if (other.raw == null) return false
|
||||
if (!raw.contentEquals(other.raw)) return false
|
||||
} else if (other.raw != null) return false
|
||||
if (format != other.format) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// 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()
|
||||
return result
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val Cursor.asIterable: Iterable<Cursor>
|
||||
get() = object : Iterable<Cursor> {
|
||||
override fun iterator(): Iterator<Cursor> = object : Iterator<Cursor> {
|
||||
override fun hasNext(): Boolean = position + 1 < count
|
||||
override fun next(): Cursor = if (moveToNext()) {
|
||||
this@asIterable
|
||||
} else {
|
||||
throw IllegalStateException("You can't access more items, then there are")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
package de.markusfisch.android.binaryeye.rs
|
||||
|
||||
import android.content.Context
|
||||
import android.support.v8.renderscript.*
|
||||
import de.markusfisch.android.binaryeye.renderscript.ScriptC_rotator
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
class Preprocessor(
|
||||
context: Context,
|
||||
width: Int,
|
||||
height: Int,
|
||||
private val orientation: Int
|
||||
) {
|
||||
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 var yuvType: Type? = null
|
||||
private var yuvAlloc: Allocation? = null
|
||||
private var resizedType: Type? = null
|
||||
private var resizedAlloc: Allocation? = null
|
||||
private var rotatedType: Type? = null
|
||||
private var rotatedAlloc: Allocation? = null
|
||||
|
||||
init {
|
||||
yuvType = Type.createXY(
|
||||
rs,
|
||||
Element.U8(rs),
|
||||
width,
|
||||
height // use only grayscale part
|
||||
)
|
||||
yuvAlloc = Allocation.createTyped(
|
||||
rs,
|
||||
yuvType,
|
||||
Allocation.USAGE_SCRIPT
|
||||
)
|
||||
|
||||
val f = .75f
|
||||
var w = (width * f).roundToInt()
|
||||
var h = (height * f).roundToInt()
|
||||
|
||||
resizedType = Type.createXY(
|
||||
rs,
|
||||
Element.U8(rs),
|
||||
w,
|
||||
h
|
||||
)
|
||||
resizedAlloc = Allocation.createTyped(
|
||||
rs,
|
||||
resizedType,
|
||||
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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun destroy() {
|
||||
yuvType?.destroy()
|
||||
yuvType = null
|
||||
yuvAlloc?.destroy()
|
||||
yuvAlloc = null
|
||||
resizedType?.destroy()
|
||||
resizedType = null
|
||||
resizedAlloc?.destroy()
|
||||
resizedAlloc = null
|
||||
rotatedType?.destroy()
|
||||
rotatedType = null
|
||||
rotatedAlloc?.destroy()
|
||||
rotatedAlloc = null
|
||||
resizeScript.destroy()
|
||||
rotatorScript.destroy()
|
||||
rs.destroy()
|
||||
}
|
||||
|
||||
fun process(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
|
||||
)
|
||||
}
|
||||
rotatedAlloc?.copyTo(frame)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,104 +0,0 @@
|
||||
package de.markusfisch.android.binaryeye.rs
|
||||
|
||||
import de.markusfisch.android.binaryeye.renderscript.ScriptC_rotator
|
||||
import de.markusfisch.android.binaryeye.renderscript.ScriptC_yuv2gray
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.support.v8.renderscript.Allocation
|
||||
import android.support.v8.renderscript.Element
|
||||
import android.support.v8.renderscript.RenderScript
|
||||
import android.support.v8.renderscript.Type
|
||||
|
||||
class YuvToGray(context: Context) {
|
||||
private val rs: RenderScript = RenderScript.create(context)
|
||||
private val rotatorScript: ScriptC_rotator = ScriptC_rotator(rs)
|
||||
private val yuv2grayScript: ScriptC_yuv2gray = ScriptC_yuv2gray(rs)
|
||||
|
||||
private var yuvType: Type? = null
|
||||
private var rgbaType: Type? = null
|
||||
private var yuvAlloc: Allocation? = null
|
||||
private var rgbaAlloc: Allocation? = null
|
||||
private var destAlloc: Allocation? = null
|
||||
private var dest: Bitmap? = null
|
||||
private var odd = true
|
||||
|
||||
fun destroy() {
|
||||
yuvType?.destroy()
|
||||
yuvType = null
|
||||
rgbaType?.destroy()
|
||||
rgbaType = null
|
||||
yuvAlloc?.destroy()
|
||||
yuvAlloc = null
|
||||
rgbaAlloc?.destroy()
|
||||
rgbaAlloc = null
|
||||
destAlloc?.destroy()
|
||||
destAlloc = null
|
||||
dest?.recycle()
|
||||
dest = null
|
||||
rotatorScript.destroy()
|
||||
yuv2grayScript.destroy()
|
||||
rs.destroy()
|
||||
}
|
||||
|
||||
fun convert(
|
||||
data: ByteArray,
|
||||
width: Int,
|
||||
height: Int,
|
||||
orientation: Int): Bitmap {
|
||||
if (dest == null) {
|
||||
yuvType = Type.createXY(rs, Element.U8(rs), width, height * 3 / 2)
|
||||
yuvAlloc = Allocation.createTyped(rs, yuvType,
|
||||
Allocation.USAGE_SCRIPT)
|
||||
rgbaType = Type.createXY(rs, Element.RGBA_8888(rs), width, height)
|
||||
rgbaAlloc = Allocation.createTyped(rs, rgbaType,
|
||||
Allocation.USAGE_SCRIPT)
|
||||
|
||||
var w = width
|
||||
var h = height
|
||||
when (orientation) {
|
||||
90, 270 -> {
|
||||
val tmp = w
|
||||
w = h
|
||||
h = tmp
|
||||
}
|
||||
}
|
||||
|
||||
dest = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888)
|
||||
destAlloc = Allocation.createFromBitmap(
|
||||
rs,
|
||||
dest,
|
||||
Allocation.MipmapControl.MIPMAP_NONE,
|
||||
Allocation.USAGE_SCRIPT)
|
||||
}
|
||||
|
||||
yuvAlloc?.copyFrom(data)
|
||||
yuv2grayScript._inYUV = yuvAlloc
|
||||
// width/height are uint_32 but Kotlin wants toLong()
|
||||
yuv2grayScript._inWidth = width.toLong()
|
||||
yuv2grayScript._inHeight = height.toLong()
|
||||
|
||||
// invert every second frame to also read inverted barcodes
|
||||
if (odd) {
|
||||
yuv2grayScript.forEach_yuv2gray(rgbaAlloc)
|
||||
} else {
|
||||
yuv2grayScript.forEach_yuv2inverted(rgbaAlloc)
|
||||
}
|
||||
odd = odd xor true
|
||||
|
||||
rotatorScript._inImage = rgbaAlloc
|
||||
rotatorScript._inWidth = width
|
||||
rotatorScript._inHeight = height
|
||||
|
||||
when (orientation) {
|
||||
0 -> destAlloc?.copyFrom(rgbaAlloc)
|
||||
90 -> rotatorScript.forEach_rotate90(destAlloc, destAlloc)
|
||||
180 -> rotatorScript.forEach_rotate180(destAlloc, destAlloc)
|
||||
270 -> rotatorScript.forEach_rotate270(destAlloc, destAlloc)
|
||||
}
|
||||
|
||||
destAlloc?.copyTo(dest)
|
||||
|
||||
return dest!!
|
||||
}
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
package de.markusfisch.android.binaryeye.view
|
||||
|
||||
import android.content.Context
|
||||
import android.content.res.Configuration
|
||||
import android.content.res.Resources
|
||||
import android.graphics.Point
|
||||
import android.os.Build
|
||||
import android.util.TypedValue
|
||||
|
||||
object SystemBarMetrics {
|
||||
fun getStatusAndToolBarHeight(context: Context): Int {
|
||||
return getStatusBarHeight(context.resources) + getToolBarHeight(context)
|
||||
}
|
||||
|
||||
fun getStatusBarHeight(res: Resources): Int {
|
||||
return getIdentifierDimen(res, "status_bar_height")
|
||||
}
|
||||
|
||||
fun getToolBarHeight(context: Context): Int {
|
||||
val tv = TypedValue()
|
||||
return if (context.theme.resolveAttribute(
|
||||
android.R.attr.actionBarSize,
|
||||
tv,
|
||||
true))
|
||||
TypedValue.complexToDimensionPixelSize(
|
||||
tv.data,
|
||||
context.resources.displayMetrics)
|
||||
else
|
||||
0
|
||||
}
|
||||
|
||||
fun getNavigationBarSize(res:Resources):Point {
|
||||
val size = Point(0, 0)
|
||||
if (!getIdentifierBoolean(res, "config_showNavigationBar")) {
|
||||
return size
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR2) {
|
||||
val conf = res.getConfiguration()
|
||||
if (conf.orientation == Configuration.ORIENTATION_LANDSCAPE &&
|
||||
// according to https://developer.android.com/training/multiscreen/screensizes.html#TaskUseSWQuali
|
||||
// only a screen < 600 dp is considered to be a phone
|
||||
// and can move its navigation bar to the side
|
||||
conf.smallestScreenWidthDp < 600) {
|
||||
size.x = getIdentifierDimen(res,
|
||||
"navigation_bar_height_landscape")
|
||||
return size
|
||||
}
|
||||
}
|
||||
size.y = getIdentifierDimen(res, "navigation_bar_height")
|
||||
return size
|
||||
}
|
||||
|
||||
private fun getIdentifierBoolean(res:Resources, name:String):Boolean {
|
||||
val id = res.getIdentifier(name, "bool", "android")
|
||||
return id > 0 && res.getBoolean(id)
|
||||
}
|
||||
|
||||
private fun getIdentifierDimen(res: Resources, name: String): Int {
|
||||
val id = res.getIdentifier(name, "dimen", "android")
|
||||
return if (id > 0) res.getDimensionPixelSize(id) else 0
|
||||
}
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
package de.markusfisch.android.binaryeye.widget
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Canvas
|
||||
import android.util.AttributeSet
|
||||
import de.markusfisch.android.binaryeye.app.setSystemAndToolBarTransparency
|
||||
import de.markusfisch.android.scalingimageview.widget.ScalingImageView
|
||||
|
||||
open class ConfinedScalingImageView : ScalingImageView {
|
||||
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)
|
||||
setSystemAndToolBarTransparency(context, !bounds.contains(mappedRect))
|
||||
}
|
||||
}
|
||||
@@ -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.setSystemAndToolBarTransparency
|
||||
|
||||
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
|
||||
setSystemAndToolBarTransparency(
|
||||
context,
|
||||
false,
|
||||
scrollable
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onScrollChanged(x: Int, y: Int, oldx: Int, oldy: Int) {
|
||||
super.onScrollChanged(x, y, oldx, oldy)
|
||||
setSystemAndToolBarTransparency(context, y > 0, scrollable)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
package de.markusfisch.android.binaryeye.widget
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.*
|
||||
import android.support.v4.content.ContextCompat
|
||||
import android.util.AttributeSet
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.app.windowInsets
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
class CropImageView(context: Context, attr: AttributeSet) :
|
||||
ConfinedScalingImageView(context, attr) {
|
||||
var onScan: (() -> Rect)? = null
|
||||
|
||||
private val boundsPaint = Paint(Paint.ANTI_ALIAS_FLAG)
|
||||
private val candidatePaint = Paint(Paint.ANTI_ALIAS_FLAG)
|
||||
private val lastMappedRect = RectF()
|
||||
private val dp = context.resources.displayMetrics.density
|
||||
private val padding: Int = (dp * 24f).roundToInt()
|
||||
private val onScanRunnable = Runnable {
|
||||
onScan?.invoke()?.also {
|
||||
candidateRect = it
|
||||
invalidate()
|
||||
}
|
||||
}
|
||||
|
||||
private var candidateRect: Rect? = null
|
||||
|
||||
init {
|
||||
boundsPaint.color = ContextCompat.getColor(context, R.color.crop_bound)
|
||||
boundsPaint.style = Paint.Style.STROKE
|
||||
boundsPaint.strokeWidth = dp * 2f
|
||||
boundsPaint.pathEffect = DashPathEffect(floatArrayOf(dp * 10f, dp * 10f), 0f)
|
||||
candidatePaint.color = ContextCompat.getColor(context, R.color.candidate)
|
||||
candidatePaint.style = Paint.Style.FILL_AND_STROKE
|
||||
candidatePaint.strokeWidth = dp * 3f
|
||||
scaleType = ScaleType.CENTER_CROP
|
||||
}
|
||||
|
||||
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(): RectF {
|
||||
return bounds
|
||||
}
|
||||
|
||||
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)
|
||||
val rc = candidateRect
|
||||
if (rc != null && rc.width() > 0) {
|
||||
canvas.drawRect(rc, candidatePaint)
|
||||
candidateRect = null
|
||||
}
|
||||
val mr = mappedRect ?: return
|
||||
if (mr != lastMappedRect) {
|
||||
removeCallbacks(onScanRunnable)
|
||||
postDelayed(onScanRunnable, 500)
|
||||
lastMappedRect.set(mr)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,90 +1,106 @@
|
||||
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.LuminanceSource
|
||||
import com.google.zxing.MultiFormatReader
|
||||
import com.google.zxing.MultiFormatWriter
|
||||
import com.google.zxing.ReaderException
|
||||
import com.google.zxing.Result
|
||||
import com.google.zxing.RGBLuminanceSource
|
||||
import com.google.zxing.common.HybridBinarizer
|
||||
|
||||
import android.graphics.Bitmap
|
||||
|
||||
import java.util.Arrays
|
||||
import java.util.EnumMap
|
||||
import java.util.EnumSet
|
||||
import com.google.zxing.*
|
||||
import com.google.zxing.common.HybridBinarizer
|
||||
import java.util.*
|
||||
|
||||
class Zxing {
|
||||
companion object {
|
||||
private val black = 0xff000000.toInt()
|
||||
private val white = 0xffffffff.toInt()
|
||||
|
||||
fun encodeAsBitmap(
|
||||
text: String,
|
||||
format: BarcodeFormat,
|
||||
width: Int,
|
||||
height: Int): Bitmap? {
|
||||
val result = MultiFormatWriter().encode(text, format,
|
||||
width, height, null)
|
||||
val w = result.getWidth()
|
||||
val h = result.getHeight()
|
||||
val pixels = IntArray(w * h)
|
||||
var offset = 0
|
||||
for (y in 0..h - 1) {
|
||||
for (x in 0..w - 1) {
|
||||
pixels[offset + x] = if (result.get(x, y))
|
||||
black
|
||||
else
|
||||
white
|
||||
}
|
||||
offset += w
|
||||
}
|
||||
val bitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888)
|
||||
bitmap.setPixels(pixels, 0, width, 0, 0, w, h)
|
||||
return bitmap
|
||||
}
|
||||
}
|
||||
|
||||
private val multiFormatReader: MultiFormatReader = MultiFormatReader()
|
||||
|
||||
init {
|
||||
val decodeFormats = EnumSet.noneOf<BarcodeFormat>(
|
||||
BarcodeFormat::class.java)
|
||||
decodeFormats.addAll(EnumSet.copyOf(Arrays.asList(
|
||||
BarcodeFormat.AZTEC,
|
||||
BarcodeFormat.CODABAR,
|
||||
BarcodeFormat.CODE_39,
|
||||
BarcodeFormat.CODE_93,
|
||||
BarcodeFormat.CODE_128,
|
||||
BarcodeFormat.DATA_MATRIX,
|
||||
BarcodeFormat.EAN_8,
|
||||
BarcodeFormat.EAN_13,
|
||||
BarcodeFormat.ITF,
|
||||
BarcodeFormat.MAXICODE,
|
||||
BarcodeFormat.PDF_417,
|
||||
BarcodeFormat.QR_CODE,
|
||||
BarcodeFormat.RSS_14,
|
||||
BarcodeFormat.RSS_EXPANDED,
|
||||
BarcodeFormat.UPC_A,
|
||||
BarcodeFormat.UPC_E,
|
||||
BarcodeFormat.UPC_EAN_EXTENSION
|
||||
)))
|
||||
BarcodeFormat::class.java
|
||||
)
|
||||
decodeFormats.addAll(
|
||||
EnumSet.copyOf(
|
||||
listOf(
|
||||
BarcodeFormat.AZTEC,
|
||||
BarcodeFormat.CODABAR,
|
||||
BarcodeFormat.CODE_39,
|
||||
BarcodeFormat.CODE_93,
|
||||
BarcodeFormat.CODE_128,
|
||||
BarcodeFormat.DATA_MATRIX,
|
||||
BarcodeFormat.EAN_8,
|
||||
BarcodeFormat.EAN_13,
|
||||
BarcodeFormat.ITF,
|
||||
BarcodeFormat.MAXICODE,
|
||||
BarcodeFormat.PDF_417,
|
||||
BarcodeFormat.QR_CODE,
|
||||
BarcodeFormat.RSS_14,
|
||||
BarcodeFormat.RSS_EXPANDED,
|
||||
BarcodeFormat.UPC_A,
|
||||
BarcodeFormat.UPC_E,
|
||||
BarcodeFormat.UPC_EAN_EXTENSION
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
val hints = EnumMap<DecodeHintType, Any>(DecodeHintType::class.java)
|
||||
hints.put(DecodeHintType.POSSIBLE_FORMATS, decodeFormats)
|
||||
hints[DecodeHintType.POSSIBLE_FORMATS] = decodeFormats
|
||||
|
||||
multiFormatReader.setHints(hints)
|
||||
}
|
||||
|
||||
fun decodeBitmap(bitmap: Bitmap): Result? {
|
||||
fun decode(
|
||||
yuvData: ByteArray,
|
||||
width: Int,
|
||||
height: Int,
|
||||
invert: Boolean = false
|
||||
): Result? {
|
||||
val source = PlanarYUVLuminanceSource(
|
||||
yuvData,
|
||||
width,
|
||||
height,
|
||||
0,
|
||||
0,
|
||||
width,
|
||||
height,
|
||||
false
|
||||
)
|
||||
return decodeLuminanceSource(source, invert)
|
||||
}
|
||||
|
||||
fun decodePositiveNegative(bitmap: Bitmap): Result? {
|
||||
val result = decode(bitmap, false)
|
||||
if (result != null) {
|
||||
return result
|
||||
}
|
||||
return decode(bitmap, true)
|
||||
}
|
||||
|
||||
fun decode(bitmap: Bitmap, invert: Boolean = false): Result? {
|
||||
val pixels = IntArray(bitmap.width * bitmap.height)
|
||||
return decode(pixels, bitmap, invert)
|
||||
}
|
||||
|
||||
fun decode(
|
||||
pixels: IntArray,
|
||||
bitmap: Bitmap,
|
||||
invert: Boolean = false
|
||||
): Result? {
|
||||
val width = bitmap.width
|
||||
val height = bitmap.height
|
||||
val data = IntArray(width * height)
|
||||
bitmap.getPixels(data, 0, width, 0, 0, width, height)
|
||||
return decodeLuminanceSource(RGBLuminanceSource(width, height, data))
|
||||
if (bitmap.config != Bitmap.Config.ARGB_8888) {
|
||||
bitmap.copy(Bitmap.Config.ARGB_8888, true)
|
||||
} else {
|
||||
bitmap
|
||||
}.getPixels(pixels, 0, width, 0, 0, width, height)
|
||||
val source = RGBLuminanceSource(width, height, pixels)
|
||||
return decodeLuminanceSource(source, invert)
|
||||
}
|
||||
|
||||
private fun decodeLuminanceSource(
|
||||
source: LuminanceSource,
|
||||
invert: Boolean
|
||||
): Result? {
|
||||
return decodeLuminanceSource(
|
||||
if (invert) {
|
||||
source.invert()
|
||||
} else {
|
||||
source
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun decodeLuminanceSource(source: LuminanceSource): Result? {
|
||||
@@ -97,4 +113,43 @@ class Zxing {
|
||||
multiFormatReader.reset()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val BLACK = 0xff000000.toInt()
|
||||
private const val WHITE = 0xffffffff.toInt()
|
||||
|
||||
fun encodeAsBitmap(
|
||||
text: String,
|
||||
format: BarcodeFormat,
|
||||
width: Int,
|
||||
height: Int
|
||||
): Bitmap? {
|
||||
val hints = EnumMap<EncodeHintType, Any>(EncodeHintType::class.java)
|
||||
hints[EncodeHintType.CHARACTER_SET] = "utf-8"
|
||||
val result = MultiFormatWriter().encode(
|
||||
text,
|
||||
format,
|
||||
width,
|
||||
height,
|
||||
hints
|
||||
)
|
||||
val w = result.width
|
||||
val h = result.height
|
||||
val pixels = IntArray(w * h)
|
||||
var offset = 0
|
||||
for (y in 0 until h) {
|
||||
for (x in 0 until w) {
|
||||
pixels[offset + x] = if (result.get(x, y)) {
|
||||
BLACK
|
||||
} else {
|
||||
WHITE
|
||||
}
|
||||
}
|
||||
offset += w
|
||||
}
|
||||
val bitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888)
|
||||
bitmap.setPixels(pixels, 0, w, 0, 0, w, h)
|
||||
return bitmap
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 5.8 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 2.9 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 3.8 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 7.8 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
@@ -1,7 +0,0 @@
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="line"
|
||||
android:gravity="center">
|
||||
<stroke
|
||||
android:width="1dp"
|
||||
android:color="@color/accent"/>
|
||||
</shape>
|
||||
@@ -3,6 +3,6 @@
|
||||
<item>
|
||||
<bitmap
|
||||
android:gravity="center"
|
||||
android:src="@mipmap/ic_launcher"/>
|
||||
android:src="@drawable/logo"/>
|
||||
</item>
|
||||
</layer-list>
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
<vector android:height="24dp" android:tint="#FFFFFF"
|
||||
android:viewportHeight="24.0" android:viewportWidth="24.0"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#FF000000" android:pathData="M6,19c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2L18,7L6,7v12zM8.46,11.88l1.41,-1.41L12,12.59l2.12,-2.12 1.41,1.41L13.41,14l2.12,2.12 -1.41,1.41L12,15.41l-2.12,2.12 -1.41,-1.41L10.59,14l-2.13,-2.12zM15.5,4l-1,-1h-5l-1,1L5,4v2h14L19,4z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,5 @@
|
||||
<vector android:height="24dp" android:tint="#FFFFFF"
|
||||
android:viewportHeight="24.0" android:viewportWidth="24.0"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#FF000000" android:pathData="M13,3c-4.97,0 -9,4.03 -9,9L1,12l3.89,3.89 0.07,0.14L9,12L6,12c0,-3.87 3.13,-7 7,-7s7,3.13 7,7 -3.13,7 -7,7c-1.93,0 -3.68,-0.79 -4.94,-2.06l-1.42,1.42C8.27,19.99 10.51,21 13,21c4.97,0 9,-4.03 9,-9s-4.03,-9 -9,-9zM12,8v5l4.28,2.54 0.72,-1.21 -3.5,-2.08L13.5,8L12,8z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,6 @@
|
||||
<vector android:height="24dp" android:tint="#FFFFFF"
|
||||
android:viewportHeight="24.0" android:viewportWidth="24.0"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#FF000000"
|
||||
android:pathData="M20,4L4,4c-1.1,0 -1.99,0.9 -1.99,2L2,18c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2L22,6c0,-1.1 -0.9,-2 -2,-2zM20,18L4,18L4,8l8,5 8,-5v10zM12,11L4,6h16l-8,5z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:height="24dp"
|
||||
android:tint="#FFFFFF"
|
||||
android:viewportHeight="24.0"
|
||||
android:viewportWidth="24.0"
|
||||
android:width="24dp">
|
||||
<path android:fillColor="#FF000000"
|
||||
android:pathData="M14,10L2,10v2h12v-2zM14,6L2,6v2h12L14,6zM18,14v-4h-2v4h-4v2h4v4h2v-4h4v-2h-4zM2,16h8v-2L2,14v2z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,5 @@
|
||||
<vector android:height="24dp" android:tint="#FFFFFF"
|
||||
android:viewportHeight="24.0" android:viewportWidth="24.0"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#FF000000" android:pathData="M6,2c-1.1,0 -1.99,0.9 -1.99,2L4,20c0,1.1 0.89,2 1.99,2L18,22c1.1,0 2,-0.9 2,-2L20,8l-6,-6L6,2zM13,9L13,3.5L18.5,9L13,9z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,5 @@
|
||||
<vector android:height="24dp" android:tint="#FFFFFF"
|
||||
android:viewportHeight="24" android:viewportWidth="24"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#FF000000" android:pathData="M19.1,12.9a2.8,2.8 0,0 0,0.1 -0.9,2.8 2.8,0 0,0 -0.1,-0.9l2.1,-1.6a0.7,0.7 0,0 0,0.1 -0.6L19.4,5.5a0.7,0.7 0,0 0,-0.6 -0.2l-2.4,1a6.5,6.5 0,0 0,-1.6 -0.9l-0.4,-2.6a0.5,0.5 0,0 0,-0.5 -0.4H10.1a0.5,0.5 0,0 0,-0.5 0.4L9.3,5.4a5.6,5.6 0,0 0,-1.7 0.9l-2.4,-1a0.4,0.4 0,0 0,-0.5 0.2l-2,3.4c-0.1,0.2 0,0.4 0.2,0.6l2,1.6a2.8,2.8 0,0 0,-0.1 0.9,2.8 2.8,0 0,0 0.1,0.9L2.8,14.5a0.7,0.7 0,0 0,-0.1 0.6l1.9,3.4a0.7,0.7 0,0 0,0.6 0.2l2.4,-1a6.5,6.5 0,0 0,1.6 0.9l0.4,2.6a0.5,0.5 0,0 0,0.5 0.4h3.8a0.5,0.5 0,0 0,0.5 -0.4l0.3,-2.6a5.6,5.6 0,0 0,1.7 -0.9l2.4,1a0.4,0.4 0,0 0,0.5 -0.2l2,-3.4c0.1,-0.2 0,-0.4 -0.2,-0.6ZM12,15.6A3.6,3.6 0,1 1,15.6 12,3.6 3.6,0 0,1 12,15.6Z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,5 @@
|
||||
<vector android:height="24dp" android:tint="#FFFFFF"
|
||||
android:viewportHeight="24.0" android:viewportWidth="24.0"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#FF000000" android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM17,13L7,13v-2h10v2z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,5 @@
|
||||
<vector android:height="24dp" android:tint="#FFFFFF"
|
||||
android:viewportHeight="24.0" android:viewportWidth="24.0"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#FF000000" android:pathData="M15.55,5.55L11,1v3.07C7.06,4.56 4,7.92 4,12s3.05,7.44 7,7.93v-2.02c-2.84,-0.48 -5,-2.94 -5,-5.91s2.16,-5.43 5,-5.91L11,10l4.55,-4.45zM19.93,11c-0.17,-1.39 -0.72,-2.73 -1.62,-3.89l-1.42,1.42c0.54,0.75 0.88,1.6 1.02,2.47h2.02zM13,17.9v2.02c1.39,-0.17 2.74,-0.71 3.9,-1.61l-1.44,-1.44c-0.75,0.54 -1.59,0.89 -2.46,1.03zM16.89,15.48l1.42,1.41c0.9,-1.16 1.45,-2.5 1.62,-3.89h-2.02c-0.14,0.87 -0.48,1.72 -1.02,2.48z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,5 @@
|
||||
<vector android:height="24dp" android:tint="#FFFFFF"
|
||||
android:viewportHeight="24.0" android:viewportWidth="24.0"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#FF000000" android:pathData="M17,3L5,3c-1.11,0 -2,0.9 -2,2v14c0,1.1 0.89,2 2,2h14c1.1,0 2,-0.9 2,-2L21,7l-4,-4zM12,19c-1.66,0 -3,-1.34 -3,-3s1.34,-3 3,-3 3,1.34 3,3 -1.34,3 -3,3zM15,9L5,9L5,5h10v4z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,5 @@
|
||||
<vector android:height="24dp" android:tint="#FFFFFF"
|
||||
android:viewportHeight="24.0" android:viewportWidth="24.0"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#FF000000" android:pathData="M15.5,14h-0.79l-0.28,-0.27C15.41,12.59 16,11.11 16,9.5 16,5.91 13.09,3 9.5,3S3,5.91 3,9.5 5.91,16 9.5,16c1.61,0 3.09,-0.59 4.23,-1.57l0.27,0.28v0.79l5,4.99L20.49,19l-4.99,-5zM9.5,14C7.01,14 5,11.99 5,9.5S7.01,5 9.5,5 14,7.01 14,9.5 11.99,14 9.5,14z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,6 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp"
|
||||
android:tint="#FFFFFF" android:viewportHeight="24.0"
|
||||
android:viewportWidth="24.0" android:width="24dp">
|
||||
<path android:fillColor="#FF000000"
|
||||
android:pathData="M15.5,14h-0.79l-0.28,-0.27C15.41,12.59 16,11.11 16,9.5 16,5.91 13.09,3 9.5,3S3,5.91 3,9.5 5.91,16 9.5,16c1.61,0 3.09,-0.59 4.23,-1.57l0.27,0.28v0.79l5,4.99L20.49,19l-4.99,-5zM9.5,14C7.01,14 5,11.99 5,9.5S7.01,5 9.5,5 14,7.01 14,9.5 11.99,14 9.5,14z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,6 @@
|
||||
<vector android:height="24dp" android:tint="#FFFFFF"
|
||||
android:viewportHeight="24.0" android:viewportWidth="24.0"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#FF000000"
|
||||
android:pathData="M20,2L4,2c-1.1,0 -1.99,0.9 -1.99,2L2,22l4,-4h14c1.1,0 2,-0.9 2,-2L22,4c0,-1.1 -0.9,-2 -2,-2zM9,11L7,11L7,9h2v2zM13,11h-2L11,9h2v2zM17,11h-2L15,9h2v2z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,5 @@
|
||||
<vector android:height="24dp" android:tint="#FFFFFF"
|
||||
android:viewportHeight="24.0" android:viewportWidth="24.0"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#FF000000" android:pathData="M20,4h-3.17L15,2L9,2L7.17,4L4,4c-1.1,0 -2,0.9 -2,2v12c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2L22,6c0,-1.1 -0.9,-2 -2,-2zM15,15.5L15,13L9,13v2.5L5.5,12 9,8.5L9,11h6L15,8.5l3.5,3.5 -3.5,3.5z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,6 @@
|
||||
<vector android:height="24dp" android:tint="#FFFFFF"
|
||||
android:viewportHeight="24.0" android:viewportWidth="24.0"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#FF000000"
|
||||
android:pathData="M6.62,10.79c1.44,2.83 3.76,5.14 6.59,6.59l2.2,-2.2c0.27,-0.27 0.67,-0.36 1.02,-0.24 1.12,0.37 2.33,0.57 3.57,0.57 0.55,0 1,0.45 1,1V20c0,0.55 -0.45,1 -1,1 -9.39,0 -17,-7.61 -17,-17 0,-0.55 0.45,-1 1,-1h3.5c0.55,0 1,0.45 1,1 0,1.25 0.2,2.45 0.57,3.57 0.11,0.35 0.03,0.74 -0.25,1.02l-2.2,2.2z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,6 @@
|
||||
<vector android:height="24dp" android:tint="#FFFFFF"
|
||||
android:viewportHeight="24.0" android:viewportWidth="24.0"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#FF000000"
|
||||
android:pathData="M19,3h-1L18,1h-2v2L8,3L8,1L6,1v2L5,3c-1.11,0 -2,0.9 -2,2v14c0,1.1 0.89,2 2,2h14c1.1,0 2,-0.9 2,-2L21,5c0,-1.1 -0.9,-2 -2,-2zM12,6c1.66,0 3,1.34 3,3s-1.34,3 -3,3 -3,-1.34 -3,-3 1.34,-3 3,-3zM18,18L6,18v-1c0,-2 4,-3.1 6,-3.1s6,1.1 6,3.1v1z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,6 @@
|
||||
<vector android:height="24dp" android:tint="#FFFFFF"
|
||||
android:viewportHeight="24.0" android:viewportWidth="24.0"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#FF000000"
|
||||
android:pathData="M17,12h-5v5h5v-5zM16,1v2L8,3L8,1L6,1v2L5,3c-1.11,0 -1.99,0.9 -1.99,2L3,19c0,1.1 0.89,2 2,2h14c1.1,0 2,-0.9 2,-2L21,5c0,-1.1 -0.9,-2 -2,-2h-1L18,1h-2zM19,19L5,19L5,8h14v11z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,5 @@
|
||||
<vector android:height="24dp" android:tint="#FFFFFF"
|
||||
android:viewportHeight="24.0" android:viewportWidth="24.0"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#FF000000" android:pathData="M1,9l2,2c4.97,-4.97 13.03,-4.97 18,0l2,-2C16.93,2.93 7.08,2.93 1,9zM9,17l3,3 3,-3c-1.65,-1.66 -4.34,-1.66 -6,0zM5,13l2,2c2.76,-2.76 7.24,-2.76 10,0l2,-2C15.14,9.14 8.87,9.14 5,13z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,3 @@
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<solid android:color="#b6d46f"/>
|
||||
</shape>
|
||||
@@ -0,0 +1,70 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M52,-884.362h29v71h-29z"/>
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M52,-884.362h29v71h-29z"/>
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M52,-884.362h29v71h-29z"/>
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M32.002,-24.974h3.127v7.656h-3.127z"/>
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M52,-884.362h29v71h-29z"/>
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M52,-884.362h29v71h-29z"/>
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M52,-884.362h29v71h-29z"/>
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M52,-884.362h29v71h-29z"/>
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M52,-884.362h29v71h-29z"/>
|
||||
<path
|
||||
android:pathData="m54.826,27.893c-6.851,0 -13.701,2.581 -18.924,7.745 -10.445,10.328 -10.413,27.062 0,37.423l36.218,36.036 37.879,0.902 -0.07,-38.363 -36.18,-35.998c-5.207,-5.18 -12.073,-7.745 -18.924,-7.745z"
|
||||
android:strokeAlpha="1"
|
||||
android:strokeLineJoin="miter"
|
||||
android:strokeWidth="8.60725021"
|
||||
android:fillColor="#000000"
|
||||
android:strokeColor="#00000000"
|
||||
android:fillType="nonZero"
|
||||
android:fillAlpha="0.1254902"
|
||||
android:strokeLineCap="butt"/>
|
||||
<path
|
||||
android:pathData="M28.001,54.118a26.525,26.227 0,1 0,53.049 0a26.525,26.227 0,1 0,-53.049 0z"
|
||||
android:strokeAlpha="1"
|
||||
android:strokeLineJoin="miter"
|
||||
android:strokeWidth="4"
|
||||
android:fillColor="#ffffff"
|
||||
android:strokeColor="#00000000"
|
||||
android:fillAlpha="1"
|
||||
android:strokeLineCap="butt"/>
|
||||
<path
|
||||
android:pathData="M39.26,54.988a14.509,14.346 0,1 0,29.019 0a14.509,14.346 0,1 0,-29.019 0z"
|
||||
android:strokeAlpha="1"
|
||||
android:strokeLineJoin="miter"
|
||||
android:strokeWidth="4"
|
||||
android:fillColor="#000000"
|
||||
android:strokeColor="#00000000"
|
||||
android:fillAlpha="1"
|
||||
android:strokeLineCap="butt"/>
|
||||
<path
|
||||
android:pathData="M54.034,44.513a9.535,9.428 0,1 0,19.07 0a9.535,9.428 0,1 0,-19.07 0z"
|
||||
android:strokeAlpha="1"
|
||||
android:strokeLineJoin="miter"
|
||||
android:strokeWidth="10"
|
||||
android:fillColor="#ffffff"
|
||||
android:strokeColor="#00000000"
|
||||
android:fillAlpha="1"
|
||||
android:strokeLineCap="butt"/>
|
||||
</vector>
|
||||
@@ -9,9 +9,7 @@
|
||||
android:id="@+id/main_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:fitsSystemWindows="true"
|
||||
android:background="@drawable/background_scanner">
|
||||
android:fitsSystemWindows="true">
|
||||
<include layout="@layout/toolbar"/>
|
||||
<SeekBar
|
||||
android:id="@+id/zoom"
|
||||
@@ -33,6 +31,7 @@
|
||||
android:layout_alignParentRight="true"
|
||||
android:layout_alignParentBottom="true"
|
||||
android:layout_margin="20dp"
|
||||
android:contentDescription="@string/toggle_flash"
|
||||
android:src="@drawable/ic_action_flash"
|
||||
app:elevation="4dp"/>
|
||||
</RelativeLayout>
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
<RelativeLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:fitsSystemWindows="true">
|
||||
<de.markusfisch.android.binaryeye.widget.CropImageView
|
||||
android:id="@+id/image"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:contentDescription="@string/view_barcode"/>
|
||||
<include layout="@layout/toolbar"/>
|
||||
<android.support.design.widget.FloatingActionButton
|
||||
tools:ignore="UnusedAttribute"
|
||||
android:id="@+id/scan"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_alignParentRight="true"
|
||||
android:layout_alignParentBottom="true"
|
||||
android:layout_margin="20dp"
|
||||
android:contentDescription="@string/scan_code"
|
||||
android:src="@drawable/ic_action_scan"
|
||||
app:elevation="4dp"/>
|
||||
</RelativeLayout>
|
||||
@@ -0,0 +1,21 @@
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:text="@string/save_as_file_name"/>
|
||||
<EditText
|
||||
tools:targetApi="o"
|
||||
android:id="@+id/file_name"
|
||||
android:inputType="text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/file_name"
|
||||
android:importantForAutofill="no"/>
|
||||
</LinearLayout>
|
||||
@@ -1,6 +1,24 @@
|
||||
<ImageView
|
||||
<RelativeLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/barcode"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:contentDescription="@string/view_barcode"/>
|
||||
android:layout_height="match_parent">
|
||||
<de.markusfisch.android.binaryeye.widget.ConfinedScalingImageView
|
||||
android:id="@+id/barcode"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:contentDescription="@string/view_barcode"/>
|
||||
<android.support.design.widget.FloatingActionButton
|
||||
tools:ignore="UnusedAttribute"
|
||||
android:id="@+id/share"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_alignParentRight="true"
|
||||
android:layout_alignParentBottom="true"
|
||||
android:layout_margin="20dp"
|
||||
android:contentDescription="@string/share"
|
||||
android:src="@drawable/ic_action_share"
|
||||
app:elevation="4dp"/>
|
||||
</RelativeLayout>
|
||||
|
||||
@@ -4,24 +4,62 @@
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
<EditText
|
||||
android:id="@+id/content"
|
||||
<de.markusfisch.android.binaryeye.widget.ConfinedScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="16dp"
|
||||
android:gravity="start|top"
|
||||
android:imeOptions="flagNoExtractUi"
|
||||
android:inputType="textMultiLine"
|
||||
android:typeface="monospace"/>
|
||||
android:layout_height="match_parent">
|
||||
<RelativeLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<EditText
|
||||
android:id="@+id/content"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="16dp"
|
||||
android:gravity="start|top"
|
||||
android:hint="@string/content"
|
||||
android:imeOptions="flagNoExtractUi"
|
||||
android:importantForAutofill="no"
|
||||
android:inputType="textMultiLine"
|
||||
android:typeface="monospace"
|
||||
tools:ignore="UnusedAttribute"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/format"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@+id/content"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginRight="16dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:gravity="end|top"
|
||||
android:textSize="14sp" />
|
||||
<TextView
|
||||
android:id="@+id/hex"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@+id/format"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginLeft="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginRight="16dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:typeface="monospace"
|
||||
android:textSize="12sp"
|
||||
tools:text="54 65 73 74 20 51 52 20 Test QR\n43 6F 64 65 Code"/>
|
||||
|
||||
</RelativeLayout>
|
||||
</de.markusfisch.android.binaryeye.widget.ConfinedScrollView>
|
||||
<android.support.design.widget.FloatingActionButton
|
||||
tools:ignore="UnusedAttribute"
|
||||
android:id="@+id/share"
|
||||
android:id="@+id/open"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_alignParentRight="true"
|
||||
android:layout_alignParentBottom="true"
|
||||
android:layout_margin="20dp"
|
||||
android:src="@drawable/ic_action_share"
|
||||
android:contentDescription="@string/open_url"
|
||||
android:src="@drawable/ic_action_open"
|
||||
app:elevation="4dp"/>
|
||||
</RelativeLayout>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<ScrollView
|
||||
<de.markusfisch.android.binaryeye.widget.ConfinedScrollView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
@@ -31,6 +31,7 @@
|
||||
android:layout_marginBottom="8dp"
|
||||
android:gravity="center"/>
|
||||
<SeekBar
|
||||
style="@style/Widget.AppCompat.SeekBar.Discrete"
|
||||
android:id="@+id/size_bar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
@@ -43,6 +44,7 @@
|
||||
android:layout_marginBottom="8dp"
|
||||
android:text="@string/content"/>
|
||||
<EditText
|
||||
tools:targetApi="o"
|
||||
android:id="@+id/content"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
@@ -51,11 +53,12 @@
|
||||
android:typeface="monospace"
|
||||
android:imeOptions="flagNoExtractUi"
|
||||
android:inputType="textMultiLine"
|
||||
android:hint="@string/input_content_here"/>
|
||||
android:hint="@string/input_content_here"
|
||||
android:importantForAutofill="no"/>
|
||||
<Button
|
||||
style="@style/Button"
|
||||
android:id="@+id/encode"
|
||||
android:layout_width="match_parent"
|
||||
android:text="@string/encode"/>
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
||||
</de.markusfisch.android.binaryeye.widget.ConfinedScrollView>
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
<RelativeLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
<ListView
|
||||
android:id="@+id/scans"
|
||||
android:choiceMode="singleChoice"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:divider="@color/separator"
|
||||
android:dividerHeight="1px"/>
|
||||
<android.support.v7.widget.SwitchCompat
|
||||
android:visibility="gone"
|
||||
android:id="@+id/use_history"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentTop="true"
|
||||
android:padding="16dp"
|
||||
android:text="@string/use_history"/>
|
||||
<android.support.design.widget.FloatingActionButton
|
||||
tools:ignore="UnusedAttribute"
|
||||
android:visibility="gone"
|
||||
android:id="@+id/share"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_alignParentRight="true"
|
||||
android:layout_alignParentBottom="true"
|
||||
android:layout_margin="20dp"
|
||||
android:contentDescription="@string/share"
|
||||
android:src="@drawable/ic_action_share"
|
||||
app:elevation="4dp"/>
|
||||
<include layout="@layout/progress_view"/>
|
||||
</RelativeLayout>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user