Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9dbb78ce67 | ||
|
|
47c252ab18 | ||
|
|
10e02b3164 | ||
|
|
7e82dbe730 | ||
|
|
e91c30f7c2 | ||
|
|
30a36e7e07 | ||
|
|
5f91fe2f02 | ||
|
|
cab8d6c038 | ||
|
|
4aac5dc61b | ||
|
|
fbbc20d327 | ||
|
|
5595b68ff4 | ||
|
|
c492897fa9 | ||
|
|
647c08bfb8 | ||
|
|
e5025ef58c | ||
|
|
74bb421b2d | ||
|
|
4ed8d4614a | ||
|
|
f038eb989b | ||
|
|
c5722a3890 | ||
|
|
41108007ad | ||
|
|
867f179a37 | ||
|
|
892916b0d0 | ||
|
|
3bd440fdb7 | ||
|
|
a4c8551be8 | ||
|
|
316644d3ae | ||
|
|
9fb2eb2d2b | ||
|
|
dd046a5d52 | ||
|
|
f3ddd458cb | ||
|
|
157d928620 | ||
|
|
94cb509f0b | ||
|
|
df18f8ebd3 | ||
|
|
705095472f | ||
|
|
f897973b3f | ||
|
|
c88d5cf4cd | ||
|
|
8ecd49731f | ||
|
|
6f3b4dbdab | ||
|
|
3c8c0737c4 | ||
|
|
9baf6d621a | ||
|
|
f0d0ee04af | ||
|
|
4d38da045a | ||
|
|
af70d9507b | ||
|
|
273f38c36c | ||
|
|
210cdfc171 | ||
|
|
3ec8c4b5d7 | ||
|
|
d99f0ca039 | ||
|
|
4a9e700d20 | ||
|
|
40e339ce89 | ||
|
|
9d476b3491 | ||
|
|
22b58265a5 | ||
|
|
bd4576b7ac | ||
|
|
a57f6d1904 | ||
|
|
842b38c986 | ||
|
|
b1fae727a9 | ||
|
|
db24f608d5 | ||
|
|
9f49a00bae | ||
|
|
947bbf3031 | ||
|
|
b3b57a7ba8 | ||
|
|
1627f3ead9 | ||
|
|
4343f5da82 | ||
|
|
67395a067d | ||
|
|
0de571f3c1 | ||
|
|
d83cd77a87 | ||
|
|
5ca7aff5df | ||
|
|
e4cc516a9b | ||
|
|
f160cf0953 | ||
|
|
ef5b5aa34a | ||
|
|
536ee5bc3d | ||
|
|
0becada572 | ||
|
|
0c773d6f05 | ||
|
|
f2df364840 | ||
|
|
5740b43568 | ||
|
|
b384b99b01 | ||
|
|
6affa4ce25 | ||
|
|
455d329fff | ||
|
|
792be87283 | ||
|
|
381e10bddc | ||
|
|
220d1b9d4c | ||
|
|
020cd2c192 | ||
|
|
e8766a0fd1 | ||
|
|
69c6d1324e | ||
|
|
2dd0c48225 | ||
|
|
faa536274b | ||
|
|
0a944f9fb8 | ||
|
|
a31982646a | ||
|
|
4320ee8fcc | ||
|
|
d890bf28fa | ||
|
|
6347bd568c | ||
|
|
9de11e2ae8 | ||
|
|
57c381e310 | ||
|
|
a11650eaa5 | ||
|
|
e85b006e40 | ||
|
|
a6c25be316 | ||
|
|
2b011325a6 | ||
|
|
fb96d5ecf8 | ||
|
|
897d61a646 | ||
|
|
ac67db6491 | ||
|
|
84bc7dad30 | ||
|
|
b21af65b68 | ||
|
|
a7adec7177 | ||
|
|
fc6fcfa743 | ||
|
|
47f95fc67a | ||
|
|
deccc9eb36 | ||
|
|
6505312b44 | ||
|
|
cb82fc3252 | ||
|
|
636ac2386a | ||
|
|
49d32ff95c | ||
|
|
fed7d561c5 | ||
|
|
8e22290c5f | ||
|
|
b33e474d12 | ||
|
|
40a05a1fc0 | ||
|
|
d947764882 | ||
|
|
1a5a8e0fa1 | ||
|
|
e6d9d54866 | ||
|
|
538155bf04 | ||
|
|
3266a58041 | ||
|
|
d594a43d6d | ||
|
|
0aa1f80451 | ||
|
|
36eff75546 | ||
|
|
8aef9c22dd | ||
|
|
1a159b35ee | ||
|
|
05ab87caa8 | ||
|
|
a3f0185f08 | ||
|
|
104f409486 | ||
|
|
eb8f578bd8 | ||
|
|
a8e420fc95 | ||
|
|
7120e18670 | ||
|
|
9fb34965f5 | ||
|
|
c5cbd1140b | ||
|
|
fecdcd7deb | ||
|
|
fd3d2f9934 | ||
|
|
35b6ead662 | ||
|
|
ad62cb5a03 | ||
|
|
e1905b1ff2 | ||
|
|
9aecf0d83b | ||
|
|
2645366baa | ||
|
|
37ea4ac9af | ||
|
|
4a6f5c5f92 | ||
|
|
b1ddb0fe7a | ||
|
|
2f61db731d | ||
|
|
b0de90c702 | ||
|
|
8df09090c9 | ||
|
|
9feef33275 | ||
|
|
147ad22df0 | ||
|
|
be833b7645 | ||
|
|
0eb921d579 | ||
|
|
1d156e0dbd | ||
|
|
82c1225848 | ||
|
|
33c1697d80 | ||
|
|
1d4e5d25ae | ||
|
|
386b47fbed | ||
|
|
b2c2632d76 | ||
|
|
4d64a5704d | ||
|
|
8875937022 | ||
|
|
3d33f62d7b | ||
|
|
a4050cbf8f | ||
|
|
abb362230e | ||
|
|
967cab9c7b | ||
|
|
c8d7be95e9 | ||
|
|
18146ec41a | ||
|
|
5cd6114ba1 | ||
|
|
8a310a1144 | ||
|
|
afc597ab90 | ||
|
|
6d742c4d19 | ||
|
|
1776559d40 | ||
|
|
75c1eb6b64 | ||
|
|
c5cffc00c3 | ||
|
|
c0153e822c | ||
|
|
f070269c2c | ||
|
|
3e8df89db1 | ||
|
|
56237c43cd | ||
|
|
7fc565ae67 | ||
|
|
656c1e36a8 | ||
|
|
42a6043f63 | ||
|
|
6a5fdae584 | ||
|
|
899deb77b5 | ||
|
|
3419194bfd | ||
|
|
3b55d48f65 | ||
|
|
d34673ddc3 | ||
|
|
242bf4c0a1 | ||
|
|
ed2a12eef8 | ||
|
|
4dd5c12e85 | ||
|
|
d3fae01786 | ||
|
|
37b6e6f832 | ||
|
|
e3274fd065 | ||
|
|
c75b9f73ab | ||
|
|
1432669aee | ||
|
|
00a8a94b59 | ||
|
|
c8c3da91ae | ||
|
|
8cbc73b7f8 | ||
|
|
941d875b3e | ||
|
|
1ac578845e | ||
|
|
e816e8bf89 | ||
|
|
54c368fcd8 | ||
|
|
c225e7d9eb | ||
|
|
6a0ce45a65 | ||
|
|
11c97cc4ef | ||
|
|
e3f6732bb8 | ||
|
|
53ae706b9c | ||
|
|
26a7ad2e6e | ||
|
|
9c3984ce7a | ||
|
|
f7d07e0b8a | ||
|
|
3fe0f50289 | ||
|
|
e86cf9d2cc | ||
|
|
4f1482d2c9 | ||
|
|
c42ad7dbea | ||
|
|
fa3cc8c408 | ||
|
|
e4863bad4c | ||
|
|
484bbbc997 | ||
|
|
565643f19e | ||
|
|
4c92045563 | ||
|
|
7116aa7278 | ||
|
|
860e867ad4 | ||
|
|
3b509cd09c | ||
|
|
51ec228fd8 | ||
|
|
9d1dec146d | ||
|
|
7bd9fe36fe | ||
|
|
85dba0604c | ||
|
|
56c70f5974 | ||
|
|
098b7740a0 | ||
|
|
7d92481fa6 | ||
|
|
2e285a5b1d | ||
|
|
ad25c4681f | ||
|
|
c00c87ceca | ||
|
|
e001a92870 | ||
|
|
7a32b00e1f | ||
|
|
50cedf782d | ||
|
|
fb0d22198f | ||
|
|
af528df448 | ||
|
|
a9b5771d57 | ||
|
|
a161e8fc0f | ||
|
|
ec17d07695 | ||
|
|
be2fc2f559 | ||
|
|
de3aa752ff | ||
|
|
411cbb243f | ||
|
|
f76c1521fe | ||
|
|
9b9224b100 | ||
|
|
62d3fd7e94 | ||
|
|
acdac4ca30 | ||
|
|
221cf11576 | ||
|
|
d34fceb48f | ||
|
|
4cc4cef4ca | ||
|
|
969ffd3d0a | ||
|
|
81a505c752 | ||
|
|
eafcf0dba6 | ||
|
|
0539468d3d | ||
|
|
5e9361ac77 | ||
|
|
54fb31d6a5 | ||
|
|
0f7da91e77 | ||
|
|
13bbb3e435 |
+141
@@ -1,5 +1,146 @@
|
||||
# Change Log
|
||||
|
||||
## 1.46.2
|
||||
* Fix saving external files on Android Q and newer
|
||||
|
||||
## 1.46.1
|
||||
* Use cropping limiter settings for image picking too
|
||||
* Remove extra link on result screen
|
||||
* Update Turkish translation
|
||||
|
||||
## 1.46.0
|
||||
* Support deep links
|
||||
* Support "A:" in WIFI codes
|
||||
* Allow multiple query arguments in custom URL
|
||||
* Add Wi-Fi info data section
|
||||
* Add Vietnamese translation
|
||||
* Update Brazilian Portuguese translation
|
||||
* Update Hungarian translation
|
||||
* Update Ukrainian translation
|
||||
* Update Russian translation
|
||||
|
||||
## 1.45.0
|
||||
* Add a setting to set the delay for bulk mode
|
||||
* Add support for MECARD contact format
|
||||
* Remove unused permissions on Android Q
|
||||
|
||||
## 1.44.1
|
||||
* Update Spanish translation
|
||||
* Update Japanese translation
|
||||
* Fix in-app language switching for App Bundles
|
||||
|
||||
## 1.44.0
|
||||
* Add a setting to choose barcode formats to scan
|
||||
* Ask before removing network suggestions
|
||||
* Update Traditional Chinese Translation
|
||||
* Update Russian language
|
||||
* Update Dutch translation
|
||||
|
||||
## 1.43.0
|
||||
* Add a preference item to remove WiFi network suggestions
|
||||
* Update Traditional Chinese Translation
|
||||
* Update Russian language
|
||||
|
||||
## 1.42.0
|
||||
* Add a setting to pick a default search engine
|
||||
* Update Traditional Chinese
|
||||
* Update Dutch translation
|
||||
|
||||
## 1.41.1
|
||||
* Update Bengali translation
|
||||
* Update Simplified Chinese
|
||||
|
||||
## 1.41.0
|
||||
* Use cropping limiter by default for new installations
|
||||
* Truncate all toasts with strings from the outside
|
||||
* Add Bangla translation
|
||||
* Add missing Japanese locale
|
||||
* Update Hungarian translation
|
||||
* Update Russian app description
|
||||
|
||||
## 1.40.0
|
||||
* Add a setting to show/hide scanned data when scanning continuously
|
||||
* Only show the first 50 characters when scanning continuously
|
||||
* Share binary data as hex dump
|
||||
* Update Russian language
|
||||
|
||||
## 1.39.0
|
||||
* Fix adding contacts and calendar entries on Android 11+
|
||||
* Add a setting to always start in bulk mode
|
||||
* Add Czech translation
|
||||
* Update Spanish translation
|
||||
* Update Czech translation
|
||||
|
||||
## 1.38.0
|
||||
* Add a quick settings tile to scan from settings
|
||||
* Make custom background requests time out after 5 seconds
|
||||
* Add Japanese translation
|
||||
* Updated French translation
|
||||
|
||||
## 1.37.0
|
||||
* Improve formatting date and time in history listing
|
||||
* Add haptic and audible feedback for background request errors
|
||||
|
||||
## 1.36.0
|
||||
* Add a setting to automatically return to scanning screen after copying
|
||||
* Add a button to test the background request
|
||||
* Show HTTP response for background requests
|
||||
* Improve SVG export
|
||||
* Automatically continue with an action after granting a permission
|
||||
* Update Chinese translation
|
||||
* Update French translation
|
||||
* Update Indonesian translation
|
||||
* Update Turkish translation
|
||||
* Update Ukrainian translation
|
||||
|
||||
## 1.35.0
|
||||
* Put images with transparent pixels on white/black
|
||||
* Use cropping limiter when scanning from images too
|
||||
* Support setting the error correction level when creating QR Codes
|
||||
* Support setting a custom deviating locale
|
||||
* Use adaptive icons for shortcut icons
|
||||
* Add exporting barcodes as plain text
|
||||
* Add Ukrainian translation
|
||||
* Add Turkish translation
|
||||
* Add Georgian translation
|
||||
* Update Indonesian translation
|
||||
* Update Russian translation
|
||||
* Update Italian translation
|
||||
* Update Simplified Chinese translation
|
||||
|
||||
## 1.34.0
|
||||
* Use Material Design settings with headers
|
||||
* Add a setting to choose the message type when forwarding scanned content
|
||||
|
||||
## 1.33.0
|
||||
* Add a setting to automatically send the scanned content to a custom URL
|
||||
* Add a setting to automatically put contents into clipboard
|
||||
* Add an explicit button to copy WiFi password into clipboard
|
||||
* Update Indonesian translation
|
||||
|
||||
## 1.32.1
|
||||
* Alternatively remove region of interest by dragging handle to a screen corner
|
||||
* Restrict region of interest to screen space
|
||||
* Remove extra action item to create barcode
|
||||
|
||||
## 1.32.0
|
||||
* Add bulk mode
|
||||
* Remember region of interest permanently
|
||||
* Export generated barcode as SVG
|
||||
* Run detection on images in background
|
||||
* Remove a scan from result view
|
||||
* Update Indonesian translation
|
||||
|
||||
## 1.31.0
|
||||
* Stop detection while region of interest is modified
|
||||
* Fix resetting region of interest
|
||||
* Add Polish translation
|
||||
|
||||
## 1.30.1
|
||||
* Improve usability of cropping limiter
|
||||
* Draw round corners around region of interest
|
||||
* Update Indonesian translation
|
||||
|
||||
## 1.30.0
|
||||
* Add a handle to define a region of interest
|
||||
* Add a setting to show/hide cropping limiter
|
||||
|
||||
@@ -23,6 +23,19 @@ test:
|
||||
cat:
|
||||
./gradlew test cAT
|
||||
|
||||
testview:
|
||||
adb shell am start -W -a android.intent.action.VIEW -d 'binaryeye://scan'
|
||||
|
||||
testscan:
|
||||
adb shell am start -a android.intent.action.VIEW \
|
||||
-c android.intent.category.BROWSABLE \
|
||||
-d 'binaryeye://scan/?ret=http%3A%2F%2Fmarkusfisch.de%2F%3Fresult%3D{RESULT}'
|
||||
|
||||
testurl:
|
||||
adb shell am start -a android.intent.action.VIEW \
|
||||
-c android.intent.category.BROWSABLE \
|
||||
-d 'http://markusfisch.de/BinaryEye?ret=http%3A%2F%2Fmarkusfisch.de%2F%3Fresult%3D{RESULT}'
|
||||
|
||||
install:
|
||||
adb $(TARGET) install -r app/build/outputs/apk/debug/app-debug.apk
|
||||
|
||||
|
||||
+47
@@ -4,3 +4,50 @@ 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.
|
||||
|
||||
## Permissions
|
||||
|
||||
### [ACCESS_FINE_LOCATION](https://developer.android.com/reference/android/Manifest.permission#ACCESS_FINE_LOCATION)
|
||||
|
||||
Required to add a WiFi network from a barcode (usually a QR Code).
|
||||
|
||||
Before Android Q, this feature requires using
|
||||
[WifiManager.getConfiguredNetworks][configuredNetworks]
|
||||
which requires `ACCESS_FINE_LOCATION`.
|
||||
|
||||
On Android Q an better, this permission is not requested nor required.
|
||||
|
||||
### [CAMERA](https://developer.android.com/reference/android/Manifest.permission#CAMERA)
|
||||
|
||||
Required to read a barcode from the camera image.
|
||||
|
||||
### [INTERNET](https://developer.android.com/reference/android/Manifest.permission#INTERNET)
|
||||
|
||||
Required to send a scanned code to a custom URL you can specify in settings.
|
||||
|
||||
### [VIBRATE](https://developer.android.com/reference/android/Manifest.permission#VIBRATE)
|
||||
|
||||
Required to vibrate on detection.
|
||||
|
||||
### [WRITE_EXTERNAL_STORAGE](https://developer.android.com/reference/android/Manifest.permission#WRITE_EXTERNAL_STORAGE)
|
||||
|
||||
Required to save generated barcodes as files before Android Q.
|
||||
|
||||
### [CHANGE_WIFI_STATE](https://developer.android.com/reference/android/Manifest.permission#CHANGE_WIFI_STATE)
|
||||
|
||||
Required to add a WiFi network from a barcode (usually a QR Code).
|
||||
|
||||
On Android Q and better, this feature requires using
|
||||
[WifiManager.addNetworkSuggestions][addNetworkSuggestions]
|
||||
which requires `CHANGE_WIFI_STATE`.
|
||||
|
||||
### [ACCESS_WIFI_STATE](https://developer.android.com/reference/android/Manifest.permission#ACCESS_FINE_LOCATION)
|
||||
|
||||
Required to add a WiFi network from a barcode (usually a QR Code).
|
||||
|
||||
Before Android Q, this feature requires using
|
||||
[WifiManager.getConfiguredNetworks][configuredNetworks]
|
||||
which requires `ACCESS_WIFI_STATE`.
|
||||
|
||||
[configuredNetworks]: https://developer.android.com/reference/android/net/wifi/WifiManager#getConfiguredNetworks()
|
||||
[addNetworkSuggestions]: https://developer.android.com/reference/android/net/wifi/WifiManager#addNetworkSuggestions(java.util.List%3Candroid.net.wifi.WifiNetworkSuggestion%3E)
|
||||
|
||||
@@ -12,9 +12,14 @@ library.
|
||||
|
||||
## Screenshots
|
||||
|
||||

|
||||

|
||||

|
||||
<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/screencap-scanning.png"
|
||||
alt="Screenshot Gallery" width="160"/>
|
||||
<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/screencap-scanning-cropped.png"
|
||||
alt="Screenshot Gallery" width="160"/>
|
||||
<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/screencap-decoded.png"
|
||||
alt="Screenshot Theme" width="160"/>
|
||||
<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/screencap-compose-barcode.png"
|
||||
alt="Screenshot Editor" width="160"/>
|
||||
|
||||
## Download
|
||||
|
||||
@@ -59,6 +64,54 @@ ZXing can generate the following barcode formats:
|
||||
* [QR CODE][qr_code]
|
||||
* [UPC A][upc_a]
|
||||
|
||||
## Deep Links and Intents
|
||||
|
||||
### Deep Links
|
||||
|
||||
You can invoke Binary Eye with a web URI intent from anything that can
|
||||
open URIs. There are two options:
|
||||
|
||||
1. [binaryeye://scan](binaryeye://scan)
|
||||
2. [http(s)://markusfisch.de/BinaryEye](http://markusfisch.de/BinaryEye)
|
||||
|
||||
If you want to get the scanned contents, you can add a `ret` query
|
||||
argument with a (URL encoded) URI template. For example:
|
||||
|
||||
[http://markusfisch.de/BinaryEye?ret=http%3A%2F%2Fexample.com%2F%3Fresult%3D{RESULT}](http://markusfisch.de/BinaryEye?ret=http%3A%2F%2Fexample.com%2F%3Fresult%3D{RESULT})
|
||||
|
||||
Supported symbols are:
|
||||
|
||||
* `RESULT` - scanned content
|
||||
* `RESULT_BYTES` - raw result as a hex string
|
||||
* `FORMAT` - barcode format
|
||||
* `META` - the meta data, if available
|
||||
|
||||
### SCAN Intent
|
||||
|
||||
You can also use Binary Eye from other apps by using the
|
||||
`com.google.zxing.client.android.SCAN` Intent with `startActivityForResult()`
|
||||
like this:
|
||||
|
||||
startActivityForResult(
|
||||
Intent("com.google.zxing.client.android.SCAN"),
|
||||
SOME_NUMBER
|
||||
)
|
||||
|
||||
And process the result in `onActivityResult()` of your `Activity`:
|
||||
|
||||
override fun onActivityResult(
|
||||
requestCode: Int,
|
||||
resultCode: Int,
|
||||
data: Intent?
|
||||
) {
|
||||
when (requestCode) {
|
||||
SOME_NUMBER -> if (resultCode == RESULT_OK) {
|
||||
val result = data.getStringExtra("SCAN_RESULT")
|
||||
…
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
## RenderScript
|
||||
|
||||
This app uses [RenderScript][rs] to resize and rotate the camera image.
|
||||
@@ -79,29 +132,6 @@ be thrown and the only option is to restart the app, this time with calling
|
||||
Calling `RenderScript.forceCompat()` means the processing is done in
|
||||
software so you probably don't want to enable it by default.
|
||||
|
||||
### macOS Catalina
|
||||
|
||||
At the time of writing, calling `RenderScript.forceCompat()` in a build made
|
||||
on macOS Catalina with build tools 29.0.3 will crash the app. This is a bug
|
||||
in the build tools for Catalina.
|
||||
|
||||
So *do not* build release builts on macOS Catalina with build tools 29.0.3.
|
||||
|
||||
Building on Linux with 29.0.3 works fine.
|
||||
|
||||
### Side by Side NDK
|
||||
|
||||
At the time of writing, it's important to _not_ have the NDK available when
|
||||
compiling the custom rotation kernel. This will produce a broken build for
|
||||
some ARMv7 devices running Android 6 (e.g. One Plus X, Yotaphone 2, Moto E)
|
||||
while it works for newer Android versions.
|
||||
|
||||
### App Bundle
|
||||
|
||||
At the time of writing, `./gradlew bundleRelease` will produce a broken
|
||||
App Bundle without the necessary RenderScript libraries. This is another
|
||||
bug in the build tools.
|
||||
|
||||
### 2D barcodes
|
||||
|
||||
If you want to fork this and are only interested in reading 2D barcodes
|
||||
|
||||
+22
-7
@@ -9,13 +9,14 @@ android {
|
||||
minSdkVersion 9
|
||||
targetSdkVersion sdk_version
|
||||
|
||||
versionCode 66
|
||||
versionName '1.30.0'
|
||||
versionCode 89
|
||||
versionName '1.46.2'
|
||||
|
||||
// it's recommended to set this value to the lowest API level
|
||||
// able to provide all the functionality
|
||||
// It's recommended to set this value to the lowest API level
|
||||
// able to provide all the functionality.
|
||||
renderscriptTargetApi 24
|
||||
renderscriptSupportModeEnabled true
|
||||
renderscriptSupportModeBlasEnabled true
|
||||
|
||||
testInstrumentationRunner 'android.support.test.runner.AndroidJUnitRunner'
|
||||
}
|
||||
@@ -49,10 +50,22 @@ android {
|
||||
}
|
||||
}
|
||||
|
||||
bundle {
|
||||
language {
|
||||
// To make the app bundle contain all language resources
|
||||
// so the in-app language setting works. Another solution
|
||||
// would be to use the PlayCore API to download language
|
||||
// resources on demand, but it makes much more sense to
|
||||
// simply include the ~60 kb than adding fragile code and
|
||||
// dependencies.
|
||||
enableSplit = false
|
||||
}
|
||||
}
|
||||
|
||||
lintOptions {
|
||||
// because at the time of writing, even the latest version
|
||||
// Because at the time of writing, even the latest version
|
||||
// of RenderScript is raising this error no matter if
|
||||
// renderscriptTargetApi matches targetSdkVersion
|
||||
// renderscriptTargetApi matches targetSdkVersion.
|
||||
disable 'GradleCompatible'
|
||||
}
|
||||
}
|
||||
@@ -70,9 +83,11 @@ dependencies {
|
||||
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"
|
||||
implementation "com.android.support:preference-v7:$support_version"
|
||||
implementation "com.android.support:preference-v14:$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.9.1'
|
||||
implementation 'com.github.markusfisch:ScalingImageView:1.2.0'
|
||||
implementation 'com.github.markusfisch:ScalingImageView:1.3.0'
|
||||
}
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 97 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 79 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 88 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 77 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 52 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 47 KiB |
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
@@ -1,14 +1,15 @@
|
||||
package de.markusfisch.android.binaryeye
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import android.support.test.InstrumentationRegistry
|
||||
import android.support.test.runner.AndroidJUnit4
|
||||
|
||||
import android.support.v8.renderscript.RenderScript
|
||||
import de.markusfisch.android.binaryeye.rs.Preprocessor
|
||||
import de.markusfisch.android.binaryeye.zxing.Zxing
|
||||
|
||||
import org.junit.Assert.fail;
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
import java.util.regex.Pattern
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
@@ -19,22 +20,30 @@ class PreprocessorTest {
|
||||
val assets = InstrumentationRegistry.getInstrumentation()
|
||||
.context.assets
|
||||
val pattern = Pattern.compile(
|
||||
"[0-9]+-([0-9]+)x([0-9]+)-([0-9]+)deg.yuv"
|
||||
"[0-9]+-([0-9]+)deg.jpg"
|
||||
)
|
||||
val files = assets.list("yuv") ?: return
|
||||
for (file in files) {
|
||||
val m = pattern.matcher(file)
|
||||
if (!m.find() || m.groupCount() < 3) {
|
||||
continue
|
||||
val samples = assets.list("samples")
|
||||
checkNotNull(samples) { "no samples found" }
|
||||
for (sample in samples) {
|
||||
val m = pattern.matcher(sample)
|
||||
if (!m.find() || m.groupCount() < 1) {
|
||||
fail("invalid sample: $sample")
|
||||
}
|
||||
val frameWidth = m.group(1) ?: return
|
||||
val frameHeight = m.group(2) ?: return
|
||||
val frameOrientation = m.group(3) ?: return
|
||||
val frameData = assets.open("yuv/$file").readBytes()
|
||||
val preprocessor = Preprocessor(
|
||||
val bitmap = BitmapFactory.decodeStream(
|
||||
assets.open("samples/$sample")
|
||||
)
|
||||
val frameData = getNV21(bitmap)
|
||||
val frameWidth = bitmap.width
|
||||
val frameHeight = bitmap.height
|
||||
val frameOrientation = m.group(1) ?: return
|
||||
val rs = RenderScript.create(
|
||||
InstrumentationRegistry.getTargetContext(),
|
||||
frameWidth.toInt(),
|
||||
frameHeight.toInt()
|
||||
)
|
||||
val preprocessor = Preprocessor(
|
||||
rs,
|
||||
frameWidth,
|
||||
frameHeight,
|
||||
null
|
||||
)
|
||||
val outWidth: Int
|
||||
val outHeight: Int
|
||||
@@ -50,8 +59,50 @@ class PreprocessorTest {
|
||||
}
|
||||
val result = zxing.decode(frameData, outWidth, outHeight)
|
||||
preprocessor.destroy()
|
||||
rs.destroy()
|
||||
|
||||
checkNotNull(result) { "no barcode found in $file" }
|
||||
checkNotNull(result) { "no barcode found in $sample" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getNV21(bitmap: Bitmap): ByteArray {
|
||||
val width = bitmap.width
|
||||
val height = bitmap.height
|
||||
val argb = IntArray(width * height)
|
||||
bitmap.getPixels(argb, 0, width, 0, 0, width, height)
|
||||
return encodeYUV420SP(argb, width, height)
|
||||
}
|
||||
|
||||
private fun encodeYUV420SP(argb: IntArray, width: Int, height: Int): ByteArray {
|
||||
val yuv = ByteArray(ceilIfUneven(width) * ceilIfUneven(height) * 3 / 2)
|
||||
var yIndex = 0
|
||||
var uvIndex = width * height
|
||||
var r: Int
|
||||
var g: Int
|
||||
var b: Int
|
||||
var Y: Int
|
||||
var u: Int
|
||||
var v: Int
|
||||
var index = 0
|
||||
for (y in 0 until height) {
|
||||
for (x in 0 until width) {
|
||||
val pixel = argb[index]
|
||||
r = pixel and 0xff0000 shr 16
|
||||
g = pixel and 0xff00 shr 8
|
||||
b = pixel and 0xff
|
||||
Y = (66 * r + 129 * g + 25 * b + 128 shr 8) + 16
|
||||
u = (-38 * r - 74 * g + 112 * b + 128 shr 8) + 128
|
||||
v = (112 * r - 94 * g - 18 * b + 128 shr 8) + 128
|
||||
yuv[yIndex++] = (if (Y < 0) 0 else if (Y > 255) 255 else Y).toByte()
|
||||
if (y % 2 == 0 && index % 2 == 0) {
|
||||
yuv[uvIndex++] = (if (v < 0) 0 else if (v > 255) 255 else v).toByte()
|
||||
yuv[uvIndex++] = (if (u < 0) 0 else if (u > 255) 255 else u).toByte()
|
||||
}
|
||||
++index
|
||||
}
|
||||
}
|
||||
return yuv
|
||||
}
|
||||
|
||||
private fun ceilIfUneven(n: Int) = if (n and 1 == 1) n + 1 else n
|
||||
|
||||
@@ -5,17 +5,33 @@
|
||||
<supports-screens
|
||||
android:largeScreens="true"
|
||||
android:xlargeScreens="true"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"
|
||||
android:maxSdkVersion="28"/>
|
||||
<uses-permission android:name="android.permission.CAMERA"/>
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-permission android:name="android.permission.VIBRATE"/>
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||
android:maxSdkVersion="28"/>
|
||||
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"
|
||||
android:maxSdkVersion="28"/>
|
||||
<uses-feature android:name="android.hardware.camera"/>
|
||||
<uses-feature android:name="android.hardware.camera.autofocus"/>
|
||||
<uses-sdk tools:overrideLibrary="android.support.v14.preference"/>
|
||||
<queries>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
<data android:scheme="https"/>
|
||||
</intent>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
<data android:scheme="http"/>
|
||||
</intent>
|
||||
</queries>
|
||||
<application
|
||||
tools:ignore="UnusedAttribute"
|
||||
android:name=".app.BinaryEyeApp"
|
||||
android:usesCleartextTraffic="true"
|
||||
android:allowBackup="true"
|
||||
android:fullBackupContent="@xml/backup_rules"
|
||||
android:supportsRtl="true"
|
||||
@@ -51,10 +67,16 @@
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<category android:name="android.intent.category.BROWSABLE"/>
|
||||
<data
|
||||
android:host="www.markusfisch.de"
|
||||
android:pathPrefix="/BinaryEye"
|
||||
android:scheme="http"/>
|
||||
<data android:scheme="http"
|
||||
android:host="markusfisch.de"
|
||||
android:pathPrefix="/BinaryEye"/>
|
||||
<data android:scheme="https" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<category android:name="android.intent.category.BROWSABLE"/>
|
||||
<data android:scheme="binaryeye" android:host="scan"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
@@ -76,6 +98,15 @@
|
||||
android:name=".activity.MainActivity"
|
||||
android:label="@string/app_name"
|
||||
android:exported="true"/>
|
||||
<service
|
||||
android:name=".service.ScanTileService"
|
||||
android:icon="@drawable/ic_action_scan"
|
||||
android:label="@string/scan_code"
|
||||
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
|
||||
<intent-filter>
|
||||
<action android:name="android.service.quicksettings.action.QS_TILE"/>
|
||||
</intent-filter>
|
||||
</service>
|
||||
<provider
|
||||
android:name="android.support.v4.content.FileProvider"
|
||||
android:authorities="${applicationId}.provider"
|
||||
|
||||
@@ -2,8 +2,8 @@ package de.markusfisch.android.binaryeye.actions
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import de.markusfisch.android.binaryeye.app.execShareIntent
|
||||
import de.markusfisch.android.binaryeye.app.parseAndNormalizeUri
|
||||
import de.markusfisch.android.binaryeye.content.execShareIntent
|
||||
import de.markusfisch.android.binaryeye.widget.toast
|
||||
|
||||
interface IAction {
|
||||
@@ -21,7 +21,7 @@ abstract class IntentAction : IAction {
|
||||
val intent = createIntent(context, data) ?: return context.toast(
|
||||
errorMsg
|
||||
)
|
||||
execShareIntent(context, intent)
|
||||
context.execShareIntent(intent)
|
||||
}
|
||||
|
||||
abstract suspend fun createIntent(context: Context, data: ByteArray): Intent?
|
||||
@@ -43,6 +43,6 @@ abstract class SchemeAction : IAction {
|
||||
|
||||
final override suspend fun execute(context: Context, data: ByteArray) {
|
||||
val uri = parseAndNormalizeUri(String(data))
|
||||
execShareIntent(context, Intent(intentAction, uri))
|
||||
context.execShareIntent(Intent(intentAction, uri))
|
||||
}
|
||||
}
|
||||
|
||||
+22
-15
@@ -5,10 +5,9 @@ import android.content.Intent
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.actions.IAction
|
||||
import de.markusfisch.android.binaryeye.app.alertDialog
|
||||
import de.markusfisch.android.binaryeye.app.execShareIntent
|
||||
import de.markusfisch.android.binaryeye.app.parseAndNormalizeUri
|
||||
import de.markusfisch.android.binaryeye.app.prefs
|
||||
import de.markusfisch.android.binaryeye.widget.toast
|
||||
import de.markusfisch.android.binaryeye.content.startIntent
|
||||
import java.net.URLEncoder
|
||||
|
||||
object OpenOrSearchAction : IAction {
|
||||
@@ -18,28 +17,36 @@ object OpenOrSearchAction : IAction {
|
||||
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)
|
||||
view(context, String(data), true)
|
||||
}
|
||||
|
||||
private suspend fun openUri(context: Context, data: String, search: Boolean = true): Intent? {
|
||||
val uri = parseAndNormalizeUri(data)
|
||||
val intent = Intent(Intent.ACTION_VIEW, uri)
|
||||
when {
|
||||
intent.resolveActivity(context.packageManager) != null -> return intent
|
||||
search -> return getSearchIntent(context, data)
|
||||
else -> context.toast(R.string.cannot_resolve_action)
|
||||
private suspend fun view(context: Context, s: String, search: Boolean) {
|
||||
val intent = Intent(Intent.ACTION_VIEW, parseAndNormalizeUri(s))
|
||||
if (!context.startIntent(intent) && search) {
|
||||
openSearch(context, s)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private suspend fun getSearchIntent(context: Context, query: String): Intent? {
|
||||
private suspend fun openSearch(context: Context, query: String) {
|
||||
val defaultSearchUrl = prefs.defaultSearchUrl
|
||||
if (defaultSearchUrl.isNotEmpty()) {
|
||||
view(
|
||||
context,
|
||||
defaultSearchUrl + URLEncoder.encode(query, "utf-8"),
|
||||
false
|
||||
)
|
||||
return
|
||||
}
|
||||
val names = context.resources.getStringArray(
|
||||
R.array.search_engines_names
|
||||
).toMutableList()
|
||||
val urls = context.resources.getStringArray(
|
||||
R.array.search_engines_values
|
||||
).toMutableList()
|
||||
// Remove the "Always ask" entry. The arrays search_engines_*
|
||||
// are used in the preferences too.
|
||||
names.removeFirst()
|
||||
urls.removeFirst()
|
||||
if (prefs.openWithUrl.isNotEmpty()) {
|
||||
names.add(prefs.openWithUrl)
|
||||
urls.add(prefs.openWithUrl)
|
||||
@@ -49,7 +56,7 @@ object OpenOrSearchAction : IAction {
|
||||
setItems(names.toTypedArray()) { _, which ->
|
||||
resume(urls[which] + URLEncoder.encode(query, "utf-8"))
|
||||
}
|
||||
} ?: return null
|
||||
return openUri(context, queryUri, false)
|
||||
} ?: return
|
||||
view(context, queryUri, false)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ object VTypeParser {
|
||||
|
||||
fun parseVType(data: String): String? = vTypeRegex.matchEntire(
|
||||
data
|
||||
)?.groupValues?.get(1)?.toUpperCase(Locale.US)
|
||||
)?.groupValues?.get(1)?.uppercase(Locale.US)
|
||||
|
||||
fun parseMap(
|
||||
data: String
|
||||
@@ -26,7 +26,7 @@ object VTypeParser {
|
||||
it.groupValues[2]
|
||||
)
|
||||
}
|
||||
}.groupBy({ it.first.toUpperCase(Locale.US) }) { it.second }
|
||||
}.groupBy({ it.first.uppercase(Locale.US) }) { it.second }
|
||||
}
|
||||
|
||||
data class VTypeProperty(
|
||||
|
||||
+14
-5
@@ -20,10 +20,19 @@ object VCardAction : IntentAction() {
|
||||
get() = R.string.vcard_failed
|
||||
|
||||
override fun canExecuteOn(data: ByteArray): Boolean {
|
||||
return VTypeParser.parseVType(String(data)) == "VCARD"
|
||||
var stringData = String(data)
|
||||
// Quick & dirty MECARD support: simply make it a VCARD.
|
||||
if (stringData.startsWith("MECARD:")) {
|
||||
stringData = stringData
|
||||
.trim()
|
||||
.replace("MECARD:", "BEGIN:VCARD\n")
|
||||
.replace(";;", "\nEND:VCARD\n")
|
||||
.replace(";", "\n")
|
||||
}
|
||||
return VTypeParser.parseVType(stringData) == "VCARD"
|
||||
}
|
||||
|
||||
override suspend fun createIntent(context: Context, data: ByteArray): Intent? {
|
||||
override suspend fun createIntent(context: Context, data: ByteArray): Intent {
|
||||
val info = VTypeParser.parseMap(String(data))
|
||||
|
||||
return Intent(Intent.ACTION_INSERT_OR_EDIT).apply {
|
||||
@@ -50,7 +59,7 @@ object VCardAction : IntentAction() {
|
||||
}
|
||||
putExtra(
|
||||
extraPhone,
|
||||
phoneProperty.value.toLowerCase(Locale.US).removePrefix("tel:")
|
||||
phoneProperty.value.lowercase(Locale.US).removePrefix("tel:")
|
||||
)
|
||||
}
|
||||
info["EMAIL"]?.forEachIndexed { index, mailProperty ->
|
||||
@@ -142,7 +151,7 @@ object VCardAction : IntentAction() {
|
||||
} ?: this
|
||||
|
||||
private val String.mailType: Int?
|
||||
get() = when (this.toUpperCase(Locale.US)) {
|
||||
get() = when (this.uppercase(Locale.US)) {
|
||||
"HOME" -> ContactsContract.CommonDataKinds.Email.TYPE_HOME
|
||||
"WORK" -> ContactsContract.CommonDataKinds.Email.TYPE_WORK
|
||||
"OTHER" -> ContactsContract.CommonDataKinds.Email.TYPE_OTHER
|
||||
@@ -151,7 +160,7 @@ object VCardAction : IntentAction() {
|
||||
}
|
||||
|
||||
private val String.phoneType: Int?
|
||||
get() = when (this.toUpperCase(Locale.US)) {
|
||||
get() = when (this.uppercase(Locale.US)) {
|
||||
"HOME" -> ContactsContract.CommonDataKinds.Phone.TYPE_HOME
|
||||
"MOBILE", "CELL" -> ContactsContract.CommonDataKinds.Phone.TYPE_MOBILE
|
||||
"WORK" -> ContactsContract.CommonDataKinds.Phone.TYPE_WORK
|
||||
|
||||
+2
-1
@@ -56,7 +56,8 @@ object VEventAction : IntentAction() {
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("SimpleDateFormat") // we definitely don't wan't the local format
|
||||
// We definitely don't wan't the local format.
|
||||
@SuppressLint("SimpleDateFormat")
|
||||
private val dateFormats = listOf(
|
||||
SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"),
|
||||
SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss"),
|
||||
|
||||
+57
-54
@@ -10,6 +10,12 @@ import android.support.annotation.RequiresApi
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import java.util.*
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.Q)
|
||||
fun removeNetworkSuggestions(context: Context): Int =
|
||||
(context.applicationContext.getSystemService(
|
||||
Context.WIFI_SERVICE
|
||||
) as WifiManager).removeNetworkSuggestions(listOf())
|
||||
|
||||
/**
|
||||
* Normal:
|
||||
* WIFI:S:[network SSID];T:<WPA|WEP|nopass|>;P:[network password];H:<true|false|>;;
|
||||
@@ -45,7 +51,7 @@ object WifiConnector {
|
||||
invoke(parsedData.password)
|
||||
}
|
||||
return if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
||||
// WifiConfiguration is deprecated in Android Q
|
||||
// WifiConfiguration is deprecated in Android Q.
|
||||
@Suppress("DEPRECATION")
|
||||
WifiConfiguration().apply(parsedData)
|
||||
} else {
|
||||
@@ -58,7 +64,7 @@ object WifiConnector {
|
||||
Context.WIFI_SERVICE
|
||||
) as WifiManager
|
||||
return if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
||||
// WifiConfiguration is deprecated in Android Q
|
||||
// WifiConfiguration is deprecated in Android Q.
|
||||
@Suppress("DEPRECATION")
|
||||
val wifiConfig = config as WifiConfiguration
|
||||
if (wifiManager.enableWifi() &&
|
||||
@@ -72,7 +78,7 @@ object WifiConnector {
|
||||
} else {
|
||||
val suggestion = (config as WifiNetworkSuggestion.Builder).build()
|
||||
val suggestions = listOf(suggestion)
|
||||
// remove previous conflicting network suggestion
|
||||
// Remove previous conflicting network suggestion.
|
||||
wifiManager.removeNetworkSuggestions(suggestions)
|
||||
val result = wifiManager.addNetworkSuggestions(suggestions)
|
||||
if (result == WifiManager.STATUS_NETWORK_SUGGESTIONS_SUCCESS) {
|
||||
@@ -84,17 +90,17 @@ object WifiConnector {
|
||||
}
|
||||
|
||||
internal fun parseMap(string: String): Map<String, String>? {
|
||||
// normally those codes should have the last semicolon, but many
|
||||
// generators don't add it
|
||||
// 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 Code creators don't escape properly
|
||||
// Should be: ^(.+):((?:[^\\;,":]|\\.)*);$ but allows unescaped , "
|
||||
// and : because many QR Code creators don't escape properly.
|
||||
val pairRegex = """(.+?):((?:[^\\;]|\\.)*);""".toRegex()
|
||||
return wifiRegex.matchEntire(
|
||||
string
|
||||
)?.groupValues?.get(1)?.let { pairs ->
|
||||
pairRegex.findAll(pairs).map { pair ->
|
||||
pair.groupValues[1].toUpperCase(Locale.US) to pair.groupValues[2]
|
||||
pair.groupValues[1].uppercase(Locale.US) to pair.groupValues[2]
|
||||
}.toMap()
|
||||
}
|
||||
}
|
||||
@@ -102,7 +108,7 @@ object WifiConnector {
|
||||
internal class SimpleDataAccessor private constructor(
|
||||
private val inputMap: Map<String, String>
|
||||
) {
|
||||
// this should be private but because of testing it isn't possible
|
||||
// This should be private but because of testing it isn't possible.
|
||||
internal val ssid: String
|
||||
get() = inputMap.getValue("S").unescape
|
||||
internal val securityType: String
|
||||
@@ -112,7 +118,7 @@ object WifiConnector {
|
||||
internal val hidden: Boolean
|
||||
get() = inputMap["H"]?.unescape == "true"
|
||||
internal val anonymousIdentity: String
|
||||
get() = inputMap["AI"]?.unescape ?: ""
|
||||
get() = inputMap["AI"]?.unescape ?: inputMap["A"]?.unescape ?: ""
|
||||
internal val identity: String
|
||||
get() = inputMap["I"]?.unescape ?: ""
|
||||
internal val eapMethod: Int?
|
||||
@@ -164,7 +170,7 @@ object WifiConnector {
|
||||
): WifiNetworkSuggestion.Builder? {
|
||||
fun WifiNetworkSuggestion.Builder.applyCommon(
|
||||
data: SimpleDataAccessor
|
||||
): WifiNetworkSuggestion.Builder? {
|
||||
): WifiNetworkSuggestion.Builder {
|
||||
setSsid(data.ssid)
|
||||
return this
|
||||
}
|
||||
@@ -172,47 +178,44 @@ object WifiConnector {
|
||||
fun WifiNetworkSuggestion.Builder.applySecurity(
|
||||
data: SimpleDataAccessor
|
||||
): WifiNetworkSuggestion.Builder? {
|
||||
when (data.securityType) {
|
||||
"WPA", "WPA2" -> {
|
||||
data.password?.let {
|
||||
setWpa2Passphrase(it)
|
||||
try {
|
||||
when (data.securityType) {
|
||||
"WPA", "WPA2" -> {
|
||||
data.password?.let { setWpa2Passphrase(it) }
|
||||
}
|
||||
"WPA2-EAP" -> {
|
||||
setWpa2EnterpriseConfig(WifiEnterpriseConfig().apply {
|
||||
identity = data.identity
|
||||
anonymousIdentity = data.anonymousIdentity
|
||||
password = data.password
|
||||
data.eapMethod?.let { eapMethod = it }
|
||||
data.phase2Method?.let { phase2Method = it }
|
||||
})
|
||||
}
|
||||
"WPA3" -> {
|
||||
data.password?.let { setWpa3Passphrase(it) }
|
||||
}
|
||||
"WPA3-EAP" -> {
|
||||
setWpa3EnterpriseConfig(WifiEnterpriseConfig().apply {
|
||||
identity = data.identity
|
||||
anonymousIdentity = data.anonymousIdentity
|
||||
password = data.password
|
||||
data.eapMethod?.let { eapMethod = it }
|
||||
data.phase2Method?.let { phase2Method = it }
|
||||
})
|
||||
}
|
||||
}
|
||||
"WPA2-EAP" -> {
|
||||
data.password?.let {
|
||||
setWpa2Passphrase(it)
|
||||
}
|
||||
setWpa2EnterpriseConfig(WifiEnterpriseConfig().apply {
|
||||
identity = data.identity
|
||||
anonymousIdentity = data.anonymousIdentity
|
||||
password = data.password
|
||||
data.eapMethod?.let { eapMethod = it }
|
||||
data.phase2Method?.let { phase2Method = it }
|
||||
})
|
||||
}
|
||||
"WPA3" -> {
|
||||
data.password?.let {
|
||||
setWpa3Passphrase(it)
|
||||
}
|
||||
}
|
||||
"WPA3-EAP" -> {
|
||||
data.password?.let { setWpa3Passphrase(it) }
|
||||
setWpa3EnterpriseConfig(WifiEnterpriseConfig().apply {
|
||||
identity = data.identity
|
||||
anonymousIdentity = data.anonymousIdentity
|
||||
password = data.password
|
||||
data.eapMethod?.let { eapMethod = it }
|
||||
data.phase2Method?.let { phase2Method = it }
|
||||
})
|
||||
}
|
||||
} catch (e: IllegalArgumentException) {
|
||||
// Don't accept passphrases that aren't ASCII encodeable.
|
||||
return null
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
return this.applyCommon(data)?.applySecurity(data)
|
||||
return this.applyCommon(data).applySecurity(data)
|
||||
}
|
||||
|
||||
// WifiConfiguration is deprecated in Android Q
|
||||
// WifiConfiguration is deprecated in Android Q.
|
||||
@Suppress("DEPRECATION")
|
||||
private fun WifiConfiguration.apply(
|
||||
data: SimpleDataAccessor
|
||||
@@ -222,7 +225,7 @@ object WifiConnector {
|
||||
|
||||
fun WifiConfiguration.applyCommon(
|
||||
data: SimpleDataAccessor
|
||||
): WifiConfiguration? {
|
||||
): WifiConfiguration {
|
||||
allowedAuthAlgorithms.clear()
|
||||
allowedGroupCiphers.clear()
|
||||
allowedKeyManagement.clear()
|
||||
@@ -274,7 +277,7 @@ object WifiConnector {
|
||||
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
|
||||
@Suppress("DEPRECATION") // TKIP is insecure and has bad performance.
|
||||
allowedPairwiseCiphers.set(WifiConfiguration.PairwiseCipher.TKIP)
|
||||
allowedPairwiseCiphers.set(WifiConfiguration.PairwiseCipher.CCMP)
|
||||
allowedGroupCiphers.set(WifiConfiguration.GroupCipher.TKIP)
|
||||
@@ -292,7 +295,7 @@ object WifiConnector {
|
||||
return this
|
||||
}
|
||||
|
||||
return this.applyCommon(data)?.applySecurity(data)
|
||||
return this.applyCommon(data).applySecurity(data)
|
||||
}
|
||||
|
||||
private inline fun <T> requireSdk(version: Int, block: () -> T): T? {
|
||||
@@ -300,8 +303,8 @@ object WifiConnector {
|
||||
}
|
||||
}
|
||||
|
||||
// keep possibility of wrongly unescaped \ by explicitly searching
|
||||
// for special chars
|
||||
// Keep possibility of wrongly unescaped \ by explicitly searching
|
||||
// for special chars.
|
||||
private val escapedRegex = """\\([\\;,":])""".toRegex()
|
||||
private val String.unescape: String
|
||||
get() = this.replace(escapedRegex) { escaped ->
|
||||
@@ -327,12 +330,12 @@ private fun String.isHex() = length == 64 && matches(hexRegex)
|
||||
|
||||
private fun WifiManager.enableWifi(): Boolean {
|
||||
// setWifiEnabled() will always return false for Android Q
|
||||
// because Q doesn't allow apps to enable/disable Wi-Fi anymore
|
||||
// because Q doesn't allow apps to enable/disable Wi-Fi anymore.
|
||||
@Suppress("DEPRECATION")
|
||||
return isWifiEnabled || setWifiEnabled(true)
|
||||
}
|
||||
|
||||
// WifiConfiguration is deprecated in Android Q
|
||||
// WifiConfiguration is deprecated in Android Q.
|
||||
@Suppress("DEPRECATION")
|
||||
private fun WifiManager.removeOldNetwork(
|
||||
wifiConfig: WifiConfiguration
|
||||
@@ -345,13 +348,13 @@ private fun WifiManager.removeOldNetwork(
|
||||
removeNetwork(it)
|
||||
}
|
||||
} catch (e: SecurityException) {
|
||||
// the user didn't allow ACCESS_FINE_LOCATION which is
|
||||
// required to access configuredNetworks and that's fine
|
||||
// The user didn't allow ACCESS_FINE_LOCATION which is
|
||||
// required to access configuredNetworks and that's fine.
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// WifiConfiguration is deprecated in Android Q
|
||||
// WifiConfiguration is deprecated in Android Q.
|
||||
@Suppress("DEPRECATION")
|
||||
private fun WifiManager.enableNewNetwork(
|
||||
wifiConfig: WifiConfiguration
|
||||
|
||||
@@ -4,15 +4,20 @@ import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.graphics.Matrix
|
||||
import android.graphics.Rect
|
||||
import android.graphics.RectF
|
||||
import android.hardware.Camera
|
||||
import android.media.AudioManager
|
||||
import android.media.ToneGenerator
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.os.Vibrator
|
||||
import android.support.design.widget.FloatingActionButton
|
||||
import android.support.v7.app.AppCompatActivity
|
||||
import android.support.v7.widget.Toolbar
|
||||
import android.support.v8.renderscript.RSRuntimeException
|
||||
import android.support.v8.renderscript.RenderScript
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.MotionEvent
|
||||
@@ -20,50 +25,62 @@ import android.view.View
|
||||
import android.widget.SeekBar
|
||||
import com.google.zxing.Result
|
||||
import com.google.zxing.ResultMetadataType
|
||||
import com.google.zxing.ResultPointCallback
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.app.*
|
||||
import de.markusfisch.android.binaryeye.data.Scan
|
||||
import de.markusfisch.android.binaryeye.graphics.Mapping
|
||||
import de.markusfisch.android.binaryeye.graphics.frameToView
|
||||
import de.markusfisch.android.binaryeye.graphics.isPortrait
|
||||
import de.markusfisch.android.binaryeye.content.copyToClipboard
|
||||
import de.markusfisch.android.binaryeye.content.execShareIntent
|
||||
import de.markusfisch.android.binaryeye.database.Scan
|
||||
import de.markusfisch.android.binaryeye.graphics.getFrameToViewMatrix
|
||||
import de.markusfisch.android.binaryeye.graphics.map
|
||||
import de.markusfisch.android.binaryeye.net.sendAsync
|
||||
import de.markusfisch.android.binaryeye.os.error
|
||||
import de.markusfisch.android.binaryeye.os.vibrate
|
||||
import de.markusfisch.android.binaryeye.rs.Preprocessor
|
||||
import de.markusfisch.android.binaryeye.view.initSystemBars
|
||||
import de.markusfisch.android.binaryeye.view.setPaddingFromWindowInsets
|
||||
import de.markusfisch.android.binaryeye.widget.DetectorView
|
||||
import de.markusfisch.android.binaryeye.widget.toast
|
||||
import de.markusfisch.android.binaryeye.zxing.Zxing
|
||||
import de.markusfisch.android.cameraview.widget.CameraView
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import java.net.URLEncoder
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
class CameraActivity : AppCompatActivity() {
|
||||
private val zxing = Zxing(ResultPointCallback { point ->
|
||||
point?.let {
|
||||
getMapping()?.map(it)?.let {
|
||||
detectorView.post {
|
||||
detectorView.mark(listOf(it))
|
||||
}
|
||||
}
|
||||
private val zxing = Zxing { point ->
|
||||
point ?: return@Zxing
|
||||
val matrix = getMappingMatrix() ?: return@Zxing
|
||||
detectorView.post {
|
||||
detectorView.update(
|
||||
matrix.map(
|
||||
arrayOf(point),
|
||||
detectorView.coordinates
|
||||
)
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private lateinit var rs: RenderScript
|
||||
private lateinit var vibrator: Vibrator
|
||||
private lateinit var cameraView: CameraView
|
||||
private lateinit var detectorView: DetectorView
|
||||
private lateinit var zoomBar: SeekBar
|
||||
private lateinit var flashFab: View
|
||||
private lateinit var flashFab: FloatingActionButton
|
||||
|
||||
private var preprocessor: Preprocessor? = null
|
||||
private var nativeMapping: Mapping? = null
|
||||
private var rotatedMapping: Mapping? = null
|
||||
private var nativeMappingMatrix: Matrix? = null
|
||||
private var rotatedMappingMatrix: Matrix? = null
|
||||
private var recreatePreprocessor = false
|
||||
private var rotate = false
|
||||
private var invert = false
|
||||
private var flash = false
|
||||
private var decoding = true
|
||||
private var returnResult = false
|
||||
private var returnUrlTemplate: String? = null
|
||||
private var frontFacing = false
|
||||
private var bulkMode = prefs.bulkMode
|
||||
private var ignoreNext: String? = null
|
||||
private var fallbackBuffer: IntArray? = null
|
||||
|
||||
override fun onRequestPermissionsResult(
|
||||
@@ -72,7 +89,7 @@ class CameraActivity : AppCompatActivity() {
|
||||
grantResults: IntArray
|
||||
) {
|
||||
when (requestCode) {
|
||||
REQUEST_CAMERA -> if (grantResults.isNotEmpty() &&
|
||||
PERMISSION_CAMERA -> if (grantResults.isNotEmpty() &&
|
||||
grantResults[0] != PackageManager.PERMISSION_GRANTED
|
||||
) {
|
||||
toast(R.string.no_camera_no_fun)
|
||||
@@ -98,10 +115,20 @@ class CameraActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
override fun attachBaseContext(base: Context?) {
|
||||
base?.applyLocale(prefs.customLocale)
|
||||
super.attachBaseContext(base)
|
||||
}
|
||||
|
||||
override fun onCreate(state: Bundle?) {
|
||||
super.onCreate(state)
|
||||
setContentView(R.layout.activity_camera)
|
||||
|
||||
// Necessary to get the right translation after setting a
|
||||
// custom locale.
|
||||
setTitle(R.string.scan_code)
|
||||
|
||||
rs = RenderScript.create(this)
|
||||
vibrator = getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
|
||||
|
||||
initSystemBars(this)
|
||||
@@ -110,13 +137,11 @@ class CameraActivity : AppCompatActivity() {
|
||||
cameraView = findViewById(R.id.camera_view) as CameraView
|
||||
detectorView = findViewById(R.id.detector_view) as DetectorView
|
||||
zoomBar = findViewById(R.id.zoom) as SeekBar
|
||||
flashFab = findViewById(R.id.flash)
|
||||
flashFab.setOnClickListener { toggleTorchMode() }
|
||||
flashFab = findViewById(R.id.flash) as FloatingActionButton
|
||||
|
||||
initCameraView()
|
||||
initZoomBar()
|
||||
restoreZoom()
|
||||
detectorView.updateRoi = { recreatePreprocessor = true }
|
||||
initDetectorView()
|
||||
|
||||
if (intent?.action == Intent.ACTION_SEND &&
|
||||
intent.type == "text/plain"
|
||||
@@ -130,18 +155,39 @@ class CameraActivity : AppCompatActivity() {
|
||||
resetPreProcessor()
|
||||
fallbackBuffer = null
|
||||
saveZoom()
|
||||
detectorView.saveCropHandlePos()
|
||||
rs.destroy()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
System.gc()
|
||||
zxing.updateHints(prefs.tryHarder)
|
||||
returnResult = "com.google.zxing.client.android.SCAN" == intent.action
|
||||
if (hasCameraPermission(this, REQUEST_CAMERA)) {
|
||||
zxing.updateHints(
|
||||
prefs.tryHarder,
|
||||
prefs.barcodeFormats
|
||||
)
|
||||
if (prefs.bulkMode && bulkMode != prefs.bulkMode) {
|
||||
bulkMode = prefs.bulkMode
|
||||
invalidateOptionsMenu()
|
||||
ignoreNext = null
|
||||
}
|
||||
setReturnTarget(intent)
|
||||
if (hasCameraPermission(this)) {
|
||||
openCamera()
|
||||
}
|
||||
}
|
||||
|
||||
private fun setReturnTarget(intent: Intent?) {
|
||||
when {
|
||||
intent?.action == "com.google.zxing.client.android.SCAN" -> {
|
||||
returnResult = true
|
||||
}
|
||||
isReturnUrl(intent) -> intent?.data?.let {
|
||||
returnUrlTemplate = it.getQueryParameter("ret")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun resetPreProcessor() {
|
||||
preprocessor?.destroy()
|
||||
preprocessor = null
|
||||
@@ -167,7 +213,7 @@ class CameraActivity : AppCompatActivity() {
|
||||
|
||||
private fun closeCamera() {
|
||||
cameraView.close()
|
||||
// closing the camera will also shut off the flash
|
||||
// Closing the camera will also shut off the flash.
|
||||
flash = false
|
||||
}
|
||||
|
||||
@@ -176,24 +222,36 @@ class CameraActivity : AppCompatActivity() {
|
||||
zoomBar.max = savedState.getInt(ZOOM_MAX)
|
||||
zoomBar.progress = savedState.getInt(ZOOM_LEVEL)
|
||||
frontFacing = savedState.getBoolean(FRONT_FACING)
|
||||
bulkMode = savedState.getBoolean(BULK_MODE)
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
outState.putInt(ZOOM_MAX, zoomBar.max)
|
||||
outState.putInt(ZOOM_LEVEL, zoomBar.progress)
|
||||
outState.putBoolean(FRONT_FACING, frontFacing)
|
||||
outState.putBoolean(BULK_MODE, bulkMode)
|
||||
super.onSaveInstanceState(outState)
|
||||
}
|
||||
|
||||
override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
|
||||
// Always give crop handle precedence over other controls
|
||||
// because it can easily overlap and would then be inaccessible.
|
||||
if (detectorView.onTouchEvent(ev)) {
|
||||
return true
|
||||
}
|
||||
return super.dispatchTouchEvent(ev)
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||
menuInflater.inflate(R.menu.activity_camera, menu)
|
||||
menu.findItem(R.id.bulk_mode).isChecked = bulkMode
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
return when (item.itemId) {
|
||||
R.id.create -> {
|
||||
startActivity(MainActivity.getEncodeIntent(this))
|
||||
createBarcode()
|
||||
true
|
||||
}
|
||||
R.id.history -> {
|
||||
@@ -216,6 +274,12 @@ class CameraActivity : AppCompatActivity() {
|
||||
switchCamera()
|
||||
true
|
||||
}
|
||||
R.id.bulk_mode -> {
|
||||
bulkMode = bulkMode xor true
|
||||
item.isChecked = bulkMode
|
||||
ignoreNext = null
|
||||
true
|
||||
}
|
||||
R.id.preferences -> {
|
||||
startActivity(MainActivity.getPreferencesIntent(this))
|
||||
true
|
||||
@@ -228,6 +292,10 @@ class CameraActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun createBarcode() {
|
||||
startActivity(MainActivity.getEncodeIntent(this))
|
||||
}
|
||||
|
||||
private fun switchCamera() {
|
||||
closeCamera()
|
||||
frontFacing = frontFacing xor true
|
||||
@@ -239,11 +307,7 @@ class CameraActivity : AppCompatActivity() {
|
||||
Intent.ACTION_VIEW,
|
||||
Uri.parse(getString(R.string.project_url))
|
||||
)
|
||||
if (intent.resolveActivity(packageManager) != null) {
|
||||
startActivity(intent)
|
||||
} else {
|
||||
toast(R.string.project_url)
|
||||
}
|
||||
execShareIntent(intent)
|
||||
}
|
||||
|
||||
private fun handleSendText(intent: Intent) {
|
||||
@@ -285,8 +349,8 @@ class CameraActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
MotionEvent.ACTION_UP -> {
|
||||
// stop calling focusTo() as soon as it returns false
|
||||
// to avoid throwing and catching future exceptions
|
||||
// Stop calling focusTo() as soon as it returns false
|
||||
// to avoid throwing and catching future exceptions.
|
||||
if (focus) {
|
||||
focus = cameraView.focusTo(v, event.x, event.y)
|
||||
if (focus) {
|
||||
@@ -334,14 +398,15 @@ class CameraActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
override fun onCameraReady(camera: Camera) {
|
||||
// reset preprocessor to make sure it always fits the current
|
||||
// frame orientation; important for landscape to landscape
|
||||
// orientation changes
|
||||
// Reset preprocessor to make sure it always fits the current
|
||||
// frame orientation. Important for landscape to landscape
|
||||
// orientation changes.
|
||||
resetPreProcessor()
|
||||
val frameWidth = cameraView.frameWidth
|
||||
val frameHeight = cameraView.frameHeight
|
||||
val frameOrientation = cameraView.frameOrientation
|
||||
var decoding = true
|
||||
ignoreNext = null
|
||||
decoding = true
|
||||
camera.setPreviewCallback { frameData, _ ->
|
||||
if (decoding) {
|
||||
decodeFrame(
|
||||
@@ -350,8 +415,10 @@ class CameraActivity : AppCompatActivity() {
|
||||
frameHeight,
|
||||
frameOrientation
|
||||
)?.let { result ->
|
||||
postResult(result)
|
||||
decoding = false
|
||||
if (result.text != ignoreNext) {
|
||||
postResult(result)
|
||||
decoding = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -381,6 +448,7 @@ class CameraActivity : AppCompatActivity() {
|
||||
|
||||
override fun onStopTrackingTouch(seekBar: SeekBar) {}
|
||||
})
|
||||
restoreZoom()
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
@@ -392,7 +460,7 @@ class CameraActivity : AppCompatActivity() {
|
||||
params.zoom = zoom
|
||||
camera.parameters = params
|
||||
} catch (e: RuntimeException) {
|
||||
// ignore; there's nothing we can do
|
||||
// Ignore. There's nothing we can do.
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -412,11 +480,25 @@ class CameraActivity : AppCompatActivity() {
|
||||
)
|
||||
}
|
||||
|
||||
private fun updateFlashFab(available: Boolean) {
|
||||
flashFab.visibility = if (available) {
|
||||
View.GONE
|
||||
private fun initDetectorView() {
|
||||
detectorView.onRoiChange = {
|
||||
decoding = false
|
||||
}
|
||||
detectorView.onRoiChanged = {
|
||||
decoding = true
|
||||
recreatePreprocessor = true
|
||||
}
|
||||
detectorView.setPaddingFromWindowInsets()
|
||||
detectorView.restoreCropHandlePos()
|
||||
}
|
||||
|
||||
private fun updateFlashFab(unavailable: Boolean) {
|
||||
if (unavailable) {
|
||||
flashFab.setImageResource(R.drawable.ic_action_create)
|
||||
flashFab.setOnClickListener { createBarcode() }
|
||||
} else {
|
||||
View.VISIBLE
|
||||
flashFab.setImageResource(R.drawable.ic_action_flash)
|
||||
flashFab.setOnClickListener { toggleTorchMode() }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -434,7 +516,7 @@ class CameraActivity : AppCompatActivity() {
|
||||
camera.parameters = parameters
|
||||
flash = flash xor true
|
||||
} catch (e: RuntimeException) {
|
||||
// ignore; there's nothing we can do
|
||||
// Ignore. There's nothing we can do.
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -478,9 +560,9 @@ class CameraActivity : AppCompatActivity() {
|
||||
zxing.decode(frameData, w, h, invert)
|
||||
} catch (e: RSRuntimeException) {
|
||||
prefs.forceCompat = prefs.forceCompat xor true
|
||||
// now the only option is to restart the app because
|
||||
// Now the only option is to restart the app because
|
||||
// RenderScript.forceCompat() needs to be called before
|
||||
// RenderScript is initialized
|
||||
// RenderScript is initialized.
|
||||
restartApp(this)
|
||||
null
|
||||
}
|
||||
@@ -497,7 +579,7 @@ class CameraActivity : AppCompatActivity() {
|
||||
frameOrientation
|
||||
)
|
||||
val pp = Preprocessor(
|
||||
this,
|
||||
rs,
|
||||
frameWidth,
|
||||
frameHeight,
|
||||
frameRoi
|
||||
@@ -507,13 +589,13 @@ class CameraActivity : AppCompatActivity() {
|
||||
} else {
|
||||
cameraView.previewRect
|
||||
}
|
||||
nativeMapping = frameToView(
|
||||
nativeMappingMatrix = getFrameToViewMatrix(
|
||||
pp.outWidth,
|
||||
pp.outHeight,
|
||||
frameOrientation,
|
||||
viewRect
|
||||
)
|
||||
rotatedMapping = frameToView(
|
||||
rotatedMappingMatrix = getFrameToViewMatrix(
|
||||
pp.outHeight,
|
||||
pp.outWidth,
|
||||
(frameOrientation - 90 + 360) % 360,
|
||||
@@ -534,14 +616,19 @@ class CameraActivity : AppCompatActivity() {
|
||||
) {
|
||||
null
|
||||
} else {
|
||||
// map ROI in detectorView to cameraView because cameraView may
|
||||
// be larger than the detectorView
|
||||
// Map ROI in detectorView to cameraView.
|
||||
val previewRect = cameraView.previewRect
|
||||
// previewRect may be larger than the screen (and thus as the
|
||||
// detectorView) in which case its left and/or top coordinate
|
||||
// will be negative which must then be clamped to the
|
||||
// screen/DetectorView.
|
||||
val previewLeft = max(0, previewRect.left)
|
||||
val previewTop = max(0, previewRect.top)
|
||||
val previewRoi = Rect(
|
||||
previewRect.left + viewRoi.left,
|
||||
previewRect.top + viewRoi.top,
|
||||
previewRect.left + viewRoi.right,
|
||||
previewRect.top + viewRoi.bottom
|
||||
previewLeft + viewRoi.left,
|
||||
previewTop + viewRoi.top,
|
||||
previewLeft + viewRoi.right,
|
||||
previewTop + viewRoi.bottom
|
||||
)
|
||||
val previewRectWidth = previewRect.width().toFloat()
|
||||
val previewRectHeight = previewRect.height().toFloat()
|
||||
@@ -551,69 +638,127 @@ class CameraActivity : AppCompatActivity() {
|
||||
previewRoi.right.toFloat() / previewRectWidth,
|
||||
previewRoi.bottom.toFloat() / previewRectHeight
|
||||
)
|
||||
// since the ROI is always centered and symmetric, we don't
|
||||
// need to distinguish between 0 and 180 or 90 and 270 degree
|
||||
if (isPortrait(frameOrientation)) {
|
||||
Rect(
|
||||
(normalizedRoi.top * frameWidth.toFloat()).roundToInt(),
|
||||
(normalizedRoi.left * frameHeight.toFloat()).roundToInt(),
|
||||
(normalizedRoi.bottom * frameWidth.toFloat()).roundToInt(),
|
||||
(normalizedRoi.right * frameHeight.toFloat()).roundToInt()
|
||||
// Since the ROI is always centered and symmetric, we don't
|
||||
// need to distinguish between 0 and 180 or 90 and 270 degree.
|
||||
val errectedRoi = if (isPortrait(frameOrientation)) {
|
||||
RectF(
|
||||
normalizedRoi.top,
|
||||
normalizedRoi.left,
|
||||
normalizedRoi.bottom,
|
||||
normalizedRoi.right
|
||||
)
|
||||
} else {
|
||||
Rect(
|
||||
(normalizedRoi.left * frameWidth.toFloat()).roundToInt(),
|
||||
(normalizedRoi.top * frameHeight.toFloat()).roundToInt(),
|
||||
(normalizedRoi.right * frameWidth.toFloat()).roundToInt(),
|
||||
(normalizedRoi.bottom * frameHeight.toFloat()).roundToInt()
|
||||
)
|
||||
normalizedRoi
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun postResult(result: Result) {
|
||||
// get mapping for the current rotate value
|
||||
val mapping = getMapping()
|
||||
cameraView.post {
|
||||
val rp = result.resultPoints
|
||||
if (mapping != null && rp != null && rp.isNotEmpty()) {
|
||||
detectorView.mark(mapping.map(rp))
|
||||
}
|
||||
vibrator.vibrate()
|
||||
showResult(
|
||||
this@CameraActivity,
|
||||
result,
|
||||
returnResult
|
||||
Rect(
|
||||
(errectedRoi.left * frameWidth.toFloat()).roundToInt(),
|
||||
(errectedRoi.top * frameHeight.toFloat()).roundToInt(),
|
||||
(errectedRoi.right * frameWidth.toFloat()).roundToInt(),
|
||||
(errectedRoi.bottom * frameHeight.toFloat()).roundToInt()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getMapping() = if (rotate) rotatedMapping else nativeMapping
|
||||
private fun postResult(result: Result) {
|
||||
// Get matrix for the current rotate value.
|
||||
val matrix = getMappingMatrix()
|
||||
val resultPoints = result.resultPoints
|
||||
if (matrix == null ||
|
||||
resultPoints == null ||
|
||||
resultPoints.isEmpty()
|
||||
) {
|
||||
return
|
||||
}
|
||||
cameraView.post {
|
||||
detectorView.update(
|
||||
matrix.map(
|
||||
resultPoints,
|
||||
detectorView.coordinates
|
||||
)
|
||||
)
|
||||
vibrator.vibrate()
|
||||
val returnUri = returnUrlTemplate?.let {
|
||||
try {
|
||||
completeUrl(it, result)
|
||||
} catch (e: Exception) {
|
||||
e.message?.let { message ->
|
||||
toast(message)
|
||||
}
|
||||
null
|
||||
}
|
||||
}
|
||||
when {
|
||||
returnResult -> {
|
||||
setResult(Activity.RESULT_OK, getReturnIntent(result))
|
||||
finish()
|
||||
}
|
||||
returnUri != null -> execShareIntent(
|
||||
Intent(Intent.ACTION_VIEW, returnUri)
|
||||
)
|
||||
else -> showResult(
|
||||
this@CameraActivity,
|
||||
result,
|
||||
vibrator,
|
||||
bulkMode
|
||||
)
|
||||
}
|
||||
if (bulkMode) {
|
||||
ignoreNext = result.text
|
||||
if (prefs.showToastInBulkMode) {
|
||||
toast(result.text)
|
||||
}
|
||||
detectorView.postDelayed({
|
||||
decoding = true
|
||||
}, prefs.bulkModeDelay.toLong())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getMappingMatrix() = if (rotate) {
|
||||
rotatedMappingMatrix
|
||||
} else {
|
||||
nativeMappingMatrix
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val REQUEST_CAMERA = 1
|
||||
private const val PICK_FILE_RESULT_CODE = 1
|
||||
private const val ZOOM_MAX = "zoom_max"
|
||||
private const val ZOOM_LEVEL = "zoom_level"
|
||||
private const val FRONT_FACING = "front_facing"
|
||||
private const val BULK_MODE = "bulk_mode"
|
||||
}
|
||||
}
|
||||
|
||||
fun showResult(
|
||||
activity: Activity,
|
||||
result: Result,
|
||||
isResult: Boolean = false
|
||||
vibrator: Vibrator,
|
||||
bulkMode: Boolean = false,
|
||||
) {
|
||||
if (prefs.copyImmediately) {
|
||||
activity.copyToClipboard(result.text)
|
||||
}
|
||||
val scan = Scan(result)
|
||||
if (isResult) {
|
||||
activity.setResult(Activity.RESULT_OK, getReturnIntent(result))
|
||||
activity.finish()
|
||||
} else {
|
||||
if (prefs.useHistory) {
|
||||
GlobalScope.launch {
|
||||
db.insertScan(scan)
|
||||
if (prefs.sendScanUrl.isNotEmpty()) {
|
||||
scan.sendAsync(
|
||||
prefs.sendScanUrl,
|
||||
prefs.sendScanType
|
||||
) { code, body ->
|
||||
if (code == null || code < 200 || code > 299) {
|
||||
vibrator.error()
|
||||
beepBeepBeep()
|
||||
}
|
||||
if (body != null && body.isNotEmpty()) {
|
||||
activity.toast(body)
|
||||
} else if (code == null || code > 299) {
|
||||
activity.toast(R.string.background_request_failed)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (prefs.useHistory) {
|
||||
scan.id = db.insertScan(scan)
|
||||
}
|
||||
if (!bulkMode) {
|
||||
activity.startActivity(
|
||||
MainActivity.getDecodeIntent(activity, scan)
|
||||
)
|
||||
@@ -660,3 +805,44 @@ fun getReturnIntent(result: Result): Intent {
|
||||
}
|
||||
return intent
|
||||
}
|
||||
|
||||
private fun isReturnUrl(intent: Intent?): Boolean {
|
||||
val dataString = intent?.dataString
|
||||
return listOf(
|
||||
"binaryeye://scan",
|
||||
"http://markusfisch.de/BinaryEye",
|
||||
"https://markusfisch.de/BinaryEye"
|
||||
).firstOrNull {
|
||||
val result = dataString?.startsWith(it) == true
|
||||
result
|
||||
} != null
|
||||
}
|
||||
|
||||
private fun completeUrl(urlTemplate: String, result: Result) = Uri.parse(
|
||||
urlTemplate
|
||||
.replace("{RESULT}", URLEncoder.encode(result.text, "UTF-8"))
|
||||
.replace("{RESULT_BYTES}", result.rawBytes?.toHexString() ?: "")
|
||||
.replace(
|
||||
"{FORMAT}", URLEncoder.encode(
|
||||
result.barcodeFormat.toString(), "UTF-8"
|
||||
)
|
||||
)
|
||||
.replace(
|
||||
"{META}", URLEncoder.encode(
|
||||
result.resultMetadata?.toString() ?: "", "UTF-8"
|
||||
)
|
||||
)
|
||||
// And support {CODE} from the old ZXing app, too.
|
||||
.replace("{CODE}", URLEncoder.encode(result.text, "UTF-8"))
|
||||
)
|
||||
|
||||
private fun beepBeepBeep() {
|
||||
ToneGenerator(AudioManager.STREAM_ALARM, 100).startTone(
|
||||
ToneGenerator.TONE_CDMA_CONFIRM,
|
||||
1000
|
||||
)
|
||||
}
|
||||
|
||||
private fun isPortrait(
|
||||
orientation: Int
|
||||
) = orientation == 90 || orientation == 270
|
||||
|
||||
@@ -2,23 +2,39 @@ package de.markusfisch.android.binaryeye.activity
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.support.v4.app.Fragment
|
||||
import android.support.v7.app.AppCompatActivity
|
||||
import android.support.v7.widget.Toolbar
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.app.colorSystemAndToolBars
|
||||
import de.markusfisch.android.binaryeye.app.initSystemBars
|
||||
import de.markusfisch.android.binaryeye.app.setFragment
|
||||
import de.markusfisch.android.binaryeye.data.Scan
|
||||
import de.markusfisch.android.binaryeye.app.*
|
||||
import de.markusfisch.android.binaryeye.database.Scan
|
||||
import de.markusfisch.android.binaryeye.fragment.DecodeFragment
|
||||
import de.markusfisch.android.binaryeye.fragment.EncodeFragment
|
||||
import de.markusfisch.android.binaryeye.fragment.HistoryFragment
|
||||
import de.markusfisch.android.binaryeye.fragment.PreferencesFragment
|
||||
import de.markusfisch.android.binaryeye.view.colorSystemAndToolBars
|
||||
import de.markusfisch.android.binaryeye.view.initSystemBars
|
||||
import de.markusfisch.android.binaryeye.view.recordToolbarHeight
|
||||
|
||||
class MainActivity : AppCompatActivity() {
|
||||
override fun onRequestPermissionsResult(
|
||||
requestCode: Int,
|
||||
permissions: Array<String>,
|
||||
grantResults: IntArray
|
||||
) {
|
||||
when (requestCode) {
|
||||
PERMISSION_LOCATION, PERMISSION_WRITE -> if (grantResults.isNotEmpty() &&
|
||||
grantResults[0] == PackageManager.PERMISSION_GRANTED
|
||||
) {
|
||||
permissionGrantedCallback?.invoke()
|
||||
permissionGrantedCallback = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSupportNavigateUp(): Boolean {
|
||||
val fm = supportFragmentManager
|
||||
if (fm != null && fm.backStackEntryCount > 0) {
|
||||
@@ -29,6 +45,11 @@ class MainActivity : AppCompatActivity() {
|
||||
return true
|
||||
}
|
||||
|
||||
override fun attachBaseContext(base: Context?) {
|
||||
base?.applyLocale(prefs.customLocale)
|
||||
super.attachBaseContext(base)
|
||||
}
|
||||
|
||||
override fun onCreate(state: Bundle?) {
|
||||
super.onCreate(state)
|
||||
setContentView(R.layout.activity_main)
|
||||
@@ -52,7 +73,7 @@ class MainActivity : AppCompatActivity() {
|
||||
private const val PREFERENCES = "preferences"
|
||||
private const val HISTORY = "history"
|
||||
private const val ENCODE = "encode"
|
||||
private const val DECODED = "decoded"
|
||||
const val DECODED = "decoded"
|
||||
|
||||
private fun getFragmentForIntent(intent: Intent?): Fragment {
|
||||
intent ?: return PreferencesFragment()
|
||||
|
||||
@@ -4,39 +4,65 @@ import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Rect
|
||||
import android.graphics.RectF
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.os.Parcelable
|
||||
import android.os.Vibrator
|
||||
import android.support.v7.app.AppCompatActivity
|
||||
import android.support.v7.widget.Toolbar
|
||||
import android.support.v8.renderscript.RenderScript
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import com.google.zxing.Result
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.app.colorSystemAndToolBars
|
||||
import de.markusfisch.android.binaryeye.app.initSystemBars
|
||||
import de.markusfisch.android.binaryeye.app.vibrate
|
||||
import de.markusfisch.android.binaryeye.app.*
|
||||
import de.markusfisch.android.binaryeye.graphics.crop
|
||||
import de.markusfisch.android.binaryeye.graphics.loadImageUri
|
||||
import de.markusfisch.android.binaryeye.graphics.mapResult
|
||||
import de.markusfisch.android.binaryeye.view.doOnApplyWindowInsets
|
||||
import de.markusfisch.android.binaryeye.graphics.mapResultToView
|
||||
import de.markusfisch.android.binaryeye.os.vibrate
|
||||
import de.markusfisch.android.binaryeye.rs.fixTransparency
|
||||
import de.markusfisch.android.binaryeye.view.colorSystemAndToolBars
|
||||
import de.markusfisch.android.binaryeye.view.initSystemBars
|
||||
import de.markusfisch.android.binaryeye.view.recordToolbarHeight
|
||||
import de.markusfisch.android.binaryeye.view.setPaddingFromWindowInsets
|
||||
import de.markusfisch.android.binaryeye.widget.CropImageView
|
||||
import de.markusfisch.android.binaryeye.widget.DetectorView
|
||||
import de.markusfisch.android.binaryeye.widget.toast
|
||||
import de.markusfisch.android.binaryeye.zxing.Zxing
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class PickActivity : AppCompatActivity() {
|
||||
private val zxing = Zxing()
|
||||
|
||||
private lateinit var rs: RenderScript
|
||||
private lateinit var vibrator: Vibrator
|
||||
private lateinit var cropImageView: CropImageView
|
||||
private lateinit var detectorView: DetectorView
|
||||
|
||||
private var result: Result? = null
|
||||
|
||||
override fun attachBaseContext(base: Context?) {
|
||||
base?.applyLocale(prefs.customLocale)
|
||||
super.attachBaseContext(base)
|
||||
}
|
||||
|
||||
override fun onCreate(state: Bundle?) {
|
||||
super.onCreate(state)
|
||||
setContentView(R.layout.activity_pick)
|
||||
|
||||
zxing.updateHints(true)
|
||||
// Necessary to get the right translation after setting a custom
|
||||
// locale.
|
||||
setTitle(R.string.pick_code_to_scan)
|
||||
|
||||
zxing.updateHints(
|
||||
true,
|
||||
prefs.barcodeFormats
|
||||
)
|
||||
rs = RenderScript.create(this)
|
||||
vibrator = getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
|
||||
|
||||
initSystemBars(this)
|
||||
@@ -48,59 +74,90 @@ class PickActivity : AppCompatActivity() {
|
||||
colorSystemAndToolBars(this@PickActivity)
|
||||
}
|
||||
|
||||
val bitmap = if (
|
||||
intent?.action == Intent.ACTION_SEND &&
|
||||
intent.type?.startsWith("image/") == true
|
||||
) {
|
||||
loadSentImage(intent)
|
||||
} else if (
|
||||
intent?.action == Intent.ACTION_VIEW &&
|
||||
intent.type?.startsWith("image/") == true
|
||||
) {
|
||||
loadImageToOpen(intent)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
val bitmap = fixTransparency(rs, getBitmapFromIntent())
|
||||
if (bitmap == null) {
|
||||
applicationContext.toast(R.string.error_no_content)
|
||||
finish()
|
||||
return
|
||||
}
|
||||
|
||||
val scannedRect = Rect()
|
||||
fun scanWithinBounds() = crop(
|
||||
bitmap,
|
||||
cropImageView.normalizedRectInBounds,
|
||||
cropImageView.imageRotation
|
||||
)?.let {
|
||||
scannedRect.set(0, 0, it.width, it.height)
|
||||
zxing.decodePositiveNegative(it)
|
||||
}
|
||||
|
||||
cropImageView = findViewById(R.id.image) as CropImageView
|
||||
cropImageView.restrictTranslation = false
|
||||
cropImageView.setImageBitmap(bitmap)
|
||||
cropImageView.onScan = {
|
||||
scanWithinBounds()?.let {
|
||||
if (!isFinishing) {
|
||||
vibrator.vibrate()
|
||||
}
|
||||
mapResult(
|
||||
scannedRect.width(),
|
||||
scannedRect.height(),
|
||||
0,
|
||||
cropImageView.getBoundsRect(),
|
||||
it
|
||||
)
|
||||
}
|
||||
}
|
||||
cropImageView.doOnApplyWindowInsets { v, insets ->
|
||||
(v as CropImageView).windowInsets.set(insets)
|
||||
scanWithinBounds(bitmap)
|
||||
}
|
||||
|
||||
findViewById(R.id.scan).setOnClickListener {
|
||||
scanImage(scanWithinBounds())
|
||||
detectorView = findViewById(R.id.detector_view) as DetectorView
|
||||
detectorView.onRoiChanged = {
|
||||
scanWithinBounds(bitmap)
|
||||
}
|
||||
detectorView.setPaddingFromWindowInsets()
|
||||
detectorView.restoreCropHandlePos()
|
||||
|
||||
findViewById(R.id.scan).setOnClickListener {
|
||||
showResult()
|
||||
}
|
||||
}
|
||||
|
||||
private fun getBitmapFromIntent(): 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
|
||||
}
|
||||
|
||||
private fun scanWithinBounds(bitmap: Bitmap) {
|
||||
val roi = detectorView.roi
|
||||
val rectInView = if (roi.width() < 1) {
|
||||
cropImageView.getBoundsRect()
|
||||
} else {
|
||||
roi
|
||||
}
|
||||
val imageRect = cropImageView.mappedRect
|
||||
val rectInImage = normalizeRoi(imageRect, rectInView)
|
||||
val cropped = crop(
|
||||
bitmap,
|
||||
rectInImage,
|
||||
cropImageView.imageRotation
|
||||
) ?: return
|
||||
CoroutineScope(Dispatchers.IO).launch(Dispatchers.IO) {
|
||||
result = zxing.decodePositiveNegative(cropped)
|
||||
result?.let {
|
||||
withContext(Dispatchers.Main) {
|
||||
if (isFinishing) {
|
||||
return@withContext
|
||||
}
|
||||
vibrator.vibrate()
|
||||
val rc = Rect()
|
||||
imageRect.round(rc)
|
||||
rectInView.intersect(rc)
|
||||
detectorView.update(
|
||||
mapResultToView(
|
||||
cropped.width,
|
||||
cropped.height,
|
||||
0,
|
||||
rectInView,
|
||||
it.resultPoints,
|
||||
detectorView.coordinates
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
detectorView.saveCropHandlePos()
|
||||
rs.destroy()
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||
@@ -134,12 +191,24 @@ class PickActivity : AppCompatActivity() {
|
||||
return loadImageUri(contentResolver, uri)
|
||||
}
|
||||
|
||||
private fun scanImage(result: Result?) {
|
||||
if (result != null) {
|
||||
showResult(this, result)
|
||||
private fun showResult() {
|
||||
val r = result
|
||||
if (r != null) {
|
||||
showResult(this, r, vibrator)
|
||||
finish()
|
||||
return
|
||||
} else {
|
||||
applicationContext.toast(R.string.no_barcode_found)
|
||||
}
|
||||
applicationContext.toast(R.string.no_barcode_found)
|
||||
}
|
||||
}
|
||||
|
||||
private fun normalizeRoi(imageRect: RectF, roi: Rect): RectF {
|
||||
val w = imageRect.width()
|
||||
val h = imageRect.height()
|
||||
return RectF(
|
||||
(roi.left - imageRect.left) / w,
|
||||
(roi.top - imageRect.top) / h,
|
||||
(roi.right - imageRect.left) / w,
|
||||
(roi.bottom - imageRect.top) / h
|
||||
)
|
||||
}
|
||||
|
||||
@@ -9,9 +9,9 @@ class SplashActivity : AppCompatActivity() {
|
||||
override fun onCreate(state: Bundle?) {
|
||||
super.onCreate(state)
|
||||
|
||||
// it's important _not_ to inflate a layout file here
|
||||
// It's important _not_ to inflate a layout file here
|
||||
// because that would happen after the app is fully
|
||||
// initialized what is too late
|
||||
// initialized what is too late.
|
||||
|
||||
setRestartCount(intent)
|
||||
startActivity(Intent(applicationContext, CameraActivity::class.java))
|
||||
|
||||
@@ -8,7 +8,10 @@ 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
|
||||
import de.markusfisch.android.binaryeye.database.Database
|
||||
import java.text.DateFormat
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
|
||||
class ScansAdapter(context: Context, cursor: Cursor) :
|
||||
CursorAdapter(context, cursor, false) {
|
||||
@@ -56,7 +59,7 @@ class ScansAdapter(context: Context, cursor: Cursor) :
|
||||
cursor: Cursor
|
||||
) {
|
||||
val holder = getViewHolder(view)
|
||||
holder.timeView.text = cursor.getString(timeIndex)
|
||||
holder.timeView.text = formatDateTime(cursor.getString(timeIndex))
|
||||
val name = cursor.getString(nameIndex)
|
||||
val content = cursor.getString(contentIndex)
|
||||
var icon = 0
|
||||
@@ -71,8 +74,8 @@ class ScansAdapter(context: Context, cursor: Cursor) :
|
||||
holder.contentView.setCompoundDrawablesWithIntrinsicBounds(
|
||||
icon, 0, 0, 0
|
||||
)
|
||||
holder.formatView.text = cursor.getString(formatIndex)
|
||||
// view.isSelected needs to be put on the queue to work
|
||||
holder.formatView.text = prettifyFormatName(cursor.getString(formatIndex))
|
||||
// view.isSelected needs to be put on the queue to work.
|
||||
val selected = cursor.getLong(idIndex) == selectedScanId
|
||||
view.post {
|
||||
view.isSelected = selected
|
||||
@@ -95,3 +98,15 @@ class ScansAdapter(context: Context, cursor: Cursor) :
|
||||
val formatView: TextView
|
||||
)
|
||||
}
|
||||
|
||||
fun prettifyFormatName(format: String) = format.replace("_", " ")
|
||||
|
||||
private fun formatDateTime(rfc: String): String {
|
||||
SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.US).parse(rfc)?.let {
|
||||
return DateFormat.getDateTimeInstance(
|
||||
DateFormat.LONG,
|
||||
DateFormat.SHORT
|
||||
).format(it)
|
||||
}
|
||||
return rfc
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ suspend inline fun <T : Any> alertDialog(
|
||||
context: Context,
|
||||
crossinline build: AlertDialog.Builder.(resume: (T?) -> Unit) -> Unit
|
||||
): T? = withContext(Dispatchers.Main) {
|
||||
suspendCoroutine<T?> { continuation ->
|
||||
suspendCoroutine { continuation ->
|
||||
AlertDialog.Builder(context).apply {
|
||||
setOnCancelListener {
|
||||
continuation.resume(null)
|
||||
|
||||
@@ -2,7 +2,7 @@ package de.markusfisch.android.binaryeye.app
|
||||
|
||||
import android.app.Application
|
||||
import android.support.v8.renderscript.RenderScript
|
||||
import de.markusfisch.android.binaryeye.data.Database
|
||||
import de.markusfisch.android.binaryeye.database.Database
|
||||
import de.markusfisch.android.binaryeye.preference.Preferences
|
||||
|
||||
val db = Database()
|
||||
@@ -11,12 +11,11 @@ val prefs = Preferences()
|
||||
class BinaryEyeApp : Application() {
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
db.open(this)
|
||||
prefs.init(this)
|
||||
|
||||
if (prefs.forceCompat) {
|
||||
RenderScript.forceCompat()
|
||||
}
|
||||
|
||||
db.open(this)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
package de.markusfisch.android.binaryeye.app
|
||||
|
||||
import java.util.regex.Pattern
|
||||
|
||||
private val urlPattern: Pattern = Pattern.compile(
|
||||
"(?:^|[\\W])((ht|f)tp(s?):\\/\\/|www\\.)" +
|
||||
"(([\\w\\-]+\\.){1,}?([\\w\\-.~]+\\/?)*" +
|
||||
"[\\p{Alnum}.,%_=?&#\\-+()\\[\\]\\*$~@!:/{};']*)",
|
||||
Pattern.CASE_INSENSITIVE or Pattern.MULTILINE or Pattern.DOTALL
|
||||
)
|
||||
|
||||
fun getLinks(text: String): List<String> {
|
||||
val matcher = urlPattern.matcher(text)
|
||||
val urls = mutableListOf<String>()
|
||||
|
||||
while (matcher.find()) {
|
||||
val matchStart = matcher.start(1)
|
||||
val matchEnd = matcher.end()
|
||||
|
||||
var url = text.substring(matchStart, matchEnd)
|
||||
if (!url.startsWith("http://") && !url.startsWith("https://")) {
|
||||
url = "https://$url"
|
||||
}
|
||||
|
||||
urls.add(url)
|
||||
}
|
||||
|
||||
return urls
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package de.markusfisch.android.binaryeye.app
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import java.util.*
|
||||
|
||||
fun Context.applyLocale(localeName: String) {
|
||||
if (localeName.isEmpty()) {
|
||||
return
|
||||
}
|
||||
val localeParts = localeName.split("-")
|
||||
val locale = if (localeParts.size == 2) {
|
||||
Locale(localeParts[0], localeParts[1])
|
||||
} else {
|
||||
Locale(localeName)
|
||||
}
|
||||
Locale.setDefault(locale)
|
||||
val conf = resources.configuration
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
|
||||
@Suppress("DEPRECATION")
|
||||
conf.locale = locale
|
||||
} else {
|
||||
conf.setLocale(locale)
|
||||
}
|
||||
resources.updateConfiguration(conf, resources.displayMetrics)
|
||||
}
|
||||
@@ -6,41 +6,50 @@ import android.content.pm.PackageManager
|
||||
import android.support.v4.app.ActivityCompat
|
||||
import android.support.v4.content.ContextCompat
|
||||
|
||||
fun hasCameraPermission(activity: Activity, requestCode: Int = 1): Boolean {
|
||||
return hasPermission(activity, Manifest.permission.CAMERA, requestCode)
|
||||
}
|
||||
var permissionGrantedCallback: (() -> Any)? = null
|
||||
|
||||
fun hasWritePermission(activity: Activity, requestCode: Int = 2): Boolean {
|
||||
const val PERMISSION_CAMERA = 1
|
||||
fun hasCameraPermission(activity: Activity) = hasPermission(
|
||||
activity,
|
||||
Manifest.permission.CAMERA,
|
||||
PERMISSION_CAMERA
|
||||
)
|
||||
|
||||
const val PERMISSION_WRITE = 2
|
||||
fun hasWritePermission(activity: Activity, callback: () -> Any): Boolean {
|
||||
permissionGrantedCallback = callback
|
||||
return hasPermission(
|
||||
activity,
|
||||
Manifest.permission.WRITE_EXTERNAL_STORAGE,
|
||||
requestCode
|
||||
PERMISSION_WRITE
|
||||
)
|
||||
}
|
||||
|
||||
fun hasLocationPermission(activity: Activity, requestCode: Int = 3): Boolean {
|
||||
const val PERMISSION_LOCATION = 3
|
||||
fun hasLocationPermission(activity: Activity, callback: () -> Any): Boolean {
|
||||
permissionGrantedCallback = callback
|
||||
return hasPermission(
|
||||
activity,
|
||||
Manifest.permission.ACCESS_FINE_LOCATION,
|
||||
requestCode
|
||||
PERMISSION_LOCATION
|
||||
)
|
||||
}
|
||||
|
||||
fun hasPermission(
|
||||
private fun hasPermission(
|
||||
activity: Activity,
|
||||
permission: String,
|
||||
requestCode: Int
|
||||
): Boolean {
|
||||
return if (ContextCompat.checkSelfPermission(activity, permission) !=
|
||||
PackageManager.PERMISSION_GRANTED
|
||||
) {
|
||||
ActivityCompat.requestPermissions(
|
||||
activity,
|
||||
arrayOf(permission),
|
||||
requestCode
|
||||
)
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
) = if (ContextCompat.checkSelfPermission(
|
||||
activity,
|
||||
permission
|
||||
) != PackageManager.PERMISSION_GRANTED
|
||||
) {
|
||||
ActivityCompat.requestPermissions(
|
||||
activity,
|
||||
arrayOf(permission),
|
||||
requestCode
|
||||
)
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
package de.markusfisch.android.binaryeye.app
|
||||
|
||||
import android.os.Build
|
||||
import android.os.VibrationEffect
|
||||
import android.os.Vibrator
|
||||
|
||||
private const val MILLISECONDS = 100L
|
||||
|
||||
fun Vibrator.vibrate() {
|
||||
if (!prefs.vibrate) {
|
||||
return
|
||||
}
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
||||
@Suppress("DEPRECATION")
|
||||
this.vibrate(MILLISECONDS)
|
||||
} else {
|
||||
this.vibrate(
|
||||
VibrationEffect.createOneShot(
|
||||
MILLISECONDS,
|
||||
VibrationEffect.DEFAULT_AMPLITUDE
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
package de.markusfisch.android.binaryeye.app
|
||||
package de.markusfisch.android.binaryeye.content
|
||||
|
||||
import android.content.ClipData
|
||||
import android.content.Context
|
||||
@@ -11,7 +11,7 @@ fun Context.copyToClipboard(text: String) {
|
||||
(service as android.text.ClipboardManager).text = text
|
||||
} else {
|
||||
(service as android.content.ClipboardManager).setPrimaryClip(
|
||||
ClipData.newPlainText("simple text", text)
|
||||
ClipData.newPlainText("plain text", text)
|
||||
)
|
||||
}
|
||||
}
|
||||
+25
-11
@@ -1,5 +1,6 @@
|
||||
package de.markusfisch.android.binaryeye.app
|
||||
package de.markusfisch.android.binaryeye.content
|
||||
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
@@ -10,11 +11,32 @@ import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.widget.toast
|
||||
import java.io.File
|
||||
|
||||
fun Context.execShareIntent(intent: Intent) {
|
||||
if (!startIntent(intent)) {
|
||||
toast(R.string.cannot_resolve_action)
|
||||
}
|
||||
}
|
||||
|
||||
fun Context.startIntent(intent: Intent) = try {
|
||||
// Avoid using `intent.resolveActivity()` at API level 30+ due
|
||||
// to the new package visibility restrictions. In order for
|
||||
// `resolveActivity()` to "see" another package, we would need
|
||||
// to list that package/intent in a `<queries>` block in the
|
||||
// Manifest. But since we used `resolveActivity()` only to avoid
|
||||
// an exception if the Intent cannot be resolved, it's much easier
|
||||
// and more robust to just try and catch that exception if
|
||||
// necessary.
|
||||
startActivity(intent)
|
||||
true
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
false
|
||||
}
|
||||
|
||||
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)
|
||||
context.execShareIntent(intent)
|
||||
}
|
||||
|
||||
fun shareUri(context: Context, uri: Uri, type: String) {
|
||||
@@ -22,15 +44,7 @@ fun shareUri(context: Context, uri: Uri, type: String) {
|
||||
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 {
|
||||
context.toast(R.string.cannot_resolve_action)
|
||||
}
|
||||
context.execShareIntent(intent)
|
||||
}
|
||||
|
||||
fun shareFile(context: Context, file: File, type: String) {
|
||||
+8
-6
@@ -1,9 +1,9 @@
|
||||
package de.markusfisch.android.binaryeye.data
|
||||
package de.markusfisch.android.binaryeye.database
|
||||
|
||||
import android.content.Context
|
||||
import android.database.Cursor
|
||||
import de.markusfisch.android.binaryeye.app.toHexString
|
||||
import de.markusfisch.android.binaryeye.app.writeExternalFile
|
||||
import de.markusfisch.android.binaryeye.io.writeExternalFile
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.OutputStream
|
||||
|
||||
@@ -16,7 +16,7 @@ fun exportCsv(
|
||||
exportCsv(outputStream, cursor, delimiter)
|
||||
}
|
||||
|
||||
fun exportCsv(cursor: Cursor, delimiter: String): String? {
|
||||
fun exportCsv(cursor: Cursor, delimiter: String): String {
|
||||
val outputStream = ByteArrayOutputStream()
|
||||
exportCsv(outputStream, cursor, delimiter)
|
||||
return outputStream.toString()
|
||||
@@ -87,6 +87,8 @@ private fun Cursor.toCsvRecord(
|
||||
return sb.toString().toByteArray()
|
||||
}
|
||||
|
||||
private fun String.quoteAndEscape() = "\"${this
|
||||
.replace("\n", " ")
|
||||
.replace("\"", "\"\"")}\""
|
||||
private fun String.quoteAndEscape() = "\"${
|
||||
this
|
||||
.replace("\n", " ")
|
||||
.replace("\"", "\"\"")
|
||||
}\""
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package de.markusfisch.android.binaryeye.data
|
||||
package de.markusfisch.android.binaryeye.database
|
||||
|
||||
import android.content.ContentValues
|
||||
import android.content.Context
|
||||
+2
-6
@@ -1,16 +1,12 @@
|
||||
package de.markusfisch.android.binaryeye.data
|
||||
package de.markusfisch.android.binaryeye.database
|
||||
|
||||
import android.app.Activity
|
||||
import android.os.Environment
|
||||
import de.markusfisch.android.binaryeye.app.hasWritePermission
|
||||
import de.markusfisch.android.binaryeye.app.writeExternalFile
|
||||
import de.markusfisch.android.binaryeye.io.writeExternalFile
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
|
||||
fun exportDatabase(activity: Activity, fileName: String): Boolean {
|
||||
if (!hasWritePermission(activity)) {
|
||||
return false
|
||||
}
|
||||
val dbFile = File(
|
||||
Environment.getDataDirectory(),
|
||||
"//data//${activity.packageName}//databases//${Database.FILE_NAME}"
|
||||
+2
-2
@@ -1,9 +1,9 @@
|
||||
package de.markusfisch.android.binaryeye.data
|
||||
package de.markusfisch.android.binaryeye.database
|
||||
|
||||
import android.content.Context
|
||||
import android.database.Cursor
|
||||
import de.markusfisch.android.binaryeye.app.toHexString
|
||||
import de.markusfisch.android.binaryeye.app.writeExternalFile
|
||||
import de.markusfisch.android.binaryeye.io.writeExternalFile
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONException
|
||||
import org.json.JSONObject
|
||||
+8
-4
@@ -1,4 +1,4 @@
|
||||
package de.markusfisch.android.binaryeye.data
|
||||
package de.markusfisch.android.binaryeye.database
|
||||
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
@@ -19,7 +19,7 @@ data class Scan(
|
||||
val suggestedPrice: String?,
|
||||
val upcEanExtension: String?,
|
||||
val timestamp: String = getDateTime(),
|
||||
val id: Long = 0L
|
||||
var id: Long = 0L
|
||||
) : Parcelable {
|
||||
constructor(result: Result) : this(
|
||||
result.text,
|
||||
@@ -138,8 +138,12 @@ private fun getRawBytes(result: Result): ByteArray? {
|
||||
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:"
|
||||
// If the byte segments are shorter than the converted string, the
|
||||
// content of the QR Code has been encoded with different encoding
|
||||
// modes (e.g. some parts in alphanumeric, some in byte encoding).
|
||||
// This is because Zxing only records byte segments for byte encoded
|
||||
// parts. Please note the byte segments can actually be longer than
|
||||
// the string because Zxing cuts off prefixes like "WIFI:".
|
||||
return if (bytes.size >= result.text.length) bytes else null
|
||||
}
|
||||
|
||||
@@ -2,20 +2,31 @@ package de.markusfisch.android.binaryeye.fragment
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.AlertDialog
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.support.v4.app.Fragment
|
||||
import android.view.*
|
||||
import android.widget.EditText
|
||||
import com.google.zxing.BarcodeFormat
|
||||
import com.google.zxing.EncodeHintType
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.app.*
|
||||
import de.markusfisch.android.binaryeye.app.hasWritePermission
|
||||
import de.markusfisch.android.binaryeye.content.copyToClipboard
|
||||
import de.markusfisch.android.binaryeye.content.shareFile
|
||||
import de.markusfisch.android.binaryeye.content.shareText
|
||||
import de.markusfisch.android.binaryeye.io.addSuffixIfNotGiven
|
||||
import de.markusfisch.android.binaryeye.io.toSaveResult
|
||||
import de.markusfisch.android.binaryeye.io.writeExternalFile
|
||||
import de.markusfisch.android.binaryeye.view.doOnApplyWindowInsets
|
||||
import de.markusfisch.android.binaryeye.view.setPaddingFromWindowInsets
|
||||
import de.markusfisch.android.binaryeye.widget.ConfinedScalingImageView
|
||||
import de.markusfisch.android.binaryeye.widget.toast
|
||||
import de.markusfisch.android.binaryeye.zxing.Zxing
|
||||
import kotlinx.coroutines.Dispatchers.Main
|
||||
import de.markusfisch.android.binaryeye.zxing.encodeAsBitmap
|
||||
import de.markusfisch.android.binaryeye.zxing.encodeAsSvg
|
||||
import de.markusfisch.android.binaryeye.zxing.encodeAsText
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import java.io.File
|
||||
@@ -25,7 +36,13 @@ import java.io.OutputStream
|
||||
import java.util.*
|
||||
|
||||
class BarcodeFragment : Fragment() {
|
||||
private var barcode: Bitmap? = null
|
||||
private enum class FileType {
|
||||
PNG, SVG, TXT
|
||||
}
|
||||
|
||||
private var barcodeBitmap: Bitmap? = null
|
||||
private var barcodeSvg: String? = null
|
||||
private var barcodeTxt: String? = null
|
||||
private var content: String = ""
|
||||
private var format: BarcodeFormat? = null
|
||||
|
||||
@@ -51,9 +68,12 @@ class BarcodeFragment : Fragment() {
|
||||
val args = arguments ?: return view
|
||||
val content = args.getString(CONTENT) ?: return view
|
||||
val format = args.getSerializable(FORMAT) as BarcodeFormat? ?: return view
|
||||
val hints = args.getSerializable(HINTS) as EnumMap<EncodeHintType, Any>?
|
||||
val size = args.getInt(SIZE)
|
||||
try {
|
||||
barcode = Zxing.encodeAsBitmap(content, format, size, size)
|
||||
barcodeBitmap = encodeAsBitmap(content, format, size, size, hints)
|
||||
barcodeSvg = encodeAsSvg(content, format, hints)
|
||||
barcodeTxt = encodeAsText(content, format, hints)
|
||||
} catch (e: Exception) {
|
||||
var message = e.message
|
||||
if (message == null || message.isEmpty()) {
|
||||
@@ -71,16 +91,15 @@ class BarcodeFragment : Fragment() {
|
||||
val imageView = view.findViewById<ConfinedScalingImageView>(
|
||||
R.id.barcode
|
||||
)
|
||||
imageView.setImageBitmap(barcode)
|
||||
imageView.setImageBitmap(barcodeBitmap)
|
||||
imageView.post {
|
||||
// make sure to invoke this after ScalingImageView.onLayout()
|
||||
// Make sure to invoke this after ScalingImageView.onLayout().
|
||||
imageView.minWidth /= 2f
|
||||
}
|
||||
|
||||
view.findViewById<View>(R.id.share).setOnClickListener {
|
||||
val bitmap = barcode
|
||||
bitmap?.let {
|
||||
share(bitmap)
|
||||
pickFileType(context, R.string.share_as) {
|
||||
shareAs(it)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,33 +117,77 @@ class BarcodeFragment : Fragment() {
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
return when (item.itemId) {
|
||||
R.id.save -> {
|
||||
askForFileNameAndSave()
|
||||
R.id.copy_to_clipboard -> {
|
||||
barcodeTxt?.let {
|
||||
context.copyToClipboard(it)
|
||||
context.toast(R.string.copied_to_clipboard)
|
||||
}
|
||||
true
|
||||
}
|
||||
R.id.export_to_file -> {
|
||||
pickFileType(context, R.string.export_as) {
|
||||
askForFileNameAndSave(it)
|
||||
}
|
||||
true
|
||||
}
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
}
|
||||
|
||||
// dialogs do not have a parent view
|
||||
private fun pickFileType(
|
||||
context: Context,
|
||||
title: Int,
|
||||
action: (FileType) -> Unit
|
||||
) {
|
||||
val fileTypes = FileType.values()
|
||||
AlertDialog.Builder(context)
|
||||
.setTitle(title)
|
||||
.setItems(fileTypes.map { it.name }.toTypedArray()) { _, which ->
|
||||
action(fileTypes[which])
|
||||
}
|
||||
.show()
|
||||
}
|
||||
|
||||
// Dialogs do not have a parent view.
|
||||
@SuppressLint("InflateParams")
|
||||
private fun askForFileNameAndSave() {
|
||||
private fun askForFileNameAndSave(fileType: FileType) {
|
||||
val ac = activity ?: return
|
||||
// Write permission is only required before Android Q.
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q &&
|
||||
!hasWritePermission(ac) { askForFileNameAndSave(fileType) }
|
||||
) {
|
||||
return
|
||||
}
|
||||
val view = ac.layoutInflater.inflate(R.layout.dialog_save_file, null)
|
||||
val editText = view.findViewById<EditText>(R.id.file_name)
|
||||
editText.setText(encodeFileName("${format.toString()}_$content"))
|
||||
AlertDialog.Builder(ac)
|
||||
.setView(view)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
val bitmap = barcode
|
||||
bitmap?.let {
|
||||
saveAsFile(
|
||||
bitmap,
|
||||
addSuffixIfNotGiven(
|
||||
editText.text.toString(),
|
||||
".png"
|
||||
)
|
||||
)
|
||||
val fileName = editText.text.toString()
|
||||
when (fileType) {
|
||||
FileType.PNG -> saveAs(
|
||||
addSuffixIfNotGiven(fileName, ".png"),
|
||||
MIME_PNG
|
||||
) {
|
||||
barcodeBitmap?.saveAsPng(it)
|
||||
}
|
||||
FileType.SVG -> saveAs(
|
||||
addSuffixIfNotGiven(fileName, ".svg"),
|
||||
MIME_SVG
|
||||
) { outputStream ->
|
||||
barcodeSvg?.let {
|
||||
outputStream.write(it.toByteArray())
|
||||
}
|
||||
}
|
||||
FileType.TXT -> saveAs(
|
||||
addSuffixIfNotGiven(fileName, ".txt"),
|
||||
MIME_TXT
|
||||
) { outputStream ->
|
||||
barcodeTxt?.let {
|
||||
outputStream.write(it.toByteArray())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||
@@ -132,25 +195,33 @@ class BarcodeFragment : Fragment() {
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun saveAsFile(bitmap: Bitmap, fileName: String) {
|
||||
private fun saveAs(
|
||||
fileName: String,
|
||||
mimeType: String,
|
||||
write: (outputStream: OutputStream) -> Unit
|
||||
) {
|
||||
val ac = activity ?: return
|
||||
if (!hasWritePermission(ac)) {
|
||||
return
|
||||
}
|
||||
GlobalScope.launch {
|
||||
val message = writeExternalFile(ac, fileName, "image/png") {
|
||||
bitmap.saveAsPng(it)
|
||||
}.toSaveResult()
|
||||
GlobalScope.launch(Main) {
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
val message = writeExternalFile(ac, fileName, mimeType, write).toSaveResult()
|
||||
launch(Dispatchers.Main) {
|
||||
ac.toast(message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun shareAs(fileType: FileType) {
|
||||
when (fileType) {
|
||||
FileType.PNG -> barcodeBitmap?.let { share(it) }
|
||||
FileType.SVG -> barcodeSvg?.let { shareText(context, it, MIME_SVG) }
|
||||
FileType.TXT -> barcodeTxt?.let { shareText(context, it) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun share(bitmap: Bitmap) {
|
||||
GlobalScope.launch {
|
||||
val ctx = context ?: return
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
val file = File(
|
||||
context.externalCacheDir,
|
||||
ctx.externalCacheDir,
|
||||
"shared_barcode.png"
|
||||
)
|
||||
val success = try {
|
||||
@@ -161,11 +232,11 @@ class BarcodeFragment : Fragment() {
|
||||
} catch (e: IOException) {
|
||||
false
|
||||
}
|
||||
GlobalScope.launch(Main) {
|
||||
launch(Dispatchers.Main) {
|
||||
if (success) {
|
||||
shareFile(context, file, "image/png")
|
||||
shareFile(ctx, file, "image/png")
|
||||
} else {
|
||||
activity?.toast(R.string.error_saving_file)
|
||||
ctx.toast(R.string.error_saving_file)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -174,16 +245,24 @@ class BarcodeFragment : Fragment() {
|
||||
companion object {
|
||||
private const val CONTENT = "content"
|
||||
private const val FORMAT = "format"
|
||||
private const val HINTS = "hints"
|
||||
private const val SIZE = "size"
|
||||
private const val MIME_PNG = "image/png"
|
||||
private const val MIME_SVG = "image/svg+xmg"
|
||||
private const val MIME_TXT = "text/plain"
|
||||
|
||||
fun newInstance(
|
||||
content: String,
|
||||
format: BarcodeFormat,
|
||||
size: Int
|
||||
size: Int,
|
||||
hints: EnumMap<EncodeHintType, Any>? = null
|
||||
): Fragment {
|
||||
val args = Bundle()
|
||||
args.putString(CONTENT, content)
|
||||
args.putSerializable(FORMAT, format)
|
||||
hints?.let {
|
||||
args.putSerializable(HINTS, it)
|
||||
}
|
||||
args.putInt(SIZE, size)
|
||||
val fragment = BarcodeFragment()
|
||||
fragment.arguments = args
|
||||
@@ -198,4 +277,4 @@ private fun Bitmap.saveAsPng(outputStream: OutputStream, quality: Int = 90) {
|
||||
|
||||
private val fileNameCharacters = "[^A-Za-z0-9]".toRegex()
|
||||
private fun encodeFileName(name: String): String =
|
||||
fileNameCharacters.replace(name, "_").take(16).trim('_').toLowerCase(Locale.getDefault())
|
||||
fileNameCharacters.replace(name, "_").take(16).trim('_').lowercase(Locale.getDefault())
|
||||
|
||||
@@ -14,8 +14,16 @@ import android.widget.TextView
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.actions.ActionRegistry
|
||||
import de.markusfisch.android.binaryeye.actions.wifi.WifiAction
|
||||
import de.markusfisch.android.binaryeye.actions.wifi.WifiConnector
|
||||
import de.markusfisch.android.binaryeye.activity.MainActivity
|
||||
import de.markusfisch.android.binaryeye.adapter.prettifyFormatName
|
||||
import de.markusfisch.android.binaryeye.app.*
|
||||
import de.markusfisch.android.binaryeye.data.Scan
|
||||
import de.markusfisch.android.binaryeye.content.copyToClipboard
|
||||
import de.markusfisch.android.binaryeye.content.shareText
|
||||
import de.markusfisch.android.binaryeye.database.Scan
|
||||
import de.markusfisch.android.binaryeye.io.askForFileName
|
||||
import de.markusfisch.android.binaryeye.io.toSaveResult
|
||||
import de.markusfisch.android.binaryeye.io.writeExternalFile
|
||||
import de.markusfisch.android.binaryeye.view.setPaddingFromWindowInsets
|
||||
import de.markusfisch.android.binaryeye.widget.toast
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
@@ -26,18 +34,22 @@ import kotlinx.coroutines.launch
|
||||
class DecodeFragment : Fragment() {
|
||||
private lateinit var contentView: EditText
|
||||
private lateinit var formatView: TextView
|
||||
private lateinit var dataView: TableLayout
|
||||
private lateinit var metaView: TableLayout
|
||||
private lateinit var hexView: TextView
|
||||
private lateinit var format: String
|
||||
private lateinit var fab: FloatingActionButton
|
||||
|
||||
private var action = ActionRegistry.DEFAULT_ACTION
|
||||
private var isBinary = false
|
||||
private val parentJob = Job()
|
||||
private val scope: CoroutineScope = CoroutineScope(Dispatchers.Main + parentJob)
|
||||
private val content: String
|
||||
get() = contentView.text.toString()
|
||||
|
||||
private val parentJob = Job()
|
||||
private val scope: CoroutineScope = CoroutineScope(Dispatchers.Main + parentJob)
|
||||
private var closeAutomatically = false
|
||||
private var action = ActionRegistry.DEFAULT_ACTION
|
||||
private var isBinary = false
|
||||
private var raw: ByteArray = ByteArray(0)
|
||||
private var id = 0L
|
||||
|
||||
override fun onCreate(state: Bundle?) {
|
||||
super.onCreate(state)
|
||||
@@ -57,14 +69,18 @@ class DecodeFragment : Fragment() {
|
||||
false
|
||||
)
|
||||
|
||||
closeAutomatically = prefs.closeAutomatically &&
|
||||
activity?.intent?.hasExtra(MainActivity.DECODED) == true
|
||||
|
||||
val scan = arguments?.getParcelable(SCAN) as Scan?
|
||||
?: throw IllegalArgumentException("DecodeFragment needs a Scan")
|
||||
id = scan.id
|
||||
|
||||
val inputContent = scan.content
|
||||
isBinary = hasNonPrintableCharacters(
|
||||
inputContent
|
||||
) or inputContent.isEmpty()
|
||||
val raw = scan.raw ?: inputContent.toByteArray()
|
||||
raw = scan.raw ?: inputContent.toByteArray()
|
||||
format = scan.format
|
||||
|
||||
contentView = view.findViewById(R.id.content)
|
||||
@@ -109,18 +125,14 @@ class DecodeFragment : Fragment() {
|
||||
}
|
||||
|
||||
formatView = view.findViewById(R.id.format)
|
||||
dataView = view.findViewById(R.id.data)
|
||||
metaView = view.findViewById(R.id.meta)
|
||||
hexView = view.findViewById(R.id.hex)
|
||||
|
||||
updateViewsAndAction(raw)
|
||||
if (action is WifiAction) {
|
||||
val password = (action as WifiAction).password
|
||||
if (password != null) {
|
||||
activity?.apply {
|
||||
copyToClipboard(password)
|
||||
toast(R.string.copied_password_to_clipboard)
|
||||
}
|
||||
}
|
||||
|
||||
if (!isBinary) {
|
||||
fillDataView(dataView, inputContent)
|
||||
}
|
||||
|
||||
if (prefs.showMetaData) {
|
||||
@@ -133,6 +145,11 @@ class DecodeFragment : Fragment() {
|
||||
return view
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
parentJob.cancel()
|
||||
}
|
||||
|
||||
private fun updateViewsAndAction(bytes: ByteArray) {
|
||||
val prevAction = action
|
||||
if (!prevAction.canExecuteOn(bytes)) {
|
||||
@@ -141,7 +158,7 @@ class DecodeFragment : Fragment() {
|
||||
formatView.text = resources.getQuantityString(
|
||||
R.plurals.barcode_info,
|
||||
bytes.size,
|
||||
format,
|
||||
prettifyFormatName(format),
|
||||
bytes.size
|
||||
)
|
||||
hexView.text = if (prefs.showHexDump) hexDump(bytes) else ""
|
||||
@@ -158,20 +175,13 @@ class DecodeFragment : Fragment() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun fillMetaView(tableLayout: TableLayout, scan: Scan) {
|
||||
private fun fillDataTable(tableLayout: TableLayout, items: LinkedHashMap<Int, String?>?) {
|
||||
val ctx = tableLayout.context
|
||||
val spaceBetween = (ctx.resources.displayMetrics.density * 16f).toInt()
|
||||
var hasMeta = false
|
||||
sortedMapOf(
|
||||
R.string.error_correction_level to scan.errorCorrectionLevel,
|
||||
R.string.issue_number to scan.issueNumber,
|
||||
R.string.orientation to scan.orientation,
|
||||
R.string.other_meta_data to scan.otherMetaData,
|
||||
R.string.pdf417_extra_metadata to scan.pdf417ExtraMetaData,
|
||||
R.string.possible_country to scan.possibleCountry,
|
||||
R.string.suggested_price to scan.suggestedPrice,
|
||||
R.string.upc_ean_extension to scan.upcEanExtension
|
||||
).forEach { item ->
|
||||
val spaceBetween = (16f * ctx.resources.displayMetrics.density).toInt()
|
||||
|
||||
var hasEntries = false
|
||||
|
||||
items?.forEach { item ->
|
||||
item.value?.let {
|
||||
val tr = TableRow(ctx)
|
||||
val keyView = TextView(ctx)
|
||||
@@ -182,30 +192,82 @@ class DecodeFragment : Fragment() {
|
||||
tr.addView(keyView)
|
||||
tr.addView(valueView)
|
||||
tableLayout.addView(tr)
|
||||
hasMeta = true
|
||||
hasEntries = true
|
||||
}
|
||||
}
|
||||
if (hasMeta) {
|
||||
if (hasEntries) {
|
||||
tableLayout.setPadding(0, 0, 0, spaceBetween)
|
||||
}
|
||||
}
|
||||
|
||||
private fun fillDataView(tableLayout: TableLayout, content: String) {
|
||||
val items = LinkedHashMap<Int, String?>()
|
||||
|
||||
if (action is WifiAction) {
|
||||
val wifiData = WifiConnector.parseMap(content)
|
||||
|
||||
if (wifiData != null) {
|
||||
items[R.string.entry_type] = getString(R.string.wifi_network)
|
||||
items[R.string.wifi_ssid] = wifiData["S"]
|
||||
items[R.string.wifi_password] = wifiData["P"]
|
||||
items[R.string.wifi_type] = wifiData["T"]
|
||||
items[R.string.wifi_hidden] = wifiData["H"]
|
||||
items[R.string.wifi_eap] = wifiData["E"]
|
||||
items[R.string.wifi_identity] = wifiData["I"]
|
||||
items[R.string.wifi_anonymous_identity] = wifiData["A"]
|
||||
items[R.string.wifi_phase2] = wifiData["PH2"]
|
||||
}
|
||||
}
|
||||
|
||||
fillDataTable(tableLayout, items)
|
||||
}
|
||||
|
||||
private fun fillMetaView(tableLayout: TableLayout, scan: Scan) {
|
||||
fillDataTable(
|
||||
tableLayout, linkedMapOf(
|
||||
R.string.error_correction_level to scan.errorCorrectionLevel,
|
||||
R.string.issue_number to scan.issueNumber,
|
||||
R.string.orientation to scan.orientation,
|
||||
R.string.other_meta_data to scan.otherMetaData,
|
||||
R.string.pdf417_extra_metadata to scan.pdf417ExtraMetaData,
|
||||
R.string.possible_country to scan.possibleCountry,
|
||||
R.string.suggested_price to scan.suggestedPrice,
|
||||
R.string.upc_ean_extension to scan.upcEanExtension
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
if (id > 0L) {
|
||||
menu.findItem(R.id.remove).isVisible = true
|
||||
}
|
||||
if (action is WifiAction) {
|
||||
menu.findItem(R.id.copy_password).isVisible = true
|
||||
}
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
return when (item.itemId) {
|
||||
R.id.copy_password -> {
|
||||
copyPasswordToClipboard()
|
||||
maybeBackOrFinish()
|
||||
true
|
||||
}
|
||||
R.id.copy_to_clipboard -> {
|
||||
copyToClipboard(content)
|
||||
copyToClipboard(textOrHex())
|
||||
maybeBackOrFinish()
|
||||
true
|
||||
}
|
||||
R.id.share -> {
|
||||
context?.also { shareText(it, content) }
|
||||
context?.also {
|
||||
shareText(it, textOrHex())
|
||||
maybeBackOrFinish()
|
||||
}
|
||||
true
|
||||
}
|
||||
R.id.create -> {
|
||||
@@ -215,10 +277,33 @@ class DecodeFragment : Fragment() {
|
||||
)
|
||||
true
|
||||
}
|
||||
R.id.remove -> {
|
||||
db.removeScan(id)
|
||||
backOrFinish()
|
||||
true
|
||||
}
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
}
|
||||
|
||||
private fun textOrHex() = if (isBinary) {
|
||||
raw.toHexString()
|
||||
} else {
|
||||
content
|
||||
}
|
||||
|
||||
private fun copyPasswordToClipboard() {
|
||||
val ac = action
|
||||
if (ac is WifiAction) {
|
||||
ac.password?.let { password ->
|
||||
activity?.apply {
|
||||
copyToClipboard(password)
|
||||
toast(R.string.copied_password_to_clipboard)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun copyToClipboard(text: String) {
|
||||
activity?.apply {
|
||||
copyToClipboard(text)
|
||||
@@ -226,24 +311,43 @@ class DecodeFragment : Fragment() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun maybeBackOrFinish() {
|
||||
if (closeAutomatically) {
|
||||
backOrFinish()
|
||||
}
|
||||
}
|
||||
|
||||
private fun backOrFinish() {
|
||||
val fm = fragmentManager
|
||||
if (fm != null && fm.backStackEntryCount > 0) {
|
||||
fm.popBackStack()
|
||||
} else {
|
||||
activity?.finish()
|
||||
}
|
||||
}
|
||||
|
||||
private fun executeAction(content: ByteArray) {
|
||||
val ac = activity ?: return
|
||||
if (content.isNotEmpty()) {
|
||||
if (action is WifiAction &&
|
||||
Build.VERSION.SDK_INT < Build.VERSION_CODES.Q &&
|
||||
!hasLocationPermission(ac)
|
||||
!hasLocationPermission(ac) { executeAction(content) }
|
||||
) {
|
||||
return
|
||||
}
|
||||
scope.launch {
|
||||
action.execute(ac, content)
|
||||
maybeBackOrFinish()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun askForFileNameAndSave(raw: ByteArray) {
|
||||
val ac = activity ?: return
|
||||
if (!hasWritePermission(ac)) {
|
||||
// Write permission is only required before Android Q.
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q &&
|
||||
!hasWritePermission(ac) { askForFileNameAndSave(raw) }
|
||||
) {
|
||||
return
|
||||
}
|
||||
scope.launch(Dispatchers.Main) {
|
||||
@@ -259,11 +363,6 @@ class DecodeFragment : Fragment() {
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
parentJob.cancel()
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val SCAN = "scan"
|
||||
|
||||
@@ -277,7 +376,7 @@ class DecodeFragment : Fragment() {
|
||||
}
|
||||
}
|
||||
|
||||
fun hexDump(bytes: ByteArray, charsPerLine: Int = 33): String {
|
||||
private fun hexDump(bytes: ByteArray, charsPerLine: Int = 33): String {
|
||||
if (charsPerLine < 4 || bytes.isEmpty()) {
|
||||
return ""
|
||||
}
|
||||
@@ -290,7 +389,7 @@ fun hexDump(bytes: ByteArray, charsPerLine: Int = 33): String {
|
||||
while (true) {
|
||||
val ord = bytes[i]
|
||||
hex.append(String.format("%02X ", ord))
|
||||
ascii.append(if (ord > 31) ord.toChar() else " ")
|
||||
ascii.append(if (ord > 31) ord.toInt().toChar() else " ")
|
||||
++i
|
||||
val posInLine = i % itemsPerLine
|
||||
val atEnd = i >= len
|
||||
|
||||
@@ -2,17 +2,25 @@ package de.markusfisch.android.binaryeye.fragment
|
||||
|
||||
import android.os.Bundle
|
||||
import android.support.v4.app.Fragment
|
||||
import android.view.*
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.*
|
||||
import com.google.zxing.BarcodeFormat
|
||||
import com.google.zxing.EncodeHintType
|
||||
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.adapter.prettifyFormatName
|
||||
import de.markusfisch.android.binaryeye.app.addFragment
|
||||
import de.markusfisch.android.binaryeye.app.hideSoftKeyboard
|
||||
import de.markusfisch.android.binaryeye.app.prefs
|
||||
import de.markusfisch.android.binaryeye.view.hideSoftKeyboard
|
||||
import de.markusfisch.android.binaryeye.view.setPaddingFromWindowInsets
|
||||
import java.util.*
|
||||
|
||||
class EncodeFragment : Fragment() {
|
||||
private lateinit var formatView: Spinner
|
||||
private lateinit var errorCorrectionLabel: TextView
|
||||
private lateinit var errorCorrectionLevel: Spinner
|
||||
private lateinit var sizeView: TextView
|
||||
private lateinit var sizeBarView: SeekBar
|
||||
private lateinit var contentView: EditText
|
||||
@@ -31,11 +39,6 @@ class EncodeFragment : Fragment() {
|
||||
BarcodeFormat.UPC_A
|
||||
)
|
||||
|
||||
override fun onCreate(state: Bundle?) {
|
||||
super.onCreate(state)
|
||||
setHasOptionsMenu(true)
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
@@ -51,11 +54,38 @@ class EncodeFragment : Fragment() {
|
||||
)
|
||||
|
||||
formatView = view.findViewById(R.id.format)
|
||||
formatView.adapter = ArrayAdapter(
|
||||
val formatAdapter = ArrayAdapter(
|
||||
ac,
|
||||
android.R.layout.simple_list_item_1,
|
||||
writers.map { it.name }
|
||||
android.R.layout.simple_spinner_item,
|
||||
writers.map { prettifyFormatName(it.name) }
|
||||
)
|
||||
formatAdapter.setDropDownViewResource(
|
||||
android.R.layout.simple_spinner_dropdown_item
|
||||
)
|
||||
formatView.adapter = formatAdapter
|
||||
formatView.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
|
||||
override fun onItemSelected(
|
||||
parentView: AdapterView<*>?,
|
||||
selectedItemView: View?,
|
||||
position: Int,
|
||||
id: Long
|
||||
) {
|
||||
val visibility = if (
|
||||
writers[position] == BarcodeFormat.QR_CODE
|
||||
) {
|
||||
View.VISIBLE
|
||||
} else {
|
||||
View.GONE
|
||||
}
|
||||
errorCorrectionLabel.visibility = visibility
|
||||
errorCorrectionLevel.visibility = visibility
|
||||
}
|
||||
|
||||
override fun onNothingSelected(parentView: AdapterView<*>?) {}
|
||||
}
|
||||
|
||||
errorCorrectionLabel = view.findViewById(R.id.error_correction_label)
|
||||
errorCorrectionLevel = view.findViewById(R.id.error_correction_level)
|
||||
|
||||
sizeView = view.findViewById(R.id.size_display)
|
||||
sizeBarView = view.findViewById(R.id.size_bar)
|
||||
@@ -94,22 +124,17 @@ class EncodeFragment : Fragment() {
|
||||
prefs.indexOfLastSelectedFormat = formatView.selectedItemPosition
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
inflater.inflate(R.menu.fragment_encode, menu)
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
return when (item.itemId) {
|
||||
R.id.next -> {
|
||||
encode()
|
||||
true
|
||||
}
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
}
|
||||
|
||||
private fun encode() {
|
||||
val hints = EnumMap<EncodeHintType, Any>(EncodeHintType::class.java)
|
||||
val format = writers[formatView.selectedItemPosition]
|
||||
if (format == BarcodeFormat.QR_CODE) {
|
||||
hints[EncodeHintType.ERROR_CORRECTION] = arrayListOf(
|
||||
ErrorCorrectionLevel.L,
|
||||
ErrorCorrectionLevel.M,
|
||||
ErrorCorrectionLevel.Q,
|
||||
ErrorCorrectionLevel.H
|
||||
)[errorCorrectionLevel.selectedItemPosition]
|
||||
}
|
||||
val size = getSize(sizeBarView.progress)
|
||||
val content = contentView.text.toString()
|
||||
if (content.isEmpty()) {
|
||||
@@ -122,7 +147,7 @@ class EncodeFragment : Fragment() {
|
||||
activity?.hideSoftKeyboard(contentView)
|
||||
addFragment(
|
||||
fragmentManager,
|
||||
BarcodeFragment.newInstance(content, format, size)
|
||||
BarcodeFragment.newInstance(content, format, size, hints)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,12 +21,15 @@ import android.widget.ListView
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.adapter.ScansAdapter
|
||||
import de.markusfisch.android.binaryeye.app.*
|
||||
import de.markusfisch.android.binaryeye.data.Database
|
||||
import de.markusfisch.android.binaryeye.data.exportCsv
|
||||
import de.markusfisch.android.binaryeye.data.exportDatabase
|
||||
import de.markusfisch.android.binaryeye.data.exportJson
|
||||
import de.markusfisch.android.binaryeye.view.setPaddingFromWindowInsets
|
||||
import de.markusfisch.android.binaryeye.view.useVisibility
|
||||
import de.markusfisch.android.binaryeye.content.copyToClipboard
|
||||
import de.markusfisch.android.binaryeye.content.shareText
|
||||
import de.markusfisch.android.binaryeye.database.Database
|
||||
import de.markusfisch.android.binaryeye.database.exportCsv
|
||||
import de.markusfisch.android.binaryeye.database.exportDatabase
|
||||
import de.markusfisch.android.binaryeye.database.exportJson
|
||||
import de.markusfisch.android.binaryeye.io.askForFileName
|
||||
import de.markusfisch.android.binaryeye.io.toSaveResult
|
||||
import de.markusfisch.android.binaryeye.view.*
|
||||
import de.markusfisch.android.binaryeye.widget.toast
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
@@ -151,7 +154,7 @@ class HistoryFragment : Fragment() {
|
||||
}
|
||||
true
|
||||
}
|
||||
listView.setOnScrollListener(systemBarScrollListener)
|
||||
listView.setOnScrollListener(systemBarListViewScrollListener)
|
||||
|
||||
fab = view.findViewById(R.id.share)
|
||||
fab.setOnClickListener { v ->
|
||||
@@ -221,7 +224,7 @@ class HistoryFragment : Fragment() {
|
||||
true
|
||||
}
|
||||
R.id.export_history -> {
|
||||
askToExportToFile(context)
|
||||
askToExportToFile()
|
||||
true
|
||||
}
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
@@ -262,7 +265,7 @@ class HistoryFragment : Fragment() {
|
||||
View.GONE
|
||||
}
|
||||
cursor?.let { cursor ->
|
||||
// close previous cursor
|
||||
// Close previous cursor.
|
||||
scansAdapter?.also { it.changeCursor(null) }
|
||||
scansAdapter = ScansAdapter(ac, cursor)
|
||||
listView.adapter = scansAdapter
|
||||
@@ -295,7 +298,7 @@ class HistoryFragment : Fragment() {
|
||||
DecodeFragment.newInstance(scan)
|
||||
)
|
||||
} catch (e: IllegalArgumentException) {
|
||||
// ignore, can never happen
|
||||
// Ignore, can never happen.
|
||||
}
|
||||
}
|
||||
|
||||
@@ -304,7 +307,7 @@ class HistoryFragment : Fragment() {
|
||||
return cursor?.getString(cursor.getColumnIndex(Database.SCANS_NAME))
|
||||
}
|
||||
|
||||
// dialogs don't have a parent layout
|
||||
// Dialogs don't have a parent layout.
|
||||
@SuppressLint("InflateParams")
|
||||
private fun askForName(context: Context, id: Long, text: String?) {
|
||||
val view = LayoutInflater.from(context).inflate(
|
||||
@@ -357,41 +360,46 @@ class HistoryFragment : Fragment() {
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun askToExportToFile(context: Context) = scope.launch {
|
||||
val ac = activity ?: return@launch
|
||||
progressView.useVisibility {
|
||||
if (!hasWritePermission(ac)) {
|
||||
return@useVisibility
|
||||
}
|
||||
val options = context.resources.getStringArray(
|
||||
R.array.export_options_values
|
||||
)
|
||||
val delimiter = alertDialog<String>(context) { resume ->
|
||||
setTitle(R.string.export_as)
|
||||
setItems(R.array.export_options_names) { _, which ->
|
||||
resume(options[which])
|
||||
private fun askToExportToFile() {
|
||||
scope.launch {
|
||||
val ac = activity ?: return@launch
|
||||
progressView.useVisibility {
|
||||
// Write permission is only required before Android Q.
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q &&
|
||||
!hasWritePermission(ac) { askToExportToFile() }
|
||||
) {
|
||||
return@useVisibility
|
||||
}
|
||||
} ?: return@useVisibility
|
||||
val name = withContext(Dispatchers.Main) {
|
||||
ac.askForFileName(
|
||||
when (delimiter) {
|
||||
"db" -> ".db"
|
||||
"json" -> ".json"
|
||||
else -> ".csv"
|
||||
}
|
||||
val options = context.resources.getStringArray(
|
||||
R.array.export_options_values
|
||||
)
|
||||
} ?: return@useVisibility
|
||||
val message = when (delimiter) {
|
||||
"db" -> exportDatabase(ac, name)
|
||||
else -> db.getScansDetailed(filter)?.use {
|
||||
when (delimiter) {
|
||||
"json" -> exportJson(context, name, it)
|
||||
else -> exportCsv(context, name, it, delimiter)
|
||||
val delimiter = alertDialog<String>(context) { resume ->
|
||||
setTitle(R.string.export_as)
|
||||
setItems(R.array.export_options_names) { _, which ->
|
||||
resume(options[which])
|
||||
}
|
||||
} ?: false
|
||||
}.toSaveResult()
|
||||
withContext(Dispatchers.Main) {
|
||||
ac.toast(message)
|
||||
} ?: return@useVisibility
|
||||
val name = withContext(Dispatchers.Main) {
|
||||
ac.askForFileName(
|
||||
when (delimiter) {
|
||||
"db" -> ".db"
|
||||
"json" -> ".json"
|
||||
else -> ".csv"
|
||||
}
|
||||
)
|
||||
} ?: return@useVisibility
|
||||
val message = when (delimiter) {
|
||||
"db" -> exportDatabase(ac, name)
|
||||
else -> db.getScansDetailed(filter)?.use {
|
||||
when (delimiter) {
|
||||
"json" -> exportJson(context, name, it)
|
||||
else -> exportCsv(context, name, it, delimiter)
|
||||
}
|
||||
} ?: false
|
||||
}.toSaveResult()
|
||||
withContext(Dispatchers.Main) {
|
||||
ac.toast(message)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+142
-102
@@ -1,114 +1,154 @@
|
||||
package de.markusfisch.android.binaryeye.fragment
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.AlertDialog
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.SharedPreferences
|
||||
import android.content.SharedPreferences.OnSharedPreferenceChangeListener
|
||||
import android.net.wifi.WifiManager
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.support.v4.app.Fragment
|
||||
import android.support.v7.widget.SwitchCompat
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.EditText
|
||||
import android.support.annotation.RequiresApi
|
||||
import android.support.v14.preference.MultiSelectListPreference
|
||||
import android.support.v7.preference.ListPreference
|
||||
import android.support.v7.preference.Preference
|
||||
import android.support.v7.preference.PreferenceFragmentCompat
|
||||
import android.support.v7.preference.PreferenceGroup
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.actions.wifi.removeNetworkSuggestions
|
||||
import de.markusfisch.android.binaryeye.activity.SplashActivity
|
||||
import de.markusfisch.android.binaryeye.app.prefs
|
||||
import de.markusfisch.android.binaryeye.preference.UrlPreference
|
||||
import de.markusfisch.android.binaryeye.view.setPaddingFromWindowInsets
|
||||
import de.markusfisch.android.binaryeye.view.systemBarRecyclerViewScrollListener
|
||||
import de.markusfisch.android.binaryeye.widget.toast
|
||||
|
||||
class PreferencesFragment : Fragment() {
|
||||
private lateinit var showCropHandleSwitch: SwitchCompat
|
||||
private lateinit var zoomBySwipingSwitch: SwitchCompat
|
||||
private lateinit var autoResizeSwitch: SwitchCompat
|
||||
private lateinit var tryHarderSwitch: SwitchCompat
|
||||
private lateinit var vibrateSwitch: SwitchCompat
|
||||
private lateinit var useHistorySwitch: SwitchCompat
|
||||
private lateinit var ignoreConsecutiveDuplicatesSwitch: SwitchCompat
|
||||
private lateinit var openImmediatelySwitch: SwitchCompat
|
||||
private lateinit var showMetaDataSwitch: SwitchCompat
|
||||
private lateinit var showHexDumpSwitch: SwitchCompat
|
||||
private lateinit var openWithUrlInput: EditText
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
state: Bundle?
|
||||
): View? {
|
||||
activity?.setTitle(R.string.preferences)
|
||||
|
||||
val view = inflater.inflate(
|
||||
R.layout.fragment_preferences,
|
||||
container,
|
||||
false
|
||||
)
|
||||
|
||||
showCropHandleSwitch = view.findViewById(R.id.show_crop_handle)
|
||||
if (prefs.showCropHandle) {
|
||||
showCropHandleSwitch.toggle()
|
||||
class PreferencesFragment : PreferenceFragmentCompat() {
|
||||
private val changeListener = object : OnSharedPreferenceChangeListener {
|
||||
override fun onSharedPreferenceChanged(
|
||||
sharedPreferences: SharedPreferences,
|
||||
key: String
|
||||
) {
|
||||
val preference = findPreference(key) ?: return
|
||||
prefs.update()
|
||||
if (preference.key == "custom_locale") {
|
||||
activity?.restartApp()
|
||||
} else {
|
||||
setSummary(preference)
|
||||
}
|
||||
}
|
||||
|
||||
zoomBySwipingSwitch = view.findViewById(R.id.zoom_by_swiping)
|
||||
if (prefs.zoomBySwiping) {
|
||||
zoomBySwipingSwitch.toggle()
|
||||
}
|
||||
|
||||
autoResizeSwitch = view.findViewById(R.id.auto_rotate)
|
||||
if (prefs.autoRotate) {
|
||||
autoResizeSwitch.toggle()
|
||||
}
|
||||
|
||||
tryHarderSwitch = view.findViewById(R.id.try_harder)
|
||||
if (prefs.tryHarder) {
|
||||
tryHarderSwitch.toggle()
|
||||
}
|
||||
|
||||
vibrateSwitch = view.findViewById(R.id.vibrate)
|
||||
if (prefs.vibrate) {
|
||||
vibrateSwitch.toggle()
|
||||
}
|
||||
|
||||
useHistorySwitch = view.findViewById(R.id.use_history)
|
||||
if (prefs.useHistory) {
|
||||
useHistorySwitch.toggle()
|
||||
}
|
||||
|
||||
ignoreConsecutiveDuplicatesSwitch = view.findViewById(
|
||||
R.id.ignore_consecutive_duplicates
|
||||
)
|
||||
if (prefs.ignoreConsecutiveDuplicates) {
|
||||
ignoreConsecutiveDuplicatesSwitch.toggle()
|
||||
}
|
||||
|
||||
openImmediatelySwitch = view.findViewById(R.id.open_immediately)
|
||||
if (prefs.openImmediately) {
|
||||
openImmediatelySwitch.toggle()
|
||||
}
|
||||
|
||||
showMetaDataSwitch = view.findViewById(R.id.show_meta_data)
|
||||
if (prefs.showMetaData) {
|
||||
showMetaDataSwitch.toggle()
|
||||
}
|
||||
|
||||
showHexDumpSwitch = view.findViewById(R.id.show_hex_dump)
|
||||
if (prefs.showHexDump) {
|
||||
showHexDumpSwitch.toggle()
|
||||
}
|
||||
|
||||
openWithUrlInput = view.findViewById(R.id.open_with_url)
|
||||
openWithUrlInput.setText(prefs.openWithUrl)
|
||||
|
||||
(view.findViewById(R.id.scroll_view) as View).setPaddingFromWindowInsets()
|
||||
|
||||
return view
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
super.onStop()
|
||||
prefs.showCropHandle = showCropHandleSwitch.isChecked
|
||||
prefs.zoomBySwiping = zoomBySwipingSwitch.isChecked
|
||||
prefs.autoRotate = autoResizeSwitch.isChecked
|
||||
prefs.tryHarder = tryHarderSwitch.isChecked
|
||||
prefs.vibrate = vibrateSwitch.isChecked
|
||||
prefs.useHistory = useHistorySwitch.isChecked
|
||||
prefs.ignoreConsecutiveDuplicates = ignoreConsecutiveDuplicatesSwitch.isChecked
|
||||
prefs.openImmediately = openImmediatelySwitch.isChecked
|
||||
prefs.showMetaData = showMetaDataSwitch.isChecked
|
||||
prefs.showHexDump = showHexDumpSwitch.isChecked
|
||||
prefs.openWithUrl = openWithUrlInput.text.toString()
|
||||
override fun onCreatePreferences(state: Bundle?, rootKey: String?) {
|
||||
addPreferencesFromResource(R.xml.preferences)
|
||||
activity?.setTitle(R.string.preferences)
|
||||
wireClearNetworkPreferences()
|
||||
}
|
||||
|
||||
private fun wireClearNetworkPreferences() {
|
||||
val pref = findPreference("clear_network_suggestions") ?: return
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
||||
pref.isVisible = false
|
||||
} else {
|
||||
pref.setOnPreferenceClickListener {
|
||||
askToClearNetworkSuggestions(context)
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.Q)
|
||||
private fun askToClearNetworkSuggestions(context: Context) {
|
||||
AlertDialog.Builder(context)
|
||||
.setMessage(R.string.really_remove_all_networks)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
clearNetworkSuggestions(context)
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
||||
.show()
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.Q)
|
||||
private fun clearNetworkSuggestions(context: Context) {
|
||||
context.toast(
|
||||
if (
|
||||
removeNetworkSuggestions(context) == WifiManager.STATUS_NETWORK_SUGGESTIONS_SUCCESS
|
||||
) {
|
||||
R.string.clear_network_suggestions_success
|
||||
} else {
|
||||
R.string.clear_network_suggestions_nothing_to_remove
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
listView.setPaddingFromWindowInsets()
|
||||
listView.removeOnScrollListener(systemBarRecyclerViewScrollListener)
|
||||
listView.addOnScrollListener(systemBarRecyclerViewScrollListener)
|
||||
preferenceScreen.sharedPreferences
|
||||
.registerOnSharedPreferenceChangeListener(changeListener)
|
||||
setSummaries(preferenceScreen)
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
preferenceScreen.sharedPreferences
|
||||
.unregisterOnSharedPreferenceChangeListener(changeListener)
|
||||
}
|
||||
|
||||
override fun onDisplayPreferenceDialog(preference: Preference) {
|
||||
if (preference is UrlPreference) {
|
||||
val fm = fragmentManager
|
||||
UrlDialogFragment.newInstance(preference.key).apply {
|
||||
setTargetFragment(this@PreferencesFragment, 0)
|
||||
show(fm, null)
|
||||
}
|
||||
} else {
|
||||
super.onDisplayPreferenceDialog(preference)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setSummaries(screen: PreferenceGroup) {
|
||||
var i = screen.preferenceCount
|
||||
while (i-- > 0) {
|
||||
setSummary(screen.getPreference(i))
|
||||
}
|
||||
}
|
||||
|
||||
private fun setSummary(preference: Preference) {
|
||||
when (preference) {
|
||||
is UrlPreference -> {
|
||||
preference.setSummary(preference.getUrl())
|
||||
}
|
||||
is ListPreference -> {
|
||||
preference.setSummary(preference.entry)
|
||||
}
|
||||
is MultiSelectListPreference -> {
|
||||
preference.setSummary(
|
||||
preference.values.joinToString(", ") {
|
||||
it.replace(Regex("_"), " ")
|
||||
}
|
||||
)
|
||||
}
|
||||
is PreferenceGroup -> {
|
||||
setSummaries(preference)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun Activity.restartApp() {
|
||||
val intent = Intent(this, SplashActivity::class.java)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
|
||||
intent.addFlags(
|
||||
Intent.FLAG_ACTIVITY_NEW_TASK or
|
||||
Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||
)
|
||||
}
|
||||
startActivity(intent)
|
||||
finish()
|
||||
// Restart to begin with an unmodified Locale to follow system settings.
|
||||
Runtime.getRuntime().exit(0)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
package de.markusfisch.android.binaryeye.fragment
|
||||
|
||||
import android.os.Bundle
|
||||
import android.support.v7.preference.PreferenceDialogFragmentCompat
|
||||
import android.view.View
|
||||
import android.widget.TextView
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.app.prefs
|
||||
import de.markusfisch.android.binaryeye.database.Scan
|
||||
import de.markusfisch.android.binaryeye.net.sendAsync
|
||||
import de.markusfisch.android.binaryeye.preference.UrlPreference
|
||||
|
||||
class UrlDialogFragment : PreferenceDialogFragmentCompat() {
|
||||
private var urlView: TextView? = null
|
||||
private var testButton: TextView? = null
|
||||
|
||||
override fun onBindDialogView(view: View?) {
|
||||
super.onBindDialogView(view)
|
||||
urlView = view?.findViewById(R.id.url)
|
||||
testButton = view?.findViewById(R.id.test_url)
|
||||
testButton?.setOnClickListener {
|
||||
testUrl(testButton)
|
||||
}
|
||||
urlView?.text = urlPreference().getUrl()
|
||||
urlView?.hint = getString(
|
||||
when (prefs.sendScanType) {
|
||||
"0" -> R.string.url_hint_add_content
|
||||
else -> R.string.url_hint
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
override fun onDialogClosed(positiveResult: Boolean) {
|
||||
if (positiveResult) {
|
||||
urlPreference().setUrl(getUrl())
|
||||
}
|
||||
}
|
||||
|
||||
private fun getUrl() = completeUrl(urlView?.text.toString())
|
||||
|
||||
private fun urlPreference() = preference as UrlPreference
|
||||
|
||||
private fun testUrl(textView: TextView?) {
|
||||
val url = getUrl()
|
||||
if (url.isEmpty()) {
|
||||
return
|
||||
}
|
||||
textView ?: return
|
||||
textView.text = "…"
|
||||
Scan(
|
||||
"test",
|
||||
null,
|
||||
"none",
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
).sendAsync(url, prefs.sendScanType) { code, body ->
|
||||
textView.text = when {
|
||||
code != null -> "$code"
|
||||
body != null -> body
|
||||
else -> getString(R.string.background_request_failed)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun newInstance(key: String): UrlDialogFragment {
|
||||
val args = Bundle()
|
||||
args.putString(ARG_KEY, key)
|
||||
val fragment = UrlDialogFragment()
|
||||
fragment.arguments = args
|
||||
return fragment
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun completeUrl(template: String): String {
|
||||
var s = template.trim()
|
||||
if (s.isEmpty()) {
|
||||
return ""
|
||||
}
|
||||
if (!s.startsWith("http")) {
|
||||
s = "http://${s}"
|
||||
}
|
||||
if (prefs.sendScanType == "0" &&
|
||||
!s.matches(".*/[a-zA-Z._-]*\\?[a-zA-Z0-9&=_-]+=$".toRegex())
|
||||
) {
|
||||
s = "${s}?content="
|
||||
}
|
||||
return s
|
||||
}
|
||||
@@ -7,6 +7,8 @@ import android.graphics.Matrix
|
||||
import android.graphics.RectF
|
||||
import android.net.Uri
|
||||
import java.io.IOException
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
fun loadImageUri(cr: ContentResolver, uri: Uri): Bitmap? = try {
|
||||
@@ -51,12 +53,14 @@ fun crop(bitmap: Bitmap, rect: RectF, rotation: Float) = try {
|
||||
val erected = erect(bitmap, rotation)
|
||||
val w = erected.width
|
||||
val h = erected.height
|
||||
val x = max(0, (rect.left * w).roundToInt())
|
||||
val y = max(0, (rect.top * h).roundToInt())
|
||||
Bitmap.createBitmap(
|
||||
erected,
|
||||
(rect.left * w).roundToInt(),
|
||||
(rect.top * h).roundToInt(),
|
||||
(rect.width() * w).roundToInt(),
|
||||
(rect.height() * h).roundToInt()
|
||||
x,
|
||||
y,
|
||||
min(w - x, (rect.width() * w).roundToInt()),
|
||||
min(h - y, (rect.height() * h).roundToInt())
|
||||
)
|
||||
} catch (e: OutOfMemoryError) {
|
||||
null
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
package de.markusfisch.android.binaryeye.graphics
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Paint
|
||||
import android.graphics.Point
|
||||
import android.support.v4.content.ContextCompat
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
|
||||
class Candidates(context: Context) {
|
||||
private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
|
||||
private val radius = 8f * context.resources.displayMetrics.density
|
||||
|
||||
init {
|
||||
paint.color = ContextCompat.getColor(
|
||||
context,
|
||||
R.color.candidate
|
||||
)
|
||||
paint.style = Paint.Style.FILL
|
||||
}
|
||||
|
||||
fun draw(canvas: Canvas, points: List<Point>) {
|
||||
for (point in points) {
|
||||
canvas.drawCircle(
|
||||
point.x.toFloat(),
|
||||
point.y.toFloat(),
|
||||
radius,
|
||||
paint
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package de.markusfisch.android.binaryeye.graphics
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.DashPathEffect
|
||||
import android.graphics.Paint
|
||||
import android.support.v4.content.ContextCompat
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
|
||||
fun Context.getDashedBorderPaint() = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
val dp = resources.displayMetrics.density
|
||||
color = ContextCompat.getColor(applicationContext, R.color.crop_bound)
|
||||
style = Paint.Style.STROKE
|
||||
strokeWidth = dp * 2f
|
||||
pathEffect = DashPathEffect(floatArrayOf(10f * dp, 10f * dp), 0f)
|
||||
}
|
||||
@@ -1,85 +1,62 @@
|
||||
package de.markusfisch.android.binaryeye.graphics
|
||||
|
||||
import android.graphics.Point
|
||||
import android.graphics.Matrix
|
||||
import android.graphics.Rect
|
||||
import com.google.zxing.Result
|
||||
import com.google.zxing.ResultPoint
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
fun mapResult(
|
||||
fun mapResultToView(
|
||||
frameWidth: Int,
|
||||
frameHeight: Int,
|
||||
frameOrientation: Int,
|
||||
viewRect: Rect,
|
||||
result: Result
|
||||
): List<Point> = frameToView(
|
||||
resultPoints: Array<ResultPoint?>,
|
||||
targetArray: FloatArray
|
||||
): Int = getFrameToViewMatrix(
|
||||
frameWidth,
|
||||
frameHeight,
|
||||
frameOrientation,
|
||||
viewRect
|
||||
).map(
|
||||
result.resultPoints
|
||||
resultPoints,
|
||||
targetArray
|
||||
)
|
||||
|
||||
fun frameToView(
|
||||
fun getFrameToViewMatrix(
|
||||
frameWidth: Int,
|
||||
frameHeight: Int,
|
||||
frameOrientation: Int,
|
||||
viewRect: Rect
|
||||
) = Mapping(
|
||||
frameWidth,
|
||||
frameHeight,
|
||||
frameOrientation,
|
||||
viewRect.width().toFloat(),
|
||||
viewRect.height().toFloat(),
|
||||
viewRect.left,
|
||||
viewRect.top
|
||||
)
|
||||
|
||||
fun isPortrait(orientation: Int) = orientation == 90 || orientation == 270
|
||||
|
||||
data class Mapping(
|
||||
val frameWidth: Int,
|
||||
val frameHeight: Int,
|
||||
val frameOrientation: Int,
|
||||
val viewWidth: Float,
|
||||
val viewHeight: Float,
|
||||
val offsetX: Int,
|
||||
val offsetY: Int
|
||||
) {
|
||||
fun map(resultPoint: ResultPoint): Point {
|
||||
val point = Point(
|
||||
resultPoint.x.roundToInt(),
|
||||
resultPoint.y.roundToInt()
|
||||
)
|
||||
rotate(point)
|
||||
val w: Int
|
||||
val h: Int
|
||||
if (isPortrait(frameOrientation)) {
|
||||
w = frameHeight
|
||||
h = frameWidth
|
||||
} else {
|
||||
w = frameWidth
|
||||
h = frameHeight
|
||||
}
|
||||
val ratioX = viewWidth / w.toFloat()
|
||||
val ratioY = viewHeight / h.toFloat()
|
||||
point.set(
|
||||
(point.x * ratioX).roundToInt() + offsetX,
|
||||
(point.y * ratioY).roundToInt() + offsetY
|
||||
)
|
||||
return point
|
||||
}
|
||||
|
||||
fun map(points: Array<ResultPoint?>): List<Point> =
|
||||
// because ZXing apparently returns null in this array sometimes
|
||||
points.filterNotNull().map { map(it) }
|
||||
|
||||
private fun rotate(point: Point) = when (frameOrientation) {
|
||||
90 -> point.set(frameHeight - point.y, point.x)
|
||||
180 -> point.set(frameWidth - point.x, frameHeight - point.y)
|
||||
270 -> point.set(point.y, frameWidth - point.x)
|
||||
else -> {
|
||||
}
|
||||
}
|
||||
) = Matrix().apply {
|
||||
// Normalize to frame dimensions for rotation.
|
||||
postScale(
|
||||
1f / frameWidth,
|
||||
1f / frameHeight
|
||||
)
|
||||
// Rotate around center.
|
||||
postRotate(frameOrientation.toFloat(), 0.5f, 0.5f)
|
||||
// Scale up to view size.
|
||||
postScale(viewRect.width().toFloat(), viewRect.height().toFloat())
|
||||
// Apply view displacement.
|
||||
postTranslate(viewRect.left.toFloat(), viewRect.top.toFloat())
|
||||
}
|
||||
|
||||
fun Matrix.map(
|
||||
resultPoints: Array<ResultPoint?>,
|
||||
targetArray: FloatArray
|
||||
): Int {
|
||||
val max = targetArray.size
|
||||
var i = 0
|
||||
for (resultPoint in resultPoints) {
|
||||
// Because ZXing apparently returns null in this array sometimes.
|
||||
if (resultPoint == null) {
|
||||
continue
|
||||
}
|
||||
targetArray[i++] = resultPoint.x
|
||||
targetArray[i++] = resultPoint.y
|
||||
if (i >= max) {
|
||||
break
|
||||
}
|
||||
}
|
||||
mapPoints(targetArray, 0, targetArray, 0, i)
|
||||
return i
|
||||
}
|
||||
|
||||
+2
-1
@@ -1,4 +1,4 @@
|
||||
package de.markusfisch.android.binaryeye.app
|
||||
package de.markusfisch.android.binaryeye.io
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.ContentValues
|
||||
@@ -9,6 +9,7 @@ import android.provider.MediaStore
|
||||
import android.support.annotation.MainThread
|
||||
import android.widget.EditText
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.app.alertDialog
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
@@ -0,0 +1,122 @@
|
||||
package de.markusfisch.android.binaryeye.net
|
||||
|
||||
import de.markusfisch.android.binaryeye.app.toHexString
|
||||
import de.markusfisch.android.binaryeye.database.Scan
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.json.JSONObject
|
||||
import java.io.BufferedReader
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.io.InputStreamReader
|
||||
import java.net.HttpURLConnection
|
||||
import java.net.ProtocolException
|
||||
import java.net.URL
|
||||
import java.net.URLEncoder
|
||||
|
||||
fun Scan.sendAsync(url: String, type: String, callback: (Int?, String?) -> Unit) {
|
||||
CoroutineScope(Dispatchers.IO).launch(Dispatchers.IO) {
|
||||
val response = send(url, type)
|
||||
withContext(Dispatchers.Main) {
|
||||
callback(response.code, response.body)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun Scan.send(url: String, type: String): Response {
|
||||
return when (type) {
|
||||
"1" -> request(
|
||||
url + "?" + asUrlArguments()
|
||||
)
|
||||
"2" -> request(url) { con ->
|
||||
con.requestMethod = "POST"
|
||||
con.setRequestProperty("Content-Type", "application/x-www-form-urlencoded")
|
||||
con.outputStream.apply {
|
||||
write(asUrlArguments().toByteArray())
|
||||
close()
|
||||
}
|
||||
}
|
||||
"3" -> request(url) { con ->
|
||||
con.requestMethod = "POST"
|
||||
con.setRequestProperty("Content-Type", "application/json")
|
||||
con.outputStream.apply {
|
||||
write(asJson().toString().toByteArray())
|
||||
close()
|
||||
}
|
||||
}
|
||||
else -> request(
|
||||
url + urlEncode(content)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun Scan.asUrlArguments(): String = getMap().map { (k, v) ->
|
||||
"${k}=${urlEncode(v)}"
|
||||
}.joinToString("&")
|
||||
|
||||
private fun Scan.asJson() = JSONObject().apply {
|
||||
getMap().forEach { (k, v) -> put(k, v) }
|
||||
}
|
||||
|
||||
private fun urlEncode(s: String) = URLEncoder.encode(s, "utf-8")
|
||||
|
||||
private fun Scan.getMap(): Map<String, String> = mapOf(
|
||||
"content" to content,
|
||||
"raw" to raw?.toHexString(),
|
||||
"format" to format,
|
||||
"errorCorrectionLevel" to errorCorrectionLevel,
|
||||
"issueNumber" to issueNumber,
|
||||
"orientation" to orientation,
|
||||
"otherMetaData" to otherMetaData,
|
||||
"pdf417ExtraMetaData" to pdf417ExtraMetaData,
|
||||
"possibleCountry" to possibleCountry,
|
||||
"suggestedPrice" to suggestedPrice,
|
||||
"upcEanExtension" to upcEanExtension,
|
||||
"timestamp" to timestamp
|
||||
).filterNullValues()
|
||||
|
||||
private fun Map<String, String?>.filterNullValues() =
|
||||
filterValues { it != null }.mapValues { it.value as String }
|
||||
|
||||
private fun request(
|
||||
url: String,
|
||||
writer: ((HttpURLConnection) -> Any)? = null
|
||||
): Response {
|
||||
var con: HttpURLConnection? = null
|
||||
return try {
|
||||
con = URL(url).openConnection() as HttpURLConnection
|
||||
con.connectTimeout = 5000
|
||||
writer?.invoke(con)
|
||||
val body = con.inputStream.readHead()
|
||||
Response(con.responseCode, body)
|
||||
} catch (e: ProtocolException) {
|
||||
Response(null, e.message)
|
||||
} catch (e: IOException) {
|
||||
try {
|
||||
val body = con?.errorStream?.readHead() ?: e.message
|
||||
Response(con?.responseCode, body)
|
||||
} catch (e: IOException) {
|
||||
Response(null, e.message)
|
||||
}
|
||||
} finally {
|
||||
con?.disconnect()
|
||||
}
|
||||
}
|
||||
|
||||
private fun InputStream.readHead(): String {
|
||||
val sb = StringBuilder()
|
||||
val br = BufferedReader(InputStreamReader(this))
|
||||
var line = br.readLine()
|
||||
var i = 0
|
||||
while (line != null && sb.length < 240) {
|
||||
sb.append(line)
|
||||
line = br.readLine()
|
||||
++i
|
||||
}
|
||||
br.close()
|
||||
return sb.toString()
|
||||
}
|
||||
|
||||
private data class Response(val code: Int?, val body: String?)
|
||||
@@ -0,0 +1,49 @@
|
||||
package de.markusfisch.android.binaryeye.os
|
||||
|
||||
import android.os.Build
|
||||
import android.os.VibrationEffect
|
||||
import android.os.Vibrator
|
||||
import de.markusfisch.android.binaryeye.app.prefs
|
||||
|
||||
private const val MILLISECONDS = 100L
|
||||
private val errorPatternTimings = longArrayOf(
|
||||
50L, 100L, 50L, 100L, 50L
|
||||
)
|
||||
private val errorPatternAmplitudes = intArrayOf(
|
||||
255, 0, 255, 0, 255
|
||||
)
|
||||
|
||||
fun Vibrator.vibrate() {
|
||||
if (!prefs.vibrate) {
|
||||
return
|
||||
}
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
||||
@Suppress("DEPRECATION")
|
||||
vibrate(MILLISECONDS)
|
||||
} else {
|
||||
vibrate(
|
||||
VibrationEffect.createOneShot(
|
||||
MILLISECONDS,
|
||||
VibrationEffect.DEFAULT_AMPLITUDE
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun Vibrator.error() {
|
||||
if (!prefs.vibrate) {
|
||||
return
|
||||
}
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
||||
@Suppress("DEPRECATION")
|
||||
vibrate(errorPatternTimings, -1)
|
||||
} else {
|
||||
vibrate(
|
||||
VibrationEffect.createWaveform(
|
||||
errorPatternTimings,
|
||||
errorPatternAmplitudes,
|
||||
-1
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -2,11 +2,52 @@ package de.markusfisch.android.binaryeye.preference
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import android.os.Build
|
||||
import android.preference.PreferenceManager
|
||||
import android.support.annotation.RequiresApi
|
||||
|
||||
class Preferences {
|
||||
lateinit var preferences: SharedPreferences
|
||||
|
||||
var barcodeFormats = setOf(
|
||||
"AZTEC",
|
||||
"CODABAR",
|
||||
"CODE_39",
|
||||
"CODE_93",
|
||||
"CODE_128",
|
||||
"DATA_MATRIX",
|
||||
"EAN_8",
|
||||
"EAN_13",
|
||||
"ITF",
|
||||
"MAXICODE",
|
||||
"PDF_417",
|
||||
"QR_CODE",
|
||||
"RSS_14",
|
||||
"RSS_EXPANDED",
|
||||
"UPC_A",
|
||||
"UPC_E",
|
||||
"UPC_EAN_EXTENSION"
|
||||
)
|
||||
@RequiresApi(Build.VERSION_CODES.HONEYCOMB)
|
||||
set(value) {
|
||||
apply(BARCODE_FORMATS, value)
|
||||
field = value
|
||||
}
|
||||
var cropHandleX = -2 // -2 means set default roi.
|
||||
set(value) {
|
||||
apply(CROP_HANDLE_X, value)
|
||||
field = value
|
||||
}
|
||||
var cropHandleY = -2
|
||||
set(value) {
|
||||
apply(CROP_HANDLE_Y, value)
|
||||
field = value
|
||||
}
|
||||
var cropHandleOrientation = 0
|
||||
set(value) {
|
||||
apply(CROP_HANDLE_ORIENTATION, value)
|
||||
field = value
|
||||
}
|
||||
var showCropHandle = true
|
||||
set(value) {
|
||||
apply(SHOW_CROP_HANDLE, value)
|
||||
@@ -19,18 +60,39 @@ class Preferences {
|
||||
}
|
||||
var autoRotate = false
|
||||
set(value) {
|
||||
// immediately save this setting before it shouldn't change
|
||||
// on the fly while scanning
|
||||
// Immediately save this setting before it shouldn't change
|
||||
// on the fly while scanning.
|
||||
commit(AUTO_ROTATE, value)
|
||||
field = value
|
||||
}
|
||||
var tryHarder = false
|
||||
set(value) {
|
||||
// immediately save this setting because it's only ever read
|
||||
// before the camera is opened
|
||||
// Immediately save this setting because it's only ever read
|
||||
// before the camera is opened.
|
||||
commit(TRY_HARDER, value)
|
||||
field = value
|
||||
}
|
||||
var bulkMode = false
|
||||
set(value) {
|
||||
// Immediately save this setting because it's only ever read
|
||||
// before the camera is opened.
|
||||
commit(BULK_MODE, value)
|
||||
field = value
|
||||
}
|
||||
var bulkModeDelay = "500"
|
||||
set(value) {
|
||||
// Immediately save this setting because it's only ever read
|
||||
// before the camera is opened.
|
||||
commit(BULK_MODE_DELAY, value)
|
||||
field = value
|
||||
}
|
||||
var showToastInBulkMode = true
|
||||
set(value) {
|
||||
// Immediately save this setting because it's only ever read
|
||||
// before the camera is opened.
|
||||
commit(SHOW_TOAST_IN_BULK_MODE, value)
|
||||
field = value
|
||||
}
|
||||
var vibrate = true
|
||||
set(value) {
|
||||
apply(VIBRATE, value)
|
||||
@@ -46,6 +108,11 @@ class Preferences {
|
||||
apply(IGNORE_CONSECUTIVE_DUPLICATES, value)
|
||||
field = value
|
||||
}
|
||||
var copyImmediately = false
|
||||
set(value) {
|
||||
apply(COPY_IMMEDIATELY, value)
|
||||
field = value
|
||||
}
|
||||
var openImmediately = false
|
||||
set(value) {
|
||||
apply(OPEN_IMMEDIATELY, value)
|
||||
@@ -61,11 +128,36 @@ class Preferences {
|
||||
apply(SHOW_HEX_DUMP, value)
|
||||
field = value
|
||||
}
|
||||
var closeAutomatically = false
|
||||
set(value) {
|
||||
apply(CLOSE_AUTOMATICALLY, value)
|
||||
field = value
|
||||
}
|
||||
var defaultSearchUrl = ""
|
||||
set(value) {
|
||||
apply(DEFAULT_SEARCH_URL, value)
|
||||
field = value
|
||||
}
|
||||
var openWithUrl: String = ""
|
||||
set(value) {
|
||||
apply(OPEN_WITH_URL, value)
|
||||
field = value
|
||||
}
|
||||
var sendScanUrl: String = ""
|
||||
set(value) {
|
||||
apply(SEND_SCAN_URL, value)
|
||||
field = value
|
||||
}
|
||||
var sendScanType: String = "0"
|
||||
set(value) {
|
||||
apply(SEND_SCAN_TYPE, value)
|
||||
field = value
|
||||
}
|
||||
var customLocale: String = ""
|
||||
set(value) {
|
||||
apply(CUSTOM_LOCALE, value)
|
||||
field = value
|
||||
}
|
||||
var indexOfLastSelectedFormat: Int = 0
|
||||
set(value) {
|
||||
apply(INDEX_OF_LAST_SELECTED_FORMAT, value)
|
||||
@@ -73,8 +165,8 @@ class Preferences {
|
||||
}
|
||||
var forceCompat: Boolean = false
|
||||
set(value) {
|
||||
// since the app may be about to crash when forceCompat is set,
|
||||
// it's necessary to `commit()` this synchronously
|
||||
// Since the app may be about to crash when forceCompat is set,
|
||||
// it's necessary to `commit()` this synchronously.
|
||||
commit(FORCE_COMPAT, value)
|
||||
field = value
|
||||
}
|
||||
@@ -85,25 +177,68 @@ class Preferences {
|
||||
}
|
||||
|
||||
fun update() {
|
||||
showCropHandle = preferences.getBoolean(SHOW_CROP_HANDLE, showCropHandle)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
|
||||
preferences.getStringSet(BARCODE_FORMATS, barcodeFormats)?.let {
|
||||
barcodeFormats = it
|
||||
}
|
||||
}
|
||||
cropHandleX = preferences.getInt(CROP_HANDLE_X, cropHandleX)
|
||||
cropHandleY = preferences.getInt(CROP_HANDLE_Y, cropHandleY)
|
||||
cropHandleOrientation = preferences.getInt(
|
||||
CROP_HANDLE_ORIENTATION,
|
||||
cropHandleOrientation
|
||||
)
|
||||
showCropHandle = preferences.getBoolean(
|
||||
SHOW_CROP_HANDLE,
|
||||
showCropHandle
|
||||
)
|
||||
zoomBySwiping = preferences.getBoolean(ZOOM_BY_SWIPING, zoomBySwiping)
|
||||
autoRotate = preferences.getBoolean(AUTO_ROTATE, autoRotate)
|
||||
tryHarder = preferences.getBoolean(TRY_HARDER, tryHarder)
|
||||
bulkMode = preferences.getBoolean(BULK_MODE, bulkMode)
|
||||
bulkModeDelay = preferences.getString(
|
||||
BULK_MODE_DELAY,
|
||||
bulkModeDelay
|
||||
) ?: bulkModeDelay
|
||||
showToastInBulkMode = preferences.getBoolean(
|
||||
SHOW_TOAST_IN_BULK_MODE,
|
||||
showToastInBulkMode
|
||||
)
|
||||
vibrate = preferences.getBoolean(VIBRATE, vibrate)
|
||||
useHistory = preferences.getBoolean(USE_HISTORY, useHistory)
|
||||
ignoreConsecutiveDuplicates = preferences.getBoolean(
|
||||
IGNORE_CONSECUTIVE_DUPLICATES,
|
||||
ignoreConsecutiveDuplicates
|
||||
)
|
||||
copyImmediately = preferences.getBoolean(
|
||||
COPY_IMMEDIATELY,
|
||||
copyImmediately
|
||||
)
|
||||
openImmediately = preferences.getBoolean(
|
||||
OPEN_IMMEDIATELY,
|
||||
openImmediately
|
||||
)
|
||||
showMetaData = preferences.getBoolean(SHOW_META_DATA, showMetaData)
|
||||
showHexDump = preferences.getBoolean(SHOW_HEX_DUMP, showHexDump)
|
||||
closeAutomatically = preferences.getBoolean(
|
||||
CLOSE_AUTOMATICALLY,
|
||||
closeAutomatically
|
||||
)
|
||||
preferences.getString(DEFAULT_SEARCH_URL, defaultSearchUrl)?.also {
|
||||
defaultSearchUrl = it
|
||||
}
|
||||
preferences.getString(OPEN_WITH_URL, openWithUrl)?.also {
|
||||
openWithUrl = it
|
||||
}
|
||||
preferences.getString(SEND_SCAN_URL, sendScanUrl)?.also {
|
||||
sendScanUrl = it
|
||||
}
|
||||
preferences.getString(SEND_SCAN_TYPE, sendScanType)?.also {
|
||||
sendScanType = it
|
||||
}
|
||||
preferences.getString(CUSTOM_LOCALE, customLocale)?.also {
|
||||
customLocale = it
|
||||
}
|
||||
indexOfLastSelectedFormat = preferences.getInt(
|
||||
INDEX_OF_LAST_SELECTED_FORMAT,
|
||||
indexOfLastSelectedFormat
|
||||
@@ -114,10 +249,17 @@ class Preferences {
|
||||
private fun put(label: String, value: Boolean) =
|
||||
preferences.edit().putBoolean(label, value)
|
||||
|
||||
private fun put(label: String, value: String) =
|
||||
preferences.edit().putString(label, value)
|
||||
|
||||
private fun commit(label: String, value: Boolean) {
|
||||
put(label, value).commit()
|
||||
}
|
||||
|
||||
private fun commit(label: String, value: String) {
|
||||
put(label, value).commit()
|
||||
}
|
||||
|
||||
private fun apply(label: String, value: Boolean) {
|
||||
put(label, value).apply()
|
||||
}
|
||||
@@ -130,18 +272,36 @@ class Preferences {
|
||||
preferences.edit().putInt(label, value).apply()
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.HONEYCOMB)
|
||||
private fun apply(label: String, value: Set<String>) {
|
||||
preferences.edit().putStringSet(label, value).apply()
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val BARCODE_FORMATS = "barcode_formats"
|
||||
const val CROP_HANDLE_X = "crop_handle_x"
|
||||
const val CROP_HANDLE_Y = "crop_handle_y"
|
||||
const val CROP_HANDLE_ORIENTATION = "crop_handle_orientation"
|
||||
const val SHOW_CROP_HANDLE = "show_crop_handle"
|
||||
const val ZOOM_BY_SWIPING = "zoom_by_swiping"
|
||||
const val AUTO_ROTATE = "auto_rotate"
|
||||
const val TRY_HARDER = "try_harder"
|
||||
const val BULK_MODE = "bulk_mode"
|
||||
const val BULK_MODE_DELAY = "bulk_mode_delay"
|
||||
const val SHOW_TOAST_IN_BULK_MODE = "show_toast_in_bulk_mode"
|
||||
const val VIBRATE = "vibrate"
|
||||
const val USE_HISTORY = "use_history"
|
||||
const val IGNORE_CONSECUTIVE_DUPLICATES = "ignore_consecutive_duplicates"
|
||||
const val OPEN_IMMEDIATELY = "open_immediately"
|
||||
const val SHOW_HEX_DUMP = "show_hex_dump"
|
||||
const val COPY_IMMEDIATELY = "copy_immediately"
|
||||
const val SHOW_META_DATA = "show_meta_data"
|
||||
const val SHOW_HEX_DUMP = "show_hex_dump"
|
||||
const val CLOSE_AUTOMATICALLY = "close_automatically"
|
||||
const val DEFAULT_SEARCH_URL = "default_search_url"
|
||||
const val OPEN_WITH_URL = "open_with_url"
|
||||
const val SEND_SCAN_URL = "send_scan_url"
|
||||
const val SEND_SCAN_TYPE = "send_scan_type"
|
||||
const val CUSTOM_LOCALE = "custom_locale"
|
||||
const val INDEX_OF_LAST_SELECTED_FORMAT = "index_of_last_selected_format"
|
||||
const val FORCE_COMPAT = "force_compat"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
package de.markusfisch.android.binaryeye.preference
|
||||
|
||||
import android.content.Context
|
||||
import android.support.v7.preference.DialogPreference
|
||||
import android.util.AttributeSet
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
|
||||
class UrlPreference(
|
||||
context: Context?,
|
||||
attrs: AttributeSet?
|
||||
) : DialogPreference(context, attrs) {
|
||||
private var url: String? = null
|
||||
|
||||
init {
|
||||
dialogLayoutResource = R.layout.dialog_url
|
||||
}
|
||||
|
||||
fun getUrl() = url
|
||||
|
||||
fun setUrl(url: String) {
|
||||
this.url = url
|
||||
persistString(url)
|
||||
}
|
||||
|
||||
override fun onSetInitialValue(
|
||||
restorePersistedValue: Boolean,
|
||||
defaultValue: Any?
|
||||
) {
|
||||
setUrl(
|
||||
if (restorePersistedValue) {
|
||||
getPersistedString(url)
|
||||
} else {
|
||||
defaultValue as String
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package de.markusfisch.android.binaryeye.rs
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Canvas
|
||||
import android.support.v8.renderscript.Allocation
|
||||
import android.support.v8.renderscript.RenderScript
|
||||
|
||||
fun fixTransparency(rs: RenderScript, bitmap: Bitmap?): Bitmap? {
|
||||
bitmap ?: return null
|
||||
val result = analyze(rs, bitmap)
|
||||
return if (result.hasTransparentPixels) {
|
||||
val copy = Bitmap.createBitmap(
|
||||
bitmap.width,
|
||||
bitmap.height,
|
||||
bitmap.config
|
||||
)
|
||||
Canvas(copy).apply {
|
||||
drawColor(result.backgroundColor)
|
||||
drawBitmap(bitmap, 0f, 0f, null)
|
||||
}
|
||||
copy
|
||||
} else {
|
||||
bitmap
|
||||
}
|
||||
}
|
||||
|
||||
private fun analyze(rs: RenderScript, bitmap: Bitmap): Result {
|
||||
val analyzeScript = ScriptC_analyze(rs)
|
||||
val bitmapAllocation = Allocation.createFromBitmap(rs, bitmap)
|
||||
val result = analyzeScript.reduce_analyze(bitmapAllocation).get()
|
||||
bitmapAllocation.destroy()
|
||||
analyzeScript.destroy()
|
||||
return Result(
|
||||
result.x > 0,
|
||||
if (result.y > 0) 0xff000000.toInt() else 0xffffffff.toInt()
|
||||
)
|
||||
}
|
||||
|
||||
private data class Result(
|
||||
val hasTransparentPixels: Boolean,
|
||||
val backgroundColor: Int
|
||||
)
|
||||
@@ -1,15 +1,14 @@
|
||||
package de.markusfisch.android.binaryeye.rs
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Rect
|
||||
import android.support.v8.renderscript.*
|
||||
import de.markusfisch.android.binaryeye.renderscript.ScriptC_rotator
|
||||
import kotlin.math.max
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
private const val SCALE_FACTOR = .75f
|
||||
|
||||
class Preprocessor(
|
||||
context: Context,
|
||||
rs: RenderScript,
|
||||
width: Int,
|
||||
height: Int,
|
||||
private val roi: Rect?
|
||||
@@ -17,9 +16,8 @@ class Preprocessor(
|
||||
var outWidth = 0
|
||||
var outHeight = 0
|
||||
|
||||
private val rs = RenderScript.create(context)
|
||||
private val resizeScript = ScriptIntrinsicResize.create(rs)
|
||||
private val rotatorScript = ScriptC_rotator(rs)
|
||||
private val rotateScript = ScriptC_rotate(rs)
|
||||
|
||||
private var yuvType: Type? = null
|
||||
private var yuvAlloc: Allocation? = null
|
||||
@@ -62,9 +60,13 @@ class Preprocessor(
|
||||
outWidth = (roiWidth * SCALE_FACTOR).roundToInt()
|
||||
outHeight = (roiHeight * SCALE_FACTOR).roundToInt()
|
||||
|
||||
// make sure the dimensions are always a multiple of 4
|
||||
// Make sure the dimensions are always a multiple of 4.
|
||||
outWidth -= outWidth % 4
|
||||
outHeight -= outHeight % 4
|
||||
|
||||
// Make sure the dimensions are always greater than 4.
|
||||
outWidth = max(4, outWidth)
|
||||
outHeight = max(4, outHeight)
|
||||
} else {
|
||||
outWidth = (width * SCALE_FACTOR).roundToInt()
|
||||
outHeight = (height * SCALE_FACTOR).roundToInt()
|
||||
@@ -113,8 +115,7 @@ class Preprocessor(
|
||||
rotatedAlloc?.destroy()
|
||||
rotatedAlloc = null
|
||||
resizeScript.destroy()
|
||||
rotatorScript.destroy()
|
||||
rs.destroy()
|
||||
rotateScript.destroy()
|
||||
}
|
||||
|
||||
fun resizeOnly(frame: ByteArray) {
|
||||
@@ -125,11 +126,11 @@ class Preprocessor(
|
||||
fun resizeAndRotate(frame: ByteArray) {
|
||||
resize(frame)
|
||||
val t = resizedType ?: return
|
||||
rotatorScript._inImage = resizedAlloc
|
||||
rotatorScript._inWidth = t.x
|
||||
rotatorScript._inHeight = t.y
|
||||
rotatorScript.forEach_rotate90(
|
||||
rotatedAlloc, // ignored in kernel, just to satisfy forEach
|
||||
rotateScript._inImage = resizedAlloc
|
||||
rotateScript._inWidth = t.x
|
||||
rotateScript._inHeight = t.y
|
||||
rotateScript.forEach_rotate90(
|
||||
rotatedAlloc, // Ignored in kernel, just to satisfy forEach.
|
||||
rotatedAlloc
|
||||
)
|
||||
rotatedAlloc?.copyTo(frame)
|
||||
@@ -143,7 +144,7 @@ class Preprocessor(
|
||||
0, 0,
|
||||
roi.width(), roi.height(),
|
||||
yuvAlloc,
|
||||
roi.left, roi.top
|
||||
max(0, roi.left), max(0, roi.top)
|
||||
)
|
||||
roiAlloc
|
||||
} else {
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
package de.markusfisch.android.binaryeye.service
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.service.quicksettings.TileService
|
||||
import android.support.annotation.RequiresApi
|
||||
import de.markusfisch.android.binaryeye.activity.CameraActivity
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.N)
|
||||
class ScanTileService : TileService() {
|
||||
@RequiresApi(Build.VERSION_CODES.N)
|
||||
override fun onClick() {
|
||||
super.onClick()
|
||||
val intent = Intent(applicationContext, CameraActivity::class.java)
|
||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
startActivityAndCollapse(intent)
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package de.markusfisch.android.binaryeye.app
|
||||
package de.markusfisch.android.binaryeye.view
|
||||
|
||||
import android.content.Context
|
||||
import android.view.View
|
||||
+30
-16
@@ -1,4 +1,4 @@
|
||||
package de.markusfisch.android.binaryeye.app
|
||||
package de.markusfisch.android.binaryeye.view
|
||||
|
||||
import android.content.Context
|
||||
import android.content.ContextWrapper
|
||||
@@ -6,35 +6,44 @@ import android.graphics.drawable.ColorDrawable
|
||||
import android.os.Build
|
||||
import android.support.v4.content.ContextCompat
|
||||
import android.support.v7.app.AppCompatActivity
|
||||
import android.support.v7.widget.LinearLayoutManager
|
||||
import android.support.v7.widget.RecyclerView
|
||||
import android.view.View
|
||||
import android.view.WindowManager
|
||||
import android.widget.AbsListView
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
|
||||
val systemBarScrollListener = object : AbsListView.OnScrollListener {
|
||||
val systemBarListViewScrollListener = 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
|
||||
// Give Android some time to settle down before running this,
|
||||
// not putting it on the queue makes it only work sometimes.
|
||||
view.post {
|
||||
val scrolled = firstVisibleItem > 0 ||
|
||||
(totalItemCount > 0 && firstChildScrolled(view))
|
||||
val scrollable = if (scrolled) {
|
||||
true
|
||||
} else {
|
||||
totalItemCount > 0 && lastChildOutOfView(view)
|
||||
}
|
||||
val scrollable = scrolled || totalItemCount > 0 && lastChildOutOfView(view)
|
||||
colorSystemAndToolBars(view.context, scrolled, scrollable)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onScrollStateChanged(
|
||||
view: AbsListView,
|
||||
scrollState: Int
|
||||
) {
|
||||
override fun onScrollStateChanged(view: AbsListView, scrollState: Int) {
|
||||
}
|
||||
}
|
||||
|
||||
val systemBarRecyclerViewScrollListener = object : RecyclerView.OnScrollListener() {
|
||||
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
||||
val layoutManager = recyclerView.layoutManager as LinearLayoutManager
|
||||
val scrolled = layoutManager.findFirstCompletelyVisibleItemPosition() != 0
|
||||
val scrollable = scrolled || layoutManager.findLastVisibleItemPosition() <
|
||||
recyclerView.adapter.itemCount - 1
|
||||
colorSystemAndToolBars(recyclerView.context, scrolled, scrollable)
|
||||
}
|
||||
|
||||
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,7 +59,12 @@ private fun lastChildOutOfView(listView: AbsListView): Boolean {
|
||||
|
||||
fun initSystemBars(activity: AppCompatActivity) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
activity.window.decorView.systemUiVisibility =
|
||||
// Keeps the soft keyboard from repositioning the layout.
|
||||
val window = activity.window
|
||||
window.setSoftInputMode(
|
||||
WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE
|
||||
)
|
||||
window.decorView.systemUiVisibility =
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_STABLE or
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
|
||||
@@ -95,11 +109,11 @@ fun colorSystemAndToolBars(
|
||||
}
|
||||
activity.supportActionBar?.setBackgroundDrawable(
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
|
||||
// avoid allocation on Honeycomb and better
|
||||
// Avoid allocation on Honeycomb and better.
|
||||
actionBarBackground.color = topColor
|
||||
actionBarBackground
|
||||
} else {
|
||||
// ColorDrawable.setColor() doesn't exist pre Honeycomb
|
||||
// ColorDrawable.setColor() doesn't exist pre Honeycomb.
|
||||
ColorDrawable(topColor)
|
||||
}
|
||||
)
|
||||
@@ -29,10 +29,10 @@ fun View.doOnApplyWindowInsets(f: (View, Rect) -> Unit) {
|
||||
f(v, insetsWithToolbar(insets))
|
||||
insets
|
||||
}
|
||||
// it's important to explicitly request the insets (again) in
|
||||
// It's important to explicitly request the insets (again) in
|
||||
// case the view was created in Fragment.onCreateView() because
|
||||
// setOnApplyWindowInsetsListener() won't fire when the view
|
||||
// isn't attached
|
||||
// isn't attached.
|
||||
requestApplyInsetsWhenAttached()
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ import android.content.Context
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Rect
|
||||
import android.util.AttributeSet
|
||||
import de.markusfisch.android.binaryeye.app.colorSystemAndToolBars
|
||||
import de.markusfisch.android.binaryeye.view.colorSystemAndToolBars
|
||||
import de.markusfisch.android.scalingimageview.widget.ScalingImageView
|
||||
|
||||
open class ConfinedScalingImageView : ScalingImageView {
|
||||
|
||||
@@ -3,7 +3,7 @@ package de.markusfisch.android.binaryeye.widget
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.widget.ScrollView
|
||||
import de.markusfisch.android.binaryeye.app.colorSystemAndToolBars
|
||||
import de.markusfisch.android.binaryeye.view.colorSystemAndToolBars
|
||||
|
||||
class ConfinedScrollView : ScrollView {
|
||||
private var scrollable = false
|
||||
@@ -23,8 +23,8 @@ class ConfinedScrollView : ScrollView {
|
||||
) {
|
||||
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
|
||||
// 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
|
||||
|
||||
@@ -1,56 +1,18 @@
|
||||
package de.markusfisch.android.binaryeye.widget
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.*
|
||||
import android.support.v4.content.ContextCompat
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Rect
|
||||
import android.graphics.RectF
|
||||
import android.util.AttributeSet
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.graphics.Candidates
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
class CropImageView(context: Context, attr: AttributeSet) :
|
||||
ConfinedScalingImageView(context, attr) {
|
||||
val windowInsets = Rect()
|
||||
var onScan: (() -> Unit)? = null
|
||||
|
||||
var onScan: (() -> List<Point>?)? = null
|
||||
|
||||
private val candidates = Candidates(context)
|
||||
private val boundsPaint = 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()?.let {
|
||||
candidatePoints = it
|
||||
invalidate()
|
||||
}
|
||||
}
|
||||
|
||||
private var candidatePoints: List<Point>? = 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)
|
||||
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
|
||||
)
|
||||
}
|
||||
private val onScanRunnable = Runnable { onScan?.invoke() }
|
||||
|
||||
fun getBoundsRect() = Rect(
|
||||
bounds.left.roundToInt(),
|
||||
@@ -59,40 +21,12 @@ class CropImageView(context: Context, attr: AttributeSet) :
|
||||
bounds.bottom.roundToInt()
|
||||
)
|
||||
|
||||
private fun setBoundsWithPadding(
|
||||
left: Int,
|
||||
top: Int,
|
||||
right: Int,
|
||||
bottom: Int
|
||||
) {
|
||||
val width = right - left
|
||||
val height = bottom - top
|
||||
val size = if (width < height)
|
||||
width - padding * 2
|
||||
else
|
||||
height - padding * 2
|
||||
val hpad = (width - size) / 2
|
||||
val vpad = (height - size) / 2
|
||||
setBounds(
|
||||
left.toFloat() + hpad,
|
||||
top.toFloat() + vpad,
|
||||
right.toFloat() - hpad,
|
||||
bottom.toFloat() - vpad
|
||||
)
|
||||
center(bounds)
|
||||
}
|
||||
|
||||
override fun onDraw(canvas: Canvas) {
|
||||
super.onDraw(canvas)
|
||||
canvas.drawRect(bounds, boundsPaint)
|
||||
candidatePoints?.let {
|
||||
candidates.draw(canvas, it)
|
||||
}
|
||||
candidatePoints = null
|
||||
val mr = mappedRect ?: return
|
||||
if (mr != lastMappedRect) {
|
||||
removeCallbacks(onScanRunnable)
|
||||
postDelayed(onScanRunnable, 500)
|
||||
postDelayed(onScanRunnable, 300)
|
||||
lastMappedRect.set(mr)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,51 +4,72 @@ import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.graphics.*
|
||||
import android.os.Build
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
import android.support.v4.content.ContextCompat
|
||||
import android.util.AttributeSet
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.app.prefs
|
||||
import de.markusfisch.android.binaryeye.graphics.Candidates
|
||||
import de.markusfisch.android.binaryeye.graphics.getBitmapFromDrawable
|
||||
import de.markusfisch.android.binaryeye.graphics.getDashedBorderPaint
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
import kotlin.math.round
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
class DetectorView : View {
|
||||
val coordinates = FloatArray(32)
|
||||
val roi = Rect()
|
||||
|
||||
var updateRoi: (() -> Unit)? = null
|
||||
var onRoiChange: (() -> Unit)? = null
|
||||
var onRoiChanged: (() -> Unit)? = null
|
||||
|
||||
private val candidates = Candidates(context)
|
||||
private val currentOrientation = resources.configuration.orientation
|
||||
private val invalidateRunnable: Runnable = Runnable {
|
||||
marks = null
|
||||
coordinatesLast = 0
|
||||
invalidate()
|
||||
}
|
||||
private val roiPaint = Paint(Paint.ANTI_ALIAS_FLAG)
|
||||
private val roiPaint = context.getDashedBorderPaint()
|
||||
private val dotPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
color = ContextCompat.getColor(context, R.color.dot)
|
||||
style = Paint.Style.FILL
|
||||
}
|
||||
private val dotRadius: Float
|
||||
private val handleBitmap = resources.getBitmapFromDrawable(
|
||||
R.drawable.ic_crop_handle
|
||||
R.drawable.button_crop
|
||||
)
|
||||
private val handleXRadius = handleBitmap.width / 2
|
||||
private val handleYRadius = handleBitmap.height / 2
|
||||
private val distToFull: Float
|
||||
private val handlePos = Point(-1, -1)
|
||||
private val handleHome = Point()
|
||||
private val center = Point()
|
||||
private val touchDown = Point()
|
||||
private val distToFull: Int
|
||||
private val minMoveThresholdSq: Int
|
||||
private val cornerRadius: Int
|
||||
private val fabHeight: Int
|
||||
private val padding: Int
|
||||
|
||||
private var marks: List<Point>? = null
|
||||
private var center = PointF()
|
||||
private var handlePos = PointF()
|
||||
private var coordinatesLast = 0
|
||||
private var handleGrabbed = false
|
||||
private var handleActive = false
|
||||
private var minY = 0
|
||||
private var maxY = 0
|
||||
private var minDist = 0
|
||||
private var shadeColor = 0
|
||||
|
||||
init {
|
||||
val dp = context.resources.displayMetrics.density
|
||||
distToFull = 24f * dp
|
||||
roiPaint.apply {
|
||||
style = Paint.Style.STROKE
|
||||
color = 0xffffffff.toInt()
|
||||
strokeWidth = 2f * dp
|
||||
pathEffect = DashPathEffect(floatArrayOf(10f, 20f), 0f)
|
||||
}
|
||||
dotRadius = 8f * dp
|
||||
distToFull = (24f * dp).roundToInt()
|
||||
val minMoveThreshold = (8f * dp).roundToInt()
|
||||
minMoveThresholdSq = minMoveThreshold * minMoveThreshold
|
||||
cornerRadius = (8f * dp).roundToInt()
|
||||
fabHeight = (92f * dp).roundToInt()
|
||||
padding = (20f * dp).roundToInt()
|
||||
isSaveEnabled = true
|
||||
}
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet) :
|
||||
@@ -57,21 +78,84 @@ class DetectorView : View {
|
||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) :
|
||||
super(context, attrs, defStyleAttr)
|
||||
|
||||
fun mark(points: List<Point>) {
|
||||
marks = points
|
||||
fun saveCropHandlePos() {
|
||||
val pos = getCropHandlePos()
|
||||
prefs.cropHandleX = pos.x
|
||||
prefs.cropHandleY = pos.y
|
||||
prefs.cropHandleOrientation = currentOrientation
|
||||
}
|
||||
|
||||
private fun getCropHandlePos() = if (handleActive) {
|
||||
handlePos
|
||||
} else {
|
||||
Point(-1, -1)
|
||||
}
|
||||
|
||||
fun restoreCropHandlePos() {
|
||||
setCropHandlePos(
|
||||
prefs.cropHandleX,
|
||||
prefs.cropHandleY,
|
||||
prefs.cropHandleOrientation
|
||||
)
|
||||
}
|
||||
|
||||
private fun setCropHandlePos(x: Int, y: Int, orientation: Int) {
|
||||
if (orientation == currentOrientation) {
|
||||
handlePos.set(x, y)
|
||||
} else {
|
||||
handlePos.set(y, x)
|
||||
}
|
||||
if (x > -1) {
|
||||
handleActive = true
|
||||
}
|
||||
}
|
||||
|
||||
fun update(numberOfCoordinates: Int) {
|
||||
coordinatesLast = numberOfCoordinates
|
||||
invalidate()
|
||||
removeCallbacks(invalidateRunnable)
|
||||
postDelayed(invalidateRunnable, 500)
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(): Parcelable? {
|
||||
if (!handleActive) {
|
||||
return super.onSaveInstanceState()
|
||||
}
|
||||
return SavedState(super.onSaveInstanceState()).apply {
|
||||
savedHandlePos.set(getCropHandlePos())
|
||||
savedOrientation = currentOrientation
|
||||
}
|
||||
}
|
||||
|
||||
override fun onRestoreInstanceState(state: Parcelable) {
|
||||
super.onRestoreInstanceState(
|
||||
if (state is SavedState) {
|
||||
setCropHandlePos(
|
||||
state.savedHandlePos.x,
|
||||
state.savedHandlePos.y,
|
||||
state.savedOrientation
|
||||
)
|
||||
state.superState
|
||||
} else {
|
||||
state
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
override fun onTouchEvent(event: MotionEvent?): Boolean {
|
||||
event ?: return super.onTouchEvent(event)
|
||||
val x = event.x.roundToInt()
|
||||
val y = event.y.roundToInt()
|
||||
return when (event.actionMasked) {
|
||||
MotionEvent.ACTION_DOWN -> {
|
||||
if (prefs.showCropHandle) {
|
||||
handleGrabbed = abs(event.x - handlePos.x) < handleXRadius &&
|
||||
abs(event.y - handlePos.y) < handleYRadius
|
||||
touchDown.set(x, y)
|
||||
handleGrabbed = abs(x - handlePos.x) < handleXRadius &&
|
||||
abs(y - handlePos.y) < handleYRadius
|
||||
if (handleGrabbed) {
|
||||
onRoiChange?.invoke()
|
||||
}
|
||||
handleGrabbed
|
||||
} else {
|
||||
false
|
||||
@@ -79,8 +163,13 @@ class DetectorView : View {
|
||||
}
|
||||
MotionEvent.ACTION_MOVE -> {
|
||||
if (handleGrabbed) {
|
||||
handlePos.set(event.x, event.y)
|
||||
invalidate()
|
||||
handlePos.set(x, y)
|
||||
handleActive =
|
||||
handleActive or (distSq(handlePos, touchDown) > minMoveThresholdSq)
|
||||
if (handleActive) {
|
||||
updateClipRect()
|
||||
invalidate()
|
||||
}
|
||||
true
|
||||
} else {
|
||||
false
|
||||
@@ -88,15 +177,23 @@ class DetectorView : View {
|
||||
}
|
||||
MotionEvent.ACTION_CANCEL -> {
|
||||
if (handleGrabbed) {
|
||||
snap(event.x, event.y)
|
||||
snap(x, y)
|
||||
handleGrabbed = false
|
||||
}
|
||||
false
|
||||
}
|
||||
MotionEvent.ACTION_UP -> {
|
||||
if (handleGrabbed) {
|
||||
snap(event.x, event.y)
|
||||
updateRoi?.invoke()
|
||||
if (!handleActive) {
|
||||
setHandleToDefaultRoi()
|
||||
} else {
|
||||
snap(x, y)
|
||||
}
|
||||
if (handleActive) {
|
||||
updateClipRect()
|
||||
invalidate()
|
||||
}
|
||||
onRoiChanged?.invoke()
|
||||
handleGrabbed = false
|
||||
}
|
||||
false
|
||||
@@ -105,78 +202,214 @@ class DetectorView : View {
|
||||
}
|
||||
}
|
||||
|
||||
private fun snap(x: Float, y: Float) {
|
||||
if (abs(x - center.x) < distToFull) {
|
||||
handlePos.x = center.x
|
||||
invalidate()
|
||||
}
|
||||
if (abs(y - center.y) < distToFull) {
|
||||
handlePos.y = center.y
|
||||
private fun setHandleToDefaultRoi() {
|
||||
val mn = min(center.x, center.y) * .8f
|
||||
handlePos.set(
|
||||
(center.x + mn).roundToInt(),
|
||||
(center.y + mn).roundToInt()
|
||||
)
|
||||
handleActive = true
|
||||
}
|
||||
|
||||
private fun snap(x: Int, y: Int) {
|
||||
val cx = clampX(x)
|
||||
val cy = clampY(y)
|
||||
val dx = abs(cx - center.x)
|
||||
val dy = abs(cy - center.y)
|
||||
// check if handle is close to the vertical or horizontal center line
|
||||
if (dx < distToFull ||
|
||||
dy < distToFull ||
|
||||
// check if handle is close to a screen corner
|
||||
((abs(cy - minY) < distToFull || abs(maxY - cy) < distToFull) &&
|
||||
abs(dx - center.x) < distToFull)
|
||||
) {
|
||||
reset()
|
||||
invalidate()
|
||||
}
|
||||
}
|
||||
|
||||
private fun reset() {
|
||||
handlePos.set(handleHome)
|
||||
handleActive = false
|
||||
roi.set(0, 0, 0, 0)
|
||||
}
|
||||
|
||||
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
|
||||
super.onLayout(changed, left, top, right, bottom)
|
||||
val width = right - left
|
||||
val height = bottom - top
|
||||
center.set(
|
||||
(left + (width / 2)).toFloat(),
|
||||
(top + (height / 2)).toFloat()
|
||||
left + (width / 2),
|
||||
top + (height / 2)
|
||||
)
|
||||
if (width > height) {
|
||||
handlePos.set(round(right * .75f), center.y)
|
||||
minY = padding * 2
|
||||
maxY = height - minY
|
||||
handleHome.set(
|
||||
width - handleXRadius - paddingRight - padding,
|
||||
height - handleYRadius - paddingBottom - fabHeight
|
||||
)
|
||||
if (handlePos.x == -2) {
|
||||
setHandleToDefaultRoi()
|
||||
}
|
||||
if (handleActive) {
|
||||
updateClipRect()
|
||||
} else {
|
||||
handlePos.set(center.x, round(bottom * .75f))
|
||||
reset()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDraw(canvas: Canvas) {
|
||||
canvas.drawColor(0, PorterDuff.Mode.CLEAR)
|
||||
updateClipRect()
|
||||
if (roi.height() > 0 && roi.width() > 0) {
|
||||
// canvas.clipRect() doesn't work reliably below KITKAT
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||
canvas.save()
|
||||
canvas.clipOutRectCompat(roi)
|
||||
canvas.drawColor(shadeColor, PorterDuff.Mode.SRC)
|
||||
canvas.restore()
|
||||
} else {
|
||||
canvas.drawRect(roi, roiPaint)
|
||||
}
|
||||
}
|
||||
marks?.let {
|
||||
candidates.draw(canvas, it)
|
||||
if (handleActive) {
|
||||
drawClip(canvas)
|
||||
}
|
||||
drawDots(canvas)
|
||||
if (prefs.showCropHandle) {
|
||||
canvas.drawBitmap(
|
||||
handleBitmap,
|
||||
handlePos.x - handleXRadius,
|
||||
handlePos.y - handleYRadius,
|
||||
(handlePos.x - handleXRadius).toFloat(),
|
||||
(handlePos.y - handleYRadius).toFloat(),
|
||||
null
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun drawDots(canvas: Canvas) {
|
||||
var i = 0
|
||||
while (i < coordinatesLast) {
|
||||
canvas.drawCircle(
|
||||
coordinates[i++],
|
||||
coordinates[i++],
|
||||
dotRadius,
|
||||
dotPaint
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun drawClip(canvas: Canvas) {
|
||||
if (minDist < 1) {
|
||||
return
|
||||
}
|
||||
// canvas.clipRect() doesn't work reliably below KITKAT.
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||
val radius = min(minDist / 2, cornerRadius).toFloat()
|
||||
canvas.save()
|
||||
canvas.clipOutPathCompat(
|
||||
calculateRoundedRectPath(
|
||||
roi.left.toFloat(),
|
||||
roi.top.toFloat(),
|
||||
roi.right.toFloat(),
|
||||
roi.bottom.toFloat(),
|
||||
radius,
|
||||
radius
|
||||
)
|
||||
)
|
||||
canvas.drawColor(shadeColor)
|
||||
canvas.restore()
|
||||
} else {
|
||||
canvas.drawRect(roi, roiPaint)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateClipRect() {
|
||||
clampHandlePos()
|
||||
val dx = abs(handlePos.x - center.x)
|
||||
val dy = abs(handlePos.y - center.y)
|
||||
val d = min(dx, dy)
|
||||
shadeColor = (min(1f, d / distToFull) * 128f).toInt() shl 24
|
||||
minDist = min(dx, dy)
|
||||
shadeColor = (min(
|
||||
1f,
|
||||
minDist.toFloat() / distToFull.toFloat()
|
||||
) * 128f).toInt() shl 24
|
||||
roi.set(
|
||||
(center.x - dx).roundToInt(),
|
||||
(center.y - dy).roundToInt(),
|
||||
(center.x + dx).roundToInt(),
|
||||
(center.y + dy).roundToInt()
|
||||
center.x - dx,
|
||||
center.y - dy,
|
||||
center.x + dx,
|
||||
center.y + dy
|
||||
)
|
||||
}
|
||||
|
||||
private fun clampHandlePos() {
|
||||
handlePos.x = clampX(handlePos.x)
|
||||
handlePos.y = clampY(handlePos.y)
|
||||
}
|
||||
|
||||
private fun clampX(x: Int) = min(center.x * 2, max(0, x))
|
||||
|
||||
private fun clampY(y: Int) = min(maxY, max(minY, y))
|
||||
|
||||
internal class SavedState : BaseSavedState {
|
||||
val savedHandlePos = Point()
|
||||
|
||||
var savedOrientation = 0
|
||||
|
||||
constructor(superState: Parcelable?) : super(superState)
|
||||
|
||||
private constructor(parcel: Parcel) : super(parcel) {
|
||||
savedHandlePos.set(
|
||||
parcel.readInt(),
|
||||
parcel.readInt()
|
||||
)
|
||||
savedOrientation = parcel.readInt()
|
||||
}
|
||||
|
||||
override fun writeToParcel(out: Parcel, flags: Int) {
|
||||
super.writeToParcel(out, flags)
|
||||
out.writeInt(savedHandlePos.x)
|
||||
out.writeInt(savedHandlePos.y)
|
||||
out.writeInt(savedOrientation)
|
||||
}
|
||||
|
||||
companion object {
|
||||
@JvmField
|
||||
val CREATOR = object : Parcelable.Creator<SavedState> {
|
||||
override fun createFromParcel(source: Parcel) = SavedState(source)
|
||||
override fun newArray(size: Int): Array<SavedState?> = arrayOfNulls(size)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun Canvas.clipOutRectCompat(rect: Rect) {
|
||||
private fun distSq(a: Point, b: Point): Int {
|
||||
val dx = a.x - b.x
|
||||
val dy = a.y - b.y
|
||||
return dx * dx + dy * dy
|
||||
}
|
||||
|
||||
private fun Point.set(point: Point) {
|
||||
x = point.x
|
||||
y = point.y
|
||||
}
|
||||
|
||||
private fun Canvas.clipOutPathCompat(path: Path) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
clipOutRect(rect)
|
||||
clipOutPath(path)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
clipRect(rect, Region.Op.DIFFERENCE)
|
||||
clipPath(path, Region.Op.DIFFERENCE)
|
||||
}
|
||||
}
|
||||
|
||||
private fun calculateRoundedRectPath(
|
||||
left: Float,
|
||||
top: Float,
|
||||
right: Float,
|
||||
bottom: Float,
|
||||
rx: Float,
|
||||
ry: Float
|
||||
): Path {
|
||||
val width = right - left
|
||||
val height = bottom - top
|
||||
val widthMinusCorners = width - 2 * rx
|
||||
val heightMinusCorners = height - 2 * ry
|
||||
return Path().apply {
|
||||
moveTo(right, top + ry)
|
||||
rQuadTo(0f, -ry, -rx, -ry)
|
||||
rLineTo(-widthMinusCorners, 0f)
|
||||
rQuadTo(-rx, 0f, -rx, ry)
|
||||
rLineTo(0f, heightMinusCorners)
|
||||
rQuadTo(0f, ry, rx, ry)
|
||||
rLineTo(widthMinusCorners, 0f)
|
||||
rQuadTo(rx, 0f, rx, -ry)
|
||||
rLineTo(0f, -heightMinusCorners)
|
||||
close()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,13 +4,19 @@ import android.content.Context
|
||||
import android.widget.Toast
|
||||
|
||||
fun Context.toast(message: Int) = Toast.makeText(
|
||||
this,
|
||||
applicationContext,
|
||||
message,
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
|
||||
fun Context.toast(message: String) = Toast.makeText(
|
||||
this,
|
||||
message,
|
||||
applicationContext,
|
||||
message.ellipsize(128),
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
|
||||
private fun String.ellipsize(max: Int) = if (length < max) {
|
||||
this
|
||||
} else {
|
||||
"${take(max)}…"
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package de.markusfisch.android.binaryeye.zxing
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import com.google.zxing.*
|
||||
import com.google.zxing.common.BitMatrix
|
||||
import com.google.zxing.common.HybridBinarizer
|
||||
import java.util.*
|
||||
|
||||
@@ -10,44 +11,30 @@ class Zxing(possibleResultPoint: ResultPointCallback? = null) {
|
||||
private val multiFormatReader = MultiFormatReader()
|
||||
|
||||
init {
|
||||
val decodeFormats = EnumSet.noneOf<BarcodeFormat>(
|
||||
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
|
||||
)
|
||||
)
|
||||
)
|
||||
hints[DecodeHintType.POSSIBLE_FORMATS] = decodeFormats
|
||||
possibleResultPoint?.let {
|
||||
hints[DecodeHintType.NEED_RESULT_POINT_CALLBACK] = it
|
||||
}
|
||||
}
|
||||
|
||||
fun updateHints(tryHarder: Boolean) {
|
||||
fun updateHints(tryHarder: Boolean, formats: Set<String>) {
|
||||
if (tryHarder) {
|
||||
hints[DecodeHintType.TRY_HARDER] = java.lang.Boolean.TRUE
|
||||
} else {
|
||||
hints.remove(DecodeHintType.TRY_HARDER)
|
||||
}
|
||||
setDecodeFormats(formats)
|
||||
}
|
||||
|
||||
private fun setDecodeFormats(formats: Set<String>) {
|
||||
val decodeFormats = EnumSet.noneOf(
|
||||
BarcodeFormat::class.java
|
||||
)
|
||||
decodeFormats.addAll(
|
||||
EnumSet.copyOf(
|
||||
formats.map { BarcodeFormat.valueOf(it) }
|
||||
)
|
||||
)
|
||||
hints[DecodeHintType.POSSIBLE_FORMATS] = decodeFormats
|
||||
}
|
||||
|
||||
fun decode(
|
||||
@@ -112,49 +99,110 @@ class Zxing(possibleResultPoint: ResultPointCallback? = null) {
|
||||
val bitmap = BinaryBitmap(HybridBinarizer(source))
|
||||
return try {
|
||||
multiFormatReader.decode(bitmap, hints)
|
||||
} catch (e: ReaderException) {
|
||||
} catch (e: Exception) {
|
||||
// Usually it's bad practice to blindly catch *all* exceptions
|
||||
// because this can hide problems we want to know about. But
|
||||
// since ZXing has some errors (like all software) and I don't
|
||||
// want this app to break in the hands of my users, which won't
|
||||
// see the stack trace anyway, it's better to just catch all
|
||||
// exceptions here and live on.
|
||||
null
|
||||
} finally {
|
||||
multiFormatReader.reset()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val BLACK = 0xff000000.toInt()
|
||||
private const val WHITE = 0xffffffff.toInt()
|
||||
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
|
||||
fun encodeAsBitmap(
|
||||
text: String,
|
||||
format: BarcodeFormat,
|
||||
width: Int,
|
||||
height: Int,
|
||||
hints: EnumMap<EncodeHintType, Any>? = null
|
||||
): Bitmap? {
|
||||
val bitMatrix = encode(text, format, hints, width, height)
|
||||
val w = bitMatrix.width
|
||||
val h = bitMatrix.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 (bitMatrix.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
|
||||
}
|
||||
|
||||
fun encodeAsSvg(
|
||||
text: String,
|
||||
format: BarcodeFormat,
|
||||
hints: EnumMap<EncodeHintType, Any>? = null
|
||||
): String {
|
||||
val bitMatrix = encode(text, format, hints)
|
||||
val sb = StringBuilder()
|
||||
val w = bitMatrix.width
|
||||
var h = bitMatrix.height
|
||||
val moduleHeight = if (h == 1) w / 2 else 1
|
||||
for (y in 0 until h) {
|
||||
for (x in 0 until w) {
|
||||
if (bitMatrix.get(x, y)) {
|
||||
sb.append(" M${x},${y}h1v${moduleHeight}h-1z")
|
||||
}
|
||||
val bitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888)
|
||||
bitmap.setPixels(pixels, 0, w, 0, 0, w, h)
|
||||
return bitmap
|
||||
}
|
||||
}
|
||||
h *= moduleHeight
|
||||
return """<svg width="$w" height="$h"
|
||||
viewBox="0 0 $w $h"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="$sb"/>
|
||||
</svg>
|
||||
"""
|
||||
}
|
||||
|
||||
fun encodeAsText(
|
||||
text: String,
|
||||
format: BarcodeFormat,
|
||||
hints: EnumMap<EncodeHintType, Any>? = null
|
||||
): String {
|
||||
val bitMatrix = encode(text, format, hints)
|
||||
val w = bitMatrix.width
|
||||
val h = bitMatrix.height
|
||||
val sb = StringBuilder()
|
||||
for (y in 0 until h) {
|
||||
for (x in 0 until w) {
|
||||
sb.append(if (bitMatrix.get(x, y)) "█" else " ")
|
||||
}
|
||||
sb.append("\n")
|
||||
}
|
||||
return sb.toString()
|
||||
}
|
||||
|
||||
private fun encode(
|
||||
text: String,
|
||||
format: BarcodeFormat,
|
||||
encodeHints: EnumMap<EncodeHintType, Any>? = null,
|
||||
width: Int = 0,
|
||||
height: Int = 0
|
||||
): BitMatrix {
|
||||
val hints = encodeHints ?: EnumMap<EncodeHintType, Any>(EncodeHintType::class.java)
|
||||
if (!hints.contains(EncodeHintType.CHARACTER_SET)) {
|
||||
hints[EncodeHintType.CHARACTER_SET] = "utf-8"
|
||||
}
|
||||
return MultiFormatWriter().encode(
|
||||
text,
|
||||
format,
|
||||
width,
|
||||
height,
|
||||
hints
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/accent"/>
|
||||
<foreground android:drawable="@drawable/ic_shortcut_decode_foreground"/>
|
||||
</adaptive-icon>
|
||||
@@ -0,0 +1,4 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="108dp" android:height="108dp" android:viewportWidth="108" android:viewportHeight="108">
|
||||
<path android:pathData="M48.281 55.787a5.719 5.719 0 1 1 11.438 0 5.719 5.719 0 1 1-11.438 0" android:fillColor="#ffffff"/>
|
||||
<path android:pathData="M48.639 37.916l-3.271 3.575h-5.665c-1.965 0-3.574 1.608-3.574 3.574v21.444c0 1.966 1.609 3.575 3.574 3.575h28.594c1.965 0 3.574-1.609 3.574-3.575V45.065c0-1.966-1.609-3.574-3.574-3.574h-5.665l-3.271-3.575zM54 64.722c-4.932 0-8.935-4.003-8.935-8.935s4.003-8.935 8.935-8.935 8.935 4.003 8.935 8.935-4.003 8.935-8.935 8.935z" android:fillColor="#ffffff"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,4 @@
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/accent"/>
|
||||
<foreground android:drawable="@drawable/ic_shortcut_encode_foreground"/>
|
||||
</adaptive-icon>
|
||||
@@ -0,0 +1,3 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="108dp" android:height="108dp" android:viewportWidth="108" android:viewportHeight="108">
|
||||
<path android:pathData="M69.75 56.25h-13.5v13.5h-4.5v-13.5h-13.5v-4.5h13.5v-13.5h4.5v13.5h13.5z" android:fillColor="#FFFFFF"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,4 @@
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/accent"/>
|
||||
<foreground android:drawable="@drawable/ic_shortcut_preferences_foreground"/>
|
||||
</adaptive-icon>
|
||||
@@ -0,0 +1,3 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="108dp" android:height="108dp" android:viewportWidth="108" android:viewportHeight="108">
|
||||
<path android:pathData="M70.057 56.114c0.09-0.675 0.135-1.372 0.135-2.114 0-0.719-0.045-1.439-0.158-2.114l4.566-3.553c0.405-0.315 0.517-0.922 0.27-1.372l-4.318-7.466C70.282 39 69.72 38.843 69.225 39l-5.375 2.159c-1.124-0.854-2.316-1.574-3.643-2.114l-0.81-5.712c-0.09-0.54-0.539-0.922-1.079-0.922h-8.636c-0.54 0-0.967 0.382-1.057 0.922l-0.809 5.712c-1.327 0.54-2.542 1.282-3.644 2.114L38.798 39c-0.495-0.18-1.057 0-1.327 0.495l-4.296 7.466c-0.269 0.472-0.179 1.057 0.27 1.372l4.566 3.553c-0.113 0.675-0.203 1.417-0.203 2.114 0 0.697 0.045 1.439 0.158 2.114L33.4 59.667c-0.404 0.315-0.517 0.922-0.27 1.372l4.318 7.466C37.718 69 38.28 69.158 38.775 69l5.375-2.159c1.124 0.855 2.316 1.574 3.643 2.114l0.81 5.712c0.112 0.54 0.539 0.922 1.079 0.922h8.636c0.54 0 0.989-0.382 1.057-0.922l0.809-5.712c1.327-0.54 2.542-1.259 3.644-2.114L69.202 69c0.495 0.18 1.057 0 1.327-0.495l4.318-7.466c0.27-0.495 0.158-1.057-0.27-1.372zM54 62.096c-4.453 0-8.096-3.643-8.096-8.096s3.643-8.096 8.096-8.096 8.096 3.643 8.096 8.096-3.643 8.096-8.096 8.096z" android:fillColor="#ffffff"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,12 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" xmlns:aapt="http://schemas.android.com/aapt" android:width="56dp" android:height="56dp" android:viewportWidth="56" android:viewportHeight="56">
|
||||
<path android:pathData="M4 32a24 24 0 1 1 48 0 24 24 0 1 1-48 0">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient android:gradientRadius="24" android:centerX="28" android:centerY="32" android:type="radial">
|
||||
<item android:offset="0" android:color="#FF000000"/>
|
||||
<item android:offset="1" android:color="#00000000"/>
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
<path android:pathData="M4 28a24 24 0 1 1 48 0 24 24 0 1 1-48 0" android:fillColor="#B6D46F"/>
|
||||
<path android:pathData="M33 31h2v-8c0-1.1-0.9-2-2-2h-8v2h8v8zm-10 2V17h-2v4h-4v2h4v10c0 1.1 0.9 2 2 2h10v4h2v-4h4v-2H23z" android:fillColor="#FFFFFF"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,3 @@
|
||||
<vector android:height="24dp" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#FFFFFF" android:pathData="M14 10H2v2h12v-2zm0-4H2v2h12V6zm4 8v-4h-2v4h-4v2h4v4h2v-4h4v-2h-4zM2 16h8v-2H2v2z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,3 @@
|
||||
<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="#FFFFFF" android:pathData="M12.65 10C11.83 7.67 9.61 6 7 6c-3.31 0-6 2.69-6 6s2.69 6 6 6c2.61 0 4.83-1.67 5.65-4H17v4h4v-4h2v-4H12.65zM7 14c-1.1 0-2-0.9-2-2s0.9-2 2-2 2 0.9 2 2-0.9 2-2 2z"/>
|
||||
</vector>
|
||||
@@ -1,3 +1,3 @@
|
||||
<vector android:height="24dp" android:tint="#FFFFFF" android:viewportHeight="24.0" android:viewportWidth="24.0" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#FF000000" android:pathData="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.57L14 14.71v0.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.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 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="M3 11h8V3H3v8zm2-6h4v4H5V5zM3 21h8v-8H3v8zm2-6h4v4H5v-4zm8-12v8h8V3h-8zm6 6h-4V5h4v4zm0 10h2v2h-2zm-6-6h2v2h-2zm2 2h2v2h-2zm-2 2h2v2h-2zm2 2h2v2h-2zm2-2h2v2h-2zm0-4h2v2h-2zm2 2h2v2h-2z"/>
|
||||
</vector>
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:aapt="http://schemas.android.com/aapt"
|
||||
android:width="56dp"
|
||||
android:height="56dp"
|
||||
android:viewportWidth="56"
|
||||
android:viewportHeight="56">
|
||||
<path
|
||||
android:pathData="M28,32m-24,0a24,24 0,1 1,48 0a24,24 0,1 1,-48 0">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:gradientRadius="24"
|
||||
android:centerX="28"
|
||||
android:centerY="32"
|
||||
android:type="radial">
|
||||
<item android:offset="0" android:color="#FF000000"/>
|
||||
<item android:offset="1" android:color="#00000000"/>
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
<path
|
||||
android:pathData="M28,28m-24,0a24,24 0,1 1,48 0a24,24 0,1 1,-48 0"
|
||||
android:fillColor="#B6D46F"/>
|
||||
<path
|
||||
android:pathData="M33,31L35,31L35,23C35,21.9 34.1,21 33,21L25,21L25,23L33,23L33,31ZM23,33L23,17L21,17L21,21L17,21L17,23L21,23L21,33C21,34.1 21.9,35 23,35L33,35L33,39L35,39L35,35L39,35L39,33L23,33Z"
|
||||
android:fillColor="#FFFFFF"/>
|
||||
</vector>
|
||||
@@ -1,3 +0,0 @@
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<solid android:color="#b6d46f"/>
|
||||
</shape>
|
||||
@@ -4,6 +4,10 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:contentDescription="@string/view_barcode"/>
|
||||
<de.markusfisch.android.binaryeye.widget.DetectorView
|
||||
android:id="@+id/detector_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"/>
|
||||
<android.support.design.widget.CoordinatorLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
@@ -13,6 +17,6 @@
|
||||
style="@style/FAB"
|
||||
android:id="@+id/scan"
|
||||
android:contentDescription="@string/scan_code"
|
||||
android:src="@drawable/ic_action_scan"/>
|
||||
android:src="@drawable/ic_action_search"/>
|
||||
</android.support.design.widget.CoordinatorLayout>
|
||||
</merge>
|
||||
|
||||
@@ -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">
|
||||
<EditText
|
||||
tools:targetApi="o"
|
||||
android:id="@+id/url"
|
||||
android:inputType="textUri"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/url_hint"
|
||||
android:importantForAutofill="no"/>
|
||||
<Button
|
||||
android:id="@+id/test_url"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/test_url"/>
|
||||
</LinearLayout>
|
||||
@@ -38,7 +38,7 @@
|
||||
android:layout_marginBottom="16dp"
|
||||
android:gravity="end|top"/>
|
||||
<TableLayout
|
||||
android:id="@+id/meta"
|
||||
android:id="@+id/data"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@+id/format"
|
||||
@@ -46,6 +46,15 @@
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginLeft="16dp"
|
||||
android:layout_marginRight="16dp"/>
|
||||
<TableLayout
|
||||
android:id="@+id/meta"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@+id/data"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginLeft="16dp"
|
||||
android:layout_marginRight="16dp"/>
|
||||
<TextView
|
||||
android:id="@+id/hex"
|
||||
android:layout_width="match_parent"
|
||||
|
||||
@@ -23,6 +23,18 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="16dp"/>
|
||||
<TextView
|
||||
android:id="@+id/error_correction_label"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:text="@string/error_correction_level"/>
|
||||
<Spinner
|
||||
android:id="@+id/error_correction_level"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:entries="@array/error_correction_levels"/>
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
|
||||
@@ -1,90 +0,0 @@
|
||||
<de.markusfisch.android.binaryeye.widget.ConfinedScrollView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/scroll_view"
|
||||
android:clipToPadding="false"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:padding="16dp"
|
||||
android:orientation="vertical">
|
||||
<android.support.v7.widget.SwitchCompat
|
||||
android:id="@+id/show_crop_handle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="32dp"
|
||||
android:text="@string/show_crop_handle"/>
|
||||
<android.support.v7.widget.SwitchCompat
|
||||
android:id="@+id/zoom_by_swiping"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="32dp"
|
||||
android:text="@string/zoom_by_swiping"/>
|
||||
<android.support.v7.widget.SwitchCompat
|
||||
android:id="@+id/auto_rotate"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="32dp"
|
||||
android:text="@string/auto_rotate"/>
|
||||
<android.support.v7.widget.SwitchCompat
|
||||
android:id="@+id/try_harder"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="32dp"
|
||||
android:text="@string/try_harder"/>
|
||||
<android.support.v7.widget.SwitchCompat
|
||||
android:id="@+id/vibrate"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="32dp"
|
||||
android:text="@string/vibrate"/>
|
||||
<android.support.v7.widget.SwitchCompat
|
||||
android:id="@+id/use_history"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="32dp"
|
||||
android:text="@string/use_history"/>
|
||||
<android.support.v7.widget.SwitchCompat
|
||||
android:id="@+id/ignore_consecutive_duplicates"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="32dp"
|
||||
android:text="@string/ignore_consecutive_duplicates"/>
|
||||
<android.support.v7.widget.SwitchCompat
|
||||
android:id="@+id/open_immediately"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="32dp"
|
||||
android:text="@string/open_immediately"/>
|
||||
<android.support.v7.widget.SwitchCompat
|
||||
android:id="@+id/show_meta_data"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="32dp"
|
||||
android:text="@string/show_meta_data"/>
|
||||
<android.support.v7.widget.SwitchCompat
|
||||
android:id="@+id/show_hex_dump"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="32dp"
|
||||
android:text="@string/show_hex_dump"/>
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/open_with_url"/>
|
||||
<EditText
|
||||
tools:targetApi="o"
|
||||
android:id="@+id/open_with_url"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="32dp"
|
||||
android:gravity="start|top"
|
||||
android:typeface="monospace"
|
||||
android:imeOptions="flagNoExtractUi"
|
||||
android:inputType="textUri"
|
||||
android:hint="@string/open_with_url_hint"
|
||||
android:importantForAutofill="no"/>
|
||||
</LinearLayout>
|
||||
</de.markusfisch.android.binaryeye.widget.ConfinedScrollView>
|
||||
@@ -21,6 +21,12 @@
|
||||
android:title="@string/switch_camera"
|
||||
android:icon="@drawable/ic_action_switch_camera"
|
||||
material:showAsAction="ifRoom"/>
|
||||
<item
|
||||
android:id="@+id/bulk_mode"
|
||||
android:title="@string/bulk_mode"
|
||||
android:icon="@drawable/ic_action_bulk_mode"
|
||||
android:checkable="true"
|
||||
material:showAsAction="ifRoom"/>
|
||||
<item
|
||||
android:id="@+id/preferences"
|
||||
android:title="@string/preferences"
|
||||
|
||||
@@ -2,7 +2,12 @@
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:material="http://schemas.android.com/apk/res-auto">
|
||||
<item
|
||||
android:id="@+id/save"
|
||||
android:id="@+id/copy_to_clipboard"
|
||||
android:title="@string/copy_to_clipboard"
|
||||
android:icon="@drawable/ic_action_copy"
|
||||
material:showAsAction="ifRoom"/>
|
||||
<item
|
||||
android:id="@+id/export_to_file"
|
||||
android:title="@string/export_to_file"
|
||||
android:icon="@drawable/ic_action_save"
|
||||
material:showAsAction="ifRoom"/>
|
||||
|
||||
@@ -1,6 +1,18 @@
|
||||
<menu
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:material="http://schemas.android.com/apk/res-auto">
|
||||
<item
|
||||
android:id="@+id/remove"
|
||||
android:title="@string/remove_scan"
|
||||
android:icon="@drawable/ic_action_remove"
|
||||
android:visible="false"
|
||||
material:showAsAction="ifRoom"/>
|
||||
<item
|
||||
android:id="@+id/copy_password"
|
||||
android:title="@string/copy_password"
|
||||
android:icon="@drawable/ic_action_copy_password"
|
||||
android:visible="false"
|
||||
material:showAsAction="ifRoom"/>
|
||||
<item
|
||||
android:id="@+id/copy_to_clipboard"
|
||||
android:title="@string/copy_to_clipboard"
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
<menu
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:material="http://schemas.android.com/apk/res-auto">
|
||||
<item
|
||||
android:id="@+id/next"
|
||||
android:title="@string/encode"
|
||||
android:icon="@drawable/ic_action_next"
|
||||
material:showAsAction="ifRoom"/>
|
||||
</menu>
|
||||
@@ -1,4 +1,4 @@
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background"/>
|
||||
<background android:drawable="@color/accent"/>
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
<resources>
|
||||
<string name="no_camera_no_fun">এই অ্যাপ ক্যামেরা অনুমতি ছাড়া কোনো কাজের না। বিদায়।</string>
|
||||
<string name="camera_error">ক্যামেরা পাওয়া যাচ্ছে না, পরে চেষ্টা করো</string>
|
||||
<string name="scan_code">সংকেতের পাঠোদ্ধার</string>
|
||||
<string name="compose_barcode">বারকোড বানাও</string>
|
||||
<string name="decode_barcode">বারকোডের পাঠোদ্ধার</string>
|
||||
<string name="content">তথ্য</string>
|
||||
<string name="binary_data">(বাইনারি তথ্য)</string>
|
||||
<plurals name="barcode_info">
|
||||
<item quantity="one">%2$dটি অক্ষর, %1$s</item>
|
||||
<item quantity="other">%2$dটি অক্ষর, %1$s</item>
|
||||
</plurals>
|
||||
<string name="error_correction_level">ত্রুটি ঠিককরণ পরিমাণ</string>
|
||||
<string name="error_correction_level_l" formatted="false">কম (~৭% ঠিককরণ)</string>
|
||||
<string name="error_correction_level_m" formatted="false">মধ্যম (~১৫% ঠিককরণ)</string>
|
||||
<string name="error_correction_level_q" formatted="false">বেশি (~২৫% ঠিককরণ)</string>
|
||||
<string name="error_correction_level_h" formatted="false">অনেক বেশি (~৩০% ঠিককরণ)</string>
|
||||
<string name="issue_number">সমস্যা ক্রম</string>
|
||||
<string name="orientation">দিক</string>
|
||||
<string name="other_meta_data">মেটাতথ্য</string>
|
||||
<string name="pdf417_extra_metadata">পিডিএফ৪১৭ মেটাতথ্য</string>
|
||||
<string name="possible_country">তৈরির সম্ভাব্য দেশ</string>
|
||||
<string name="suggested_price">প্রস্তসবিত দাম</string>
|
||||
<string name="upc_ean_extension">ইউপিসি ইএএন বর্ধিত</string>
|
||||
<string name="toggle_flash">আলো</string>
|
||||
<string name="share">ভাগ করো</string>
|
||||
<string name="share_as">ভাগ করার মাধ্যম</string>
|
||||
<string name="copy_to_clipboard">ক্লিপবোর্ডে অনুলিপি</string>
|
||||
<string name="copied_to_clipboard">ক্লিপবোর্ডে অনুলিপিত</string>
|
||||
<string name="copy_password">ক্লিপবোর্ডে পাসওয়ার্ড অনুলিপি করো</string>
|
||||
<string name="copied_password_to_clipboard">ক্লিপবোর্ডে পাসওয়ার্ড অনুলিপিত</string>
|
||||
<string name="open_url">ইউআরএল খুলো</string>
|
||||
<string name="cannot_resolve_action">খোলার মতো অ্যাপ নেই</string>
|
||||
<string name="pick_search_engine">একটা সার্চ ইঞ্জিন নাও</string>
|
||||
<string name="format">ধরণ</string>
|
||||
<string name="size">পিক্সেলে আকার</string>
|
||||
<string name="width_by_height">%1$d×%2$d</string>
|
||||
<string name="input_content_here">এখানে তথ্য দাও</string>
|
||||
<string name="encode">সংকেতকরণ</string>
|
||||
<string name="error_no_content">তথ্য নেই</string>
|
||||
<string name="error_encoding_barcode">বারকোড বানানো যায়নি</string>
|
||||
<string name="view_barcode">বারকোড দেখো</string>
|
||||
<string name="info">তথ্য</string>
|
||||
<string name="switch_camera">ক্যামেরা পাল্টাও</string>
|
||||
<string name="bulk_mode">ধারাবাহিকভাবে খুঁজো</string>
|
||||
<string name="bulk_mode_summary">শুরুতে সবসময় ধারাবাহিকভাবে খুঁজো</string>
|
||||
<string name="bulk_mode_delay">Delay between continuous scans</string>
|
||||
<string name="quarter_second">A quarter second</string>
|
||||
<string name="half_second">Half a second</string>
|
||||
<string name="a_second">One second</string>
|
||||
<string name="two_seconds">Two seconds</string>
|
||||
<string name="five_seconds">Five seconds</string>
|
||||
<string name="history">ইতিহাস</string>
|
||||
<string name="preferences">পছন্দসমূহ</string>
|
||||
<string name="scan_category">খুঁজো</string>
|
||||
<string name="content_category">তথ্য</string>
|
||||
<string name="locale_category">ভাষা</string>
|
||||
<string name="custom_locale">আলাদা ভাষা</string>
|
||||
<string name="follow_system_settings">ব্যবস্থার পছন্দসমূহ অনুসরণ করো</string>
|
||||
<string name="show_crop_handle">ছাটার সীমা দেখাও</string>
|
||||
<string name="show_crop_handle_summary">পরিবর্তনযোগ্য জায়গায়ই শুধু খুঁজো।</string>
|
||||
<string name="barcode_formats">Barcode formats</string>
|
||||
<string name="zoom_by_swiping">ওপরে/নিচে টানো বড়ো করার জন্য</string>
|
||||
<string name="zoom_by_swiping_summary">ক্যামেরার ছোটো-বড় নিয়ন্ত্রণের জন্য উপরে-নিচে টানো।</string>
|
||||
<string name="auto_rotate">খাড়া ১ডি বারকোড সনাক্ত করো</string>
|
||||
<string name="auto_rotate_summary">১ডি বারকোড সনাক্ত করতে স্বয়ংক্রিয়ভাবে ঘুরাও। যন্ত্রনির্ভরে এটি কার্যসম্পাদনের হার কমাতে পারে।</string>
|
||||
<string name="try_harder">পাঠকের নির্ভুলতা বাড়াও</string>
|
||||
<string name="try_harder_summary">অনেক কঠিন সংকেতের জন্য এটা চালু করো। এতে কার্যসম্পাদনার হার কমতে পারে।</string>
|
||||
<string name="show_toast_in_bulk_mode">ধারাবাহিকভাবে তথ্য দেখাও</string>
|
||||
<string name="show_toast_in_bulk_mode_summary">ধারাবাহিকভাবে তথ্য দেখানোর সময় মুহূর্তের জন্য তথ্য দেখাও।</string>
|
||||
<string name="vibrate">চিনলে কাঁপো</string>
|
||||
<string name="vibrate_summary">এটা সক্ষম রাখলে সংকেত চিনার সাথে সাথে কাঁপবে।</string>
|
||||
<string name="use_history">ইতিহাস সংরক্ষণ করো।</string>
|
||||
<string name="use_history_summary">সনাক্তকৃত সংকেত যন্ত্রে সংরক্ষণ করো।</string>
|
||||
<string name="ignore_consecutive_duplicates">দ্বিত্ব উপেক্ষা করো</string>
|
||||
<string name="ignore_consecutive_duplicates_summary">দুইটা ইতিহাস সংরক্ষণ করবে না।</string>
|
||||
<string name="open_immediately">সাথে সাথে খুলো</string>
|
||||
<string name="open_immediately_summary">না দেখে সাথে সাথে খুলো। সক্রিয় থাকলে ক্ষতিকারক তথ্য খুলতে পারে।</string>
|
||||
<string name="copy_immediately">ক্লিপবোর্ডে রাখো</string>
|
||||
<string name="copy_immediately_summary">স্বয়ংক্রিয়ভাবে ক্লিপবোর্ডে অনুলিপি করো। বিশেষ দ্রষ্টব্য অন্য অ্যাপ পটভূমিতে এই তথ্য দেখে ফেলতে পারে।</string>
|
||||
<string name="show_meta_data">মেটাতথ্য দেখাও</string>
|
||||
<string name="show_meta_data_summary">সনাক্তকৃত বারকোড নিয়ে অতিরিক্ত তথ্য দেখাও।</string>
|
||||
<string name="show_hex_dump">ষড়(hex) সংকেত দেখাও</string>
|
||||
<string name="show_hex_dump_summary">পড়া তথ্যের ষড় সংকেত দেখাও</string>
|
||||
<string name="close_automatically">অনুলিপি/ভাগ করার পরে পিছনে যাও</string>
|
||||
<string name="close_automatically_summary">তথ্য অনুলিপি/ভাগ করার পরে স্বয়ংক্রিয়ভাবে পিছনে যাও।</string>
|
||||
<string name="default_search_engine">Open unknown data in</string>
|
||||
<string name="always_ask">Always ask</string>
|
||||
<string name="open_with_url">অজানা ইউআরএল খুলো</string>
|
||||
<string name="clear_network_suggestions">Clear network suggestions</string>
|
||||
<string name="clear_network_suggestions_summary">Remove all of the network suggestions that were previously provided by this app.</string>
|
||||
<string name="really_remove_all_networks">Really remove all networks?</string>
|
||||
<string name="clear_network_suggestions_success">Network suggestions cleared</string>
|
||||
<string name="clear_network_suggestions_nothing_to_remove">Nothing to remove</string>
|
||||
<string name="send_category">অন্যকে পাঠাও</string>
|
||||
<string name="send_scan_url">প্রতিটি পাঠোদ্ধার ইউআরএল-এ পাঠাও</string>
|
||||
<string name="send_scan_type">প্রতি পাঠোদ্ধারের জন্য অনুরোধ</string>
|
||||
<string name="send_type_get_add_content">গেট(GET) দিয়ে তথ্য যোগ করো</string>
|
||||
<string name="send_type_get_query_string">সম্পূর্ণ কুয়েরি স্ট্রিং দিয়ে গেট(GET) করো</string>
|
||||
<string name="send_type_post_form">POST application/x-www-form-urlencoded</string>
|
||||
<string name="send_type_post_json">POST application/json</string>
|
||||
<string name="test_url">পরীক্ষার জন্য ইউআরএল</string>
|
||||
<string name="really_remove_scan">আসলেই এটা মুছবে?</string>
|
||||
<string name="really_remove_all_scans">আসলেই সব মুছবে?</string>
|
||||
<string name="really_remove_selected_scans">আসলেই নির্বাচিত সব মুছবে?</string>
|
||||
<string name="clear_history">ইতিহাস মুছো</string>
|
||||
<string name="copy_scan">পাঠোদ্ধার অনুলিপি করো</string>
|
||||
<string name="edit_scan">চিহ্ন সম্পাদনা করো</string>
|
||||
<string name="remove_scan">পাঠোদ্ধার মুছো</string>
|
||||
<string name="enter_name">পাঠোদ্ধার ব্যাখ্যা করতে একটা নাম দাও</string>
|
||||
<string name="enter_name_hint">পাঠোদ্ধারের নাম</string>
|
||||
<string name="no_barcode_found">বারকোড পাওয়া যায়নি</string>
|
||||
<string name="pick_list_separator">তালিকার ভুক্তি কিভাবে আলাদা করবো?</string>
|
||||
<string name="separator_line_break">বাক্য বিরতি</string>
|
||||
<string name="separator_ruler">মাপকাঠি</string>
|
||||
<string name="save_as_file_name">ডাউনলোডে নথি হিসেবে সংরক্ষণ করবে?</string>
|
||||
<string name="file_name">নথির নাম</string>
|
||||
<string name="error_saving_file">নথি সংরক্ষণ করা যায়নি</string>
|
||||
<string name="error_file_exists">নথি আগে থেকে আছে</string>
|
||||
<string name="connect_to_wifi">ওয়াইফাইর সাথে যুক্ত করো</string>
|
||||
<string name="wifi_config_failed">ওয়াইফাইয়ে ঠিক করে যুক্ত হওয়া যায়নি</string>
|
||||
<string name="wifi_added">ওয়াইফাই যোগকৃত</string>
|
||||
<string name="sms_send">এসএমএস পাঠাও</string>
|
||||
<string name="sms_error">এসএমএস পাঠানো যায়নি</string>
|
||||
<string name="tel_dial">নম্বরে ডাকো</string>
|
||||
<string name="tel_error">নম্বর ডাকা যায়নি</string>
|
||||
<string name="mail_send">চিঠি পাঠাও</string>
|
||||
<string name="mail_error">চিঠি পাঠাতে ব্যর্থ</string>
|
||||
<string name="vcard_add">যোগাযোগে যুক্ত</string>
|
||||
<string name="vcard_failed">ষোগাযোগে যোগ করা যায়নি</string>
|
||||
<string name="vevent_add">বর্ষপঞ্জিকায় যোগ করো</string>
|
||||
<string name="vevent_failed">বর্ষপঞ্জিকায় যোগ করা যায়নি</string>
|
||||
<string name="otpauth_add">২এফএ যোগ করো</string>
|
||||
<string name="search_web">ওয়েবে খুঁজো</string>
|
||||
<string name="search_scan">আগেরগুলোতে খুঁজো</string>
|
||||
<string name="export_to_file">নথিতে রপ্তানি করো</string>
|
||||
<string name="export_as">রপ্তানির ধরণ?</string>
|
||||
<string name="export_csv_comma">কমাসহ সিএসভি(.csv) নথি</string>
|
||||
<string name="export_csv_semicolon">সেমিকোলনসহ সিএসভি(.csv) নথি</string>
|
||||
<string name="export_json">জেসন নথি</string>
|
||||
<string name="export_database">এসকিউলাইট তথ্যভিত্তি</string>
|
||||
<string name="saved_in_downloads">ডাউনলোডে সংরক্ষিত</string>
|
||||
<string name="rotate_image_cw">ছবি ঘড়ির দিকে ঘুরাও</string>
|
||||
<string name="pick_file">ছবি নাও</string>
|
||||
<string name="pick_code_to_scan">খুঁজার জন্য কোড নাও</string>
|
||||
<string name="shortcut_decode">বারকোড খুজো</string>
|
||||
<string name="shortcut_encode">বারকোড বানাও</string>
|
||||
<string name="shortcut_preferences">পছন্দসমূহ</string>
|
||||
<string name="background_request_failed">পটভূমির অনুরোধ ব্যর্থ</string>
|
||||
<string name="entry_type">Type</string>
|
||||
<string name="wifi_network">Wi-Fi network</string>
|
||||
<string name="wifi_ssid">Name</string>
|
||||
<string name="wifi_type">Authentication type</string>
|
||||
<string name="wifi_password">Password</string>
|
||||
<string name="wifi_hidden">Hidden network</string>
|
||||
<string name="wifi_eap">EAP method</string>
|
||||
<string name="wifi_anonymous_identity">Anonymous identity</string>
|
||||
<string name="wifi_identity">Identity</string>
|
||||
<string name="wifi_phase2">Phase 2 method</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,161 @@
|
||||
<resources>
|
||||
<string name="no_camera_no_fun">Bez oprávnění k fotoaparátu tato aplikace nemá smysl. Na viděnou.</string>
|
||||
<string name="camera_error">Nepodařilo se otevřít fotoaparát, prosím zkuste to znovu</string>
|
||||
<string name="scan_code">Skenovat kód</string>
|
||||
<string name="compose_barcode">Vytvořit čárový kód</string>
|
||||
<string name="decode_barcode">Dekódovat čárový kód</string>
|
||||
<string name="content">Obsah</string>
|
||||
<string name="binary_data">(binární data)</string>
|
||||
<plurals name="barcode_info">
|
||||
<item quantity="one">%2$d znak, %1$s</item>
|
||||
<item quantity="few">%2$d znaky, %1$s</item>
|
||||
<item quantity="other">%2$d znaků, %1$s</item>
|
||||
</plurals>
|
||||
<string name="error_correction_level">Stupeň korekce chyb</string>
|
||||
<string name="error_correction_level_l" formatted="false">Nízký (~7% korekce)</string>
|
||||
<string name="error_correction_level_m" formatted="false">Střední (~15% korekce)</string>
|
||||
<string name="error_correction_level_q" formatted="false">Čtvrtinový (~25% korekce)</string>
|
||||
<string name="error_correction_level_h" formatted="false">Vysoký (~30% korekce)</string>
|
||||
<string name="issue_number">Issue number</string>
|
||||
<string name="orientation">Orientace</string>
|
||||
<string name="other_meta_data">Metadata</string>
|
||||
<string name="pdf417_extra_metadata">PDF417 metadata</string>
|
||||
<string name="possible_country">Možná země původu</string>
|
||||
<string name="suggested_price">Doporučená cena</string>
|
||||
<string name="upc_ean_extension">rozšíření UPC EAN</string>
|
||||
<string name="toggle_flash">Zap/vyp blesk</string>
|
||||
<string name="share">Sdílet</string>
|
||||
<string name="share_as">Sdílet jako</string>
|
||||
<string name="copy_to_clipboard">Zkopírovat do schránky</string>
|
||||
<string name="copied_to_clipboard">Zkopírováno do schránky</string>
|
||||
<string name="copy_password">Zkopírovat heslo do schránky</string>
|
||||
<string name="copied_password_to_clipboard">Heslo zkopírováno do schránky</string>
|
||||
<string name="open_url">Otevřít URL</string>
|
||||
<string name="cannot_resolve_action">Nemáte aplikaci, která by to uměla otevřít</string>
|
||||
<string name="pick_search_engine">Zvolte vyhledávač</string>
|
||||
<string name="format">Formát</string>
|
||||
<string name="size">Velikost v pixelech</string>
|
||||
<string name="width_by_height">%1$d×%2$d</string>
|
||||
<string name="input_content_here">Sem vložte obsah</string>
|
||||
<string name="encode">ZAKÓDOVAT</string>
|
||||
<string name="error_no_content">Chybí obsah</string>
|
||||
<string name="error_encoding_barcode">Nepodařilo se vygenerovat čárový kód</string>
|
||||
<string name="view_barcode">Zobrazit čárový kód</string>
|
||||
<string name="info">Info</string>
|
||||
<string name="switch_camera">Přepnout kameru</string>
|
||||
<string name="bulk_mode">Skenovat nepřetržitě</string>
|
||||
<string name="bulk_mode_summary">Always start scanning continuously.</string>
|
||||
<string name="bulk_mode_delay">Delay between continuous scans</string>
|
||||
<string name="quarter_second">A quarter second</string>
|
||||
<string name="half_second">Half a second</string>
|
||||
<string name="a_second">One second</string>
|
||||
<string name="two_seconds">Two seconds</string>
|
||||
<string name="five_seconds">Five seconds</string>
|
||||
<string name="history">Historie</string>
|
||||
<string name="preferences">Nastavení</string>
|
||||
<string name="scan_category">Skenování</string>
|
||||
<string name="content_category">Obsah</string>
|
||||
<string name="locale_category">Jazyk</string>
|
||||
<string name="custom_locale">Zvolit jazyk</string>
|
||||
<string name="follow_system_settings">Řídit se nastavením systému</string>
|
||||
<string name="show_crop_handle">Zobrazovat hrany výřezu</string>
|
||||
<string name="show_crop_handle_summary">Omezí skenování na zvolenou oblast.</string>
|
||||
<string name="barcode_formats">Barcode formats</string>
|
||||
<string name="zoom_by_swiping">Potáhnutím nahoru/dolů přiblížit</string>
|
||||
<string name="zoom_by_swiping_summary">Přetáhnutím nahoru nebo dolů se zvětší / zmenší úroveň přiblížení.</string>
|
||||
<string name="auto_rotate">Rozpoznávat otočené 1D čárové kódy</string>
|
||||
<string name="auto_rotate_summary">Automaticky otočí obraz a tak detekuje 1D čárové kódy na výšku. Na některých zařízeních může snížit výkonnost.</string>
|
||||
<string name="try_harder">Optimalizovat pro přesnost</string>
|
||||
<string name="try_harder_summary">Použijte pro velmi nečitelné kódy. Sníží výkonnost.</string>
|
||||
<string name="show_toast_in_bulk_mode">Show data in continuous mode</string>
|
||||
<string name="show_toast_in_bulk_mode_summary">Briefly show the scanned data when scanning continuously.</string>
|
||||
<string name="vibrate">Zavibrovat při detekování</string>
|
||||
<string name="vibrate_summary">Zařízení zavibruje, když rozpozná čárový kód.</string>
|
||||
<string name="use_history">Ukládat historii skenování</string>
|
||||
<string name="use_history_summary">Rozpoznané kódy se uloží do zařízení.</string>
|
||||
<string name="ignore_consecutive_duplicates">Ignorovat duplikáty</string>
|
||||
<string name="ignore_consecutive_duplicates_summary">Další skeny stejného kódu se nebudou ukládat.</string>
|
||||
<string name="open_immediately">Otevírat okamžitě</string>
|
||||
<string name="open_immediately_summary">Přeskočí kontrolu a otevře obsah okamžitě. Může otevřít škodlivý obsah.</string>
|
||||
<string name="copy_immediately">Vložit do schránky</string>
|
||||
<string name="copy_immediately_summary">Automaticky zkopírovat naskenovaný obsah do schránky. Pozor, jiné aplikace jej mohou ze schránky přečíst, i když neběží.</string>
|
||||
<string name="show_meta_data">Zobrazit metadata</string>
|
||||
<string name="show_meta_data_summary">Zobrazí další data o naskenovaném čárovém kódu.</string>
|
||||
<string name="show_hex_dump">Zobrazit hex dump</string>
|
||||
<string name="show_hex_dump_summary">Zobrazí hex dump naskenovaného obsahu.</string>
|
||||
<string name="close_automatically">Vrátit se po zkopírování/sdílení</string>
|
||||
<string name="close_automatically_summary">Po zkopírovaní či sdílení se automaticky vrátí na skenovací obrazovku.</string>
|
||||
<string name="default_search_engine">Open unknown data in</string>
|
||||
<string name="always_ask">Always ask</string>
|
||||
<string name="open_with_url">Otevřít neznámá data pomocí URL</string>
|
||||
<string name="clear_network_suggestions">Clear network suggestions</string>
|
||||
<string name="clear_network_suggestions_summary">Remove all of the network suggestions that were previously provided by this app.</string>
|
||||
<string name="really_remove_all_networks">Really remove all networks?</string>
|
||||
<string name="clear_network_suggestions_success">Network suggestions cleared</string>
|
||||
<string name="clear_network_suggestions_nothing_to_remove">Nothing to remove</string>
|
||||
<string name="send_category">Přesměrování</string>
|
||||
<string name="send_scan_url">Posílat každý sken na URL</string>
|
||||
<string name="send_scan_type">Typ requestu pro každý sken</string>
|
||||
<string name="send_type_get_add_content">GET a simply add content</string>
|
||||
<string name="send_type_get_query_string">GET with compelete query string</string>
|
||||
<string name="send_type_post_form">POST application/x-www-form-urlencoded</string>
|
||||
<string name="send_type_post_json">POST application/json</string>
|
||||
<string name="test_url">Otestovat URL</string>
|
||||
<string name="really_remove_scan">Opravdu odstranit sken?</string>
|
||||
<string name="really_remove_all_scans">Opravdu odstranit všechny skeny?</string>
|
||||
<string name="really_remove_selected_scans">Opravdu odstranit zvolené skeny?</string>
|
||||
<string name="clear_history">Vymazat historii</string>
|
||||
<string name="copy_scan">Zkopírovat sken</string>
|
||||
<string name="edit_scan">Upravit popisek</string>
|
||||
<string name="remove_scan">Odstranit sken</string>
|
||||
<string name="enter_name">Napište název popisující sken</string>
|
||||
<string name="enter_name_hint">Název skenu</string>
|
||||
<string name="no_barcode_found">Čárový kód nenalezen</string>
|
||||
<string name="pick_list_separator">Jak mají být odděleny položky?</string>
|
||||
<string name="separator_line_break">Nový řádek</string>
|
||||
<string name="separator_ruler">Svislice</string>
|
||||
<string name="save_as_file_name">Uložit jako do složky Stahování</string>
|
||||
<string name="file_name">Název souboru</string>
|
||||
<string name="error_saving_file">Soubor se nepodařilo uložit</string>
|
||||
<string name="error_file_exists">Soubor již existuje</string>
|
||||
<string name="connect_to_wifi">Připojit k WiFi</string>
|
||||
<string name="wifi_config_failed">Nepodařilo se nastavit WiFi</string>
|
||||
<string name="wifi_added">WiFi přidána</string>
|
||||
<string name="sms_send">Poslat SMS</string>
|
||||
<string name="sms_error">Nepodařilo se poslat SMS</string>
|
||||
<string name="tel_dial">Vytočit číslo</string>
|
||||
<string name="tel_error">Nepodařilo se vytočit číslo</string>
|
||||
<string name="mail_send">Poslat email</string>
|
||||
<string name="mail_error">Nepodařilo se poslat email</string>
|
||||
<string name="vcard_add">Přidat do kontaktů</string>
|
||||
<string name="vcard_failed">Nepodařilo se přidat do kontaktů</string>
|
||||
<string name="vevent_add">Vložit do kalendáře</string>
|
||||
<string name="vevent_failed">Nepodařilo se vložit do kalendáře</string>
|
||||
<string name="otpauth_add">Přidat 2FA</string>
|
||||
<string name="search_web">Vyhledat na webu</string>
|
||||
<string name="search_scan">Vyhledat sken</string>
|
||||
<string name="export_to_file">Exportovat do souboru</string>
|
||||
<string name="export_as">Exportovat jako</string>
|
||||
<string name="export_csv_comma">CSV oddělené čárkami</string>
|
||||
<string name="export_csv_semicolon">CSV oddělené středníky</string>
|
||||
<string name="export_json">soubor JSON</string>
|
||||
<string name="export_database">SQLite databáze</string>
|
||||
<string name="saved_in_downloads">Uloženo do složky Stahování</string>
|
||||
<string name="rotate_image_cw">Otočit obrázek po směru hodinových ručiček</string>
|
||||
<string name="pick_file">Zvolte obrázek</string>
|
||||
<string name="pick_code_to_scan">Zvolte kód k naskenování</string>
|
||||
<string name="shortcut_decode">Skenovat čárový kód</string>
|
||||
<string name="shortcut_encode">Vytvořit čárový kód</string>
|
||||
<string name="shortcut_preferences">Nastavení</string>
|
||||
<string name="background_request_failed">Požadavek na pozadí selhal</string>
|
||||
<string name="entry_type">Type</string>
|
||||
<string name="wifi_network">Wi-Fi network</string>
|
||||
<string name="wifi_ssid">Name</string>
|
||||
<string name="wifi_type">Authentication type</string>
|
||||
<string name="wifi_password">Password</string>
|
||||
<string name="wifi_hidden">Hidden network</string>
|
||||
<string name="wifi_eap">EAP method</string>
|
||||
<string name="wifi_anonymous_identity">Anonymous identity</string>
|
||||
<string name="wifi_identity">Identity</string>
|
||||
<string name="wifi_phase2">Phase 2 method</string>
|
||||
</resources>
|
||||
@@ -11,6 +11,10 @@
|
||||
<item quantity="other">%2$d Zeichen, %1$s</item>
|
||||
</plurals>
|
||||
<string name="error_correction_level">Fehlerkorrektur-Level</string>
|
||||
<string name="error_correction_level_l" formatted="false">Low (~7% wiederherstellbar)</string>
|
||||
<string name="error_correction_level_m" formatted="false">Medium (~15% wiederherstellbar)</string>
|
||||
<string name="error_correction_level_q" formatted="false">Quartile (~25% wiederherstellbar)</string>
|
||||
<string name="error_correction_level_h" formatted="false">High (~30% wiederherstellbar)</string>
|
||||
<string name="issue_number">Ausgabe Nummer</string>
|
||||
<string name="orientation">Orientierung</string>
|
||||
<string name="other_meta_data">Metadaten</string>
|
||||
@@ -20,8 +24,10 @@
|
||||
<string name="upc_ean_extension">UPC EAN Erweiterung</string>
|
||||
<string name="toggle_flash">Licht an/ausschalten</string>
|
||||
<string name="share">Teilen</string>
|
||||
<string name="share_as">Teilen als?</string>
|
||||
<string name="copy_to_clipboard">In die Zwischenablage kopieren</string>
|
||||
<string name="copied_to_clipboard">Inhalt wurde in die Zwischenablage kopiert</string>
|
||||
<string name="copy_password">Passwort in die Zwischenablage kopieren</string>
|
||||
<string name="copied_password_to_clipboard">Passwort wurde in die Zwischenablage kopiert</string>
|
||||
<string name="open_url">URL öffnen</string>
|
||||
<string name="cannot_resolve_action">Keine App kann das öffnen</string>
|
||||
@@ -36,19 +42,64 @@
|
||||
<string name="view_barcode">Barcode ansehen</string>
|
||||
<string name="info">Info</string>
|
||||
<string name="switch_camera">Kamera wechseln</string>
|
||||
<string name="bulk_mode">Fortlaufend Scannen</string>
|
||||
<string name="bulk_mode_summary">Immer mit fortlaufendem Scannen starten.</string>
|
||||
<string name="bulk_mode_delay">Pause zwischen fortlaufenden Scans</string>
|
||||
<string name="quarter_second">Eine Viertelsekunde</string>
|
||||
<string name="half_second">Eine halbe Sekunde</string>
|
||||
<string name="a_second">Eine Sekunde</string>
|
||||
<string name="two_seconds">Zwei Sekunden</string>
|
||||
<string name="five_seconds">Fünf Sekunden</string>
|
||||
<string name="history">Gespeicherte Codes</string>
|
||||
<string name="preferences">Einstellungen</string>
|
||||
<string name="show_crop_handle">Ausschnittseinschränker anzeigen</string>
|
||||
<string name="zoom_by_swiping">Hoch/Runter streichen um zu zoomen</string>
|
||||
<string name="auto_rotate">Vertikale Erkennung von 1D Barcodes</string>
|
||||
<string name="try_harder">Suche länger und gründlicher nach Barcodes</string>
|
||||
<string name="scan_category">Scannen</string>
|
||||
<string name="content_category">Inhalt</string>
|
||||
<string name="locale_category">Sprache</string>
|
||||
<string name="custom_locale">Spracheinstellung</string>
|
||||
<string name="follow_system_settings">Systemeinstellung übernehmen</string>
|
||||
<string name="show_crop_handle">Ausschnitt verkleinern anzeigen</string>
|
||||
<string name="show_crop_handle_summary">Scannen auf einen anpassbaren Bereich einschränken.</string>
|
||||
<string name="barcode_formats">Barcode Formate</string>
|
||||
<string name="zoom_by_swiping">Geste für Kamerazoom</string>
|
||||
<string name="zoom_by_swiping_summary">Im Kamerabildschirm nach oben oder unten streichen, um den Kamerazoom zu steuern.</string>
|
||||
<string name="auto_rotate">Vertikale Erkennung</string>
|
||||
<string name="auto_rotate_summary">Das Kamerabild automatisch drehen um vertikale 1D Barcodes zu erkennen. Je nach Gerät kann dies die Leistung beeinträchtigen.</string>
|
||||
<string name="try_harder">Gründliche Suche</string>
|
||||
<string name="try_harder_summary">Aktivieren Sie diese Option für sehr schwer lesbare Codes. Beeinträchtigt die Leistung.</string>
|
||||
<string name="show_toast_in_bulk_mode">Daten einblenden</string>
|
||||
<string name="show_toast_in_bulk_mode_summary">Die gescannten Daten werden kurz eingeblenet, wenn fortlaufend gescannt wird.</string>
|
||||
<string name="vibrate">Bei Erkennung vibrieren</string>
|
||||
<string name="vibrate_summary">Das Gerät soll vibrieren wenn ein Code erkannt wird.</string>
|
||||
<string name="use_history">Codes speichern</string>
|
||||
<string name="ignore_consecutive_duplicates">Keine aufeinanderfolgenden Duplikate speichern</string>
|
||||
<string name="open_immediately">Inspektion überspringen und Inhalte sofort öffnen</string>
|
||||
<string name="use_history_summary">Eingelesene Barcodes auf dem Gerät speichern.</string>
|
||||
<string name="ignore_consecutive_duplicates">Duplikate ignorieren</string>
|
||||
<string name="ignore_consecutive_duplicates_summary">Aufeinanderfolgende Duplikate werden nicht in der Scan-Historie gespeichert.</string>
|
||||
<string name="open_immediately">Inhalte sofort öffnen</string>
|
||||
<string name="open_immediately_summary">Inspektion überspringen und Inhalt sofort öffnen. Kann schädliche Inhalte öffnen, wenn aktiviert.</string>
|
||||
<string name="copy_immediately">Automatisch in Zwischenablage</string>
|
||||
<string name="copy_immediately_summary">Eingelesene Inhalte werden automatisch in die Zwischenablage kopiert. Bitte beachten Sie, dass andere Anwendungen die Zwischenablage im Hintergrund auslesen können.</string>
|
||||
<string name="show_meta_data">Meta Daten anzeigen</string>
|
||||
<string name="show_meta_data_summary">Zusätzliche Daten über den eingelesenen Barcode anzeigen.</string>
|
||||
<string name="show_hex_dump">Hex Dump anzeigen</string>
|
||||
<string name="show_hex_dump_summary">Zeigt einen Hexdump des eingelesenen Inhalts an.</string>
|
||||
<string name="close_automatically">Automatisch zurückkehren</string>
|
||||
<string name="close_automatically_summary">Nach dem Kopieren/Teilen automatisch zum Scanbildschirm zurückkehren.</string>
|
||||
<string name="default_search_engine">Unbekannte Daten öffnen mit</string>
|
||||
<string name="always_ask">Immer fragen</string>
|
||||
<string name="open_with_url">URL zum Öffnen unbekannter Daten</string>
|
||||
<string name="clear_network_suggestions">WiFi Netzwerkvorschläge entfernen</string>
|
||||
<string name="clear_network_suggestions_summary">Betrifft alle WiFi Netzwerke, die zuvor mit dieser App bereitgestellt wurden.</string>
|
||||
<string name="really_remove_all_networks">Alle Netzwerkvorschläge entfernen?</string>
|
||||
<string name="clear_network_suggestions_success">Netzwerkvorschläge entfernt</string>
|
||||
<string name="clear_network_suggestions_nothing_to_remove">Nichts zu entfernen</string>
|
||||
<string name="send_category">Weiterleitung</string>
|
||||
<string name="send_scan_url">URL die für jeden Scan aufgerufen werden soll</string>
|
||||
<string name="send_scan_type">Typ des Requests für jeden Scan</string>
|
||||
<string name="send_type_get_add_content">GET mit an die URL angehängtem Inhalt</string>
|
||||
<string name="send_type_get_query_string">GET mit vollständigem Query-String</string>
|
||||
<string name="send_type_post_form">POST application/x-www-form-urlencoded</string>
|
||||
<string name="send_type_post_json">POST application/json</string>
|
||||
<string name="test_url">Test URL</string>
|
||||
<string name="really_remove_scan">Code wirklich entfernen?</string>
|
||||
<string name="really_remove_all_scans">Alle Codes entfernen?</string>
|
||||
<string name="really_remove_selected_scans">Alle ausgewählten Codes entfernen?</string>
|
||||
@@ -84,15 +135,26 @@
|
||||
<string name="search_scan">Code suchen</string>
|
||||
<string name="export_to_file">In Datei exportieren</string>
|
||||
<string name="export_as">Exportieren als?</string>
|
||||
<string name="export_csv_comma">CSV mit Kommas</string>
|
||||
<string name="export_csv_semicolon">CSV mit Semikolons</string>
|
||||
<string name="export_json">Als JSON exportieren</string>
|
||||
<string name="export_database">SQLite Datenbank exportieren</string>
|
||||
<string name="export_csv_comma">CSV Datei mit Kommas</string>
|
||||
<string name="export_csv_semicolon">CSV Datei mit Semikolons</string>
|
||||
<string name="export_json">JSON Datei</string>
|
||||
<string name="export_database">SQLite Datenbank</string>
|
||||
<string name="saved_in_downloads">In den Downloads gespeichert</string>
|
||||
<string name="rotate_image_cw">Bild im Uhrzeigersinn rotieren</string>
|
||||
<string name="pick_file">Bilddatei auswählen</string>
|
||||
<string name="pick_code_to_scan">Code im Bild auswählen</string>
|
||||
<string name="shortcut_decode">Barcode einlesen</string>
|
||||
<string name="shortcut_decode">Barcode lesen</string>
|
||||
<string name="shortcut_encode">Barcode erstellen</string>
|
||||
<string name="shortcut_preferences">Einstellungen</string>
|
||||
<string name="background_request_failed">Hintergrundanfrage fehlgeschlagen</string>
|
||||
<string name="entry_type">Typ</string>
|
||||
<string name="wifi_network">Wi-Fi Netzwerk</string>
|
||||
<string name="wifi_ssid">Name</string>
|
||||
<string name="wifi_type">Authentifizierungstyp</string>
|
||||
<string name="wifi_password">Passwort</string>
|
||||
<string name="wifi_hidden">Verstecktes Netzwerk</string>
|
||||
<string name="wifi_eap">EAP Methode</string>
|
||||
<string name="wifi_anonymous_identity">Anonyme Identität</string>
|
||||
<string name="wifi_identity">Identität</string>
|
||||
<string name="wifi_phase2">Phase 2 Methode</string>
|
||||
</resources>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user