Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3c7012e3c4 | ||
|
|
08e11c3c40 | ||
|
|
31854dab0c | ||
|
|
4f12c3e270 | ||
|
|
3ec43dabce | ||
|
|
0d4f536f71 | ||
|
|
b064a293fb | ||
|
|
d124c29919 | ||
|
|
2e89f1530a | ||
|
|
ef7e2a4b0d | ||
|
|
f680533819 | ||
|
|
a8c977c40e | ||
|
|
1e89b22953 | ||
|
|
11f028e0f3 | ||
|
|
1f7f4a23b5 | ||
|
|
41581d07ec | ||
|
|
dc819a6e1a | ||
|
|
5b51072f1a | ||
|
|
1b10e82cb2 | ||
|
|
eeae1eb0ad | ||
|
|
21c29ca373 | ||
|
|
8bcb72e91b | ||
|
|
8f345fbfbe | ||
|
|
2c88dbca02 | ||
|
|
bd1c629c85 | ||
|
|
f89e6612c4 | ||
|
|
7a0cd0ce19 | ||
|
|
8544affa1e | ||
|
|
48d3588341 | ||
|
|
783f6e4892 | ||
|
|
032cd64daf | ||
|
|
96626e6b00 | ||
|
|
11ae65ec3e | ||
|
|
91e3acb647 | ||
|
|
40c2f5947b | ||
|
|
58b9466cfc | ||
|
|
12210e1768 | ||
|
|
ad2ac457c9 | ||
|
|
634b6e8d2b | ||
|
|
6dd47f4dd0 | ||
|
|
8426f66cfb | ||
|
|
3dfa84a33e | ||
|
|
38f658e253 | ||
|
|
287d99e2b4 | ||
|
|
5cfcde9d4a | ||
|
|
b2730259fc | ||
|
|
61a2d3023c | ||
|
|
239ac7bd1a | ||
|
|
4048314cf7 | ||
|
|
3dbd772847 | ||
|
|
3a08465c35 | ||
|
|
92e3669ee5 | ||
|
|
8b297f4768 | ||
|
|
42b7f1179b | ||
|
|
9d6854c282 | ||
|
|
d943b7deff | ||
|
|
a138da9d9b | ||
|
|
194338a0d2 | ||
|
|
7eb8fdcce1 | ||
|
|
2bf1796340 | ||
|
|
cc42015da2 | ||
|
|
648c39c439 | ||
|
|
8c25b05cda | ||
|
|
780bebc9d1 | ||
|
|
bf7eb7efd5 | ||
|
|
d00d08bae3 | ||
|
|
379256a7fe | ||
|
|
180339269f | ||
|
|
49af17cba8 | ||
|
|
a03a187d2d | ||
|
|
84806e70d7 | ||
|
|
058de5a141 | ||
|
|
d330fbd195 | ||
|
|
f5bc6f664f | ||
|
|
464279807c | ||
|
|
f7e5ef473a | ||
|
|
ff733ef59e | ||
|
|
35b110a472 | ||
|
|
58eabf4754 | ||
|
|
1f273a48a5 | ||
|
|
785a64725d | ||
|
|
7a42e1de73 | ||
|
|
9856ab7b6b | ||
|
|
746ff143d8 | ||
|
|
b0d4d94973 | ||
|
|
c604ad5ef2 | ||
|
|
91c2b324ac | ||
|
|
df59fd7446 | ||
|
|
449fd1f26c | ||
|
|
72d839c6e2 | ||
|
|
ab8c993a81 | ||
|
|
5f5bd913d6 | ||
|
|
92fe4dd55f | ||
|
|
dab95176f0 | ||
|
|
650503ba70 | ||
|
|
61078f214e | ||
|
|
a4b639edfe | ||
|
|
f37878dbb8 | ||
|
|
10fde7c3d6 | ||
|
|
a098fcb48c | ||
|
|
f162977640 | ||
|
|
66cab58d7b | ||
|
|
e99e2d7dcc | ||
|
|
9045a27a5d | ||
|
|
9e1208c72b | ||
|
|
90410f232a | ||
|
|
6769fa7e37 | ||
|
|
72444f5081 | ||
|
|
9a3ca4ffa3 | ||
|
|
f572923720 | ||
|
|
4971f58f44 | ||
|
|
0bf24080a8 | ||
|
|
04a21f71ef | ||
|
|
e22a215863 | ||
|
|
881829e7b2 | ||
|
|
2ad261f3b6 | ||
|
|
e0fdda78b3 | ||
|
|
9c3710c56d | ||
|
|
ddcab46d47 | ||
|
|
076da26075 | ||
|
|
dbc37714f9 | ||
|
|
50e57eaf25 | ||
|
|
340f7f9d1d | ||
|
|
a42bd793bb | ||
|
|
6e531f5395 | ||
|
|
79b3fddcf8 | ||
|
|
09f3c8f193 | ||
|
|
5f82f216fd | ||
|
|
8424e3ea31 | ||
|
|
15ce34aa92 | ||
|
|
8dfc1e37c0 | ||
|
|
a0d3b7ba87 | ||
|
|
a234dc3a9a | ||
|
|
a00f70e25b | ||
|
|
ef555c0153 | ||
|
|
9fffb53f73 | ||
|
|
b82c6200c0 | ||
|
|
5bb347c392 | ||
|
|
f8033614fa | ||
|
|
46cc2fb1a1 | ||
|
|
f2ac1e77bb | ||
|
|
89c84880c6 | ||
|
|
352fe07850 | ||
|
|
00abe33314 | ||
|
|
142df25d7e | ||
|
|
021d775835 | ||
|
|
06df0c4d64 | ||
|
|
32c70af812 | ||
|
|
7cc1777e7a | ||
|
|
23e0564594 | ||
|
|
0447384bea | ||
|
|
b4d66e28e2 | ||
|
|
ee1c56d99f | ||
|
|
33ef78443e | ||
|
|
9f1e34b06d | ||
|
|
24766f55d4 | ||
|
|
0ae7d080e3 | ||
|
|
7d8c6016db | ||
|
|
3d923035e3 | ||
|
|
a489f3aa22 | ||
|
|
7dfd7a1303 | ||
|
|
9abc01fc4f | ||
|
|
d10a33c1b7 | ||
|
|
357b2b5d59 | ||
|
|
dfd64405da | ||
|
|
045daf9deb | ||
|
|
46b9ef516c | ||
|
|
0265909fd7 | ||
|
|
ceef92bb7c | ||
|
|
46785bab01 | ||
|
|
ccbb0f9e8a | ||
|
|
833d8e93d3 | ||
|
|
6dff9992d8 | ||
|
|
0b3dfe93cf | ||
|
|
e79ad49d15 | ||
|
|
38f7ba1ec6 | ||
|
|
4d268240fd | ||
|
|
1131f749a9 | ||
|
|
93a3cd2620 | ||
|
|
a866a2eab3 | ||
|
|
c5d6bfa4b2 | ||
|
|
6a63bc4e06 | ||
|
|
09ff334c4c | ||
|
|
45a2ac0647 | ||
|
|
425f3a7b80 | ||
|
|
f895b42cfe | ||
|
|
ecd4c487e3 | ||
|
|
750671c408 | ||
|
|
ead856221f | ||
|
|
34f14d0ff0 | ||
|
|
1b21297d83 | ||
|
|
53cb6fe007 | ||
|
|
f1daa19056 | ||
|
|
bd9da5889c | ||
|
|
d5949e557a | ||
|
|
51079e5208 | ||
|
|
09f3368bf0 | ||
|
|
84e03e3cc8 | ||
|
|
cff299771a | ||
|
|
7a327ab7c4 | ||
|
|
72b8924574 | ||
|
|
527b7a6a59 | ||
|
|
e9267e4e5a | ||
|
|
ac18733368 | ||
|
|
300c3bfd26 | ||
|
|
6449d9962d | ||
|
|
12554e63e2 | ||
|
|
af60ee6edb | ||
|
|
fe31fc6df2 | ||
|
|
6d4f3cafbf | ||
|
|
fa23b8078c | ||
|
|
e3c0fe8857 | ||
|
|
fa568686b6 | ||
|
|
304357b43c | ||
|
|
1e76b14038 | ||
|
|
d0777350a2 | ||
|
|
c7c1e74240 | ||
|
|
c793e1399b | ||
|
|
c550a6ff51 | ||
|
|
05b0f719e0 | ||
|
|
4326df5069 | ||
|
|
ba24287f04 | ||
|
|
bd8f1b17dd | ||
|
|
2c21aeb83a | ||
|
|
9ccff808bc | ||
|
|
32e936dd71 | ||
|
|
58eebacd4f | ||
|
|
75b945e842 | ||
|
|
a9ea77f74f | ||
|
|
3df658165b |
@@ -1,5 +1,142 @@
|
||||
# Change Log
|
||||
|
||||
## 1.63.4
|
||||
* Update Chinese translation
|
||||
* Improve support for old devices
|
||||
|
||||
## 1.63.3
|
||||
* Fix SCAN Intent
|
||||
|
||||
## 1.63.2
|
||||
* Add DX Film Edge barcode format
|
||||
* Update Turkish translation
|
||||
* Update Ukrainian translation
|
||||
|
||||
## 1.63.1
|
||||
* Include scan label in exports/shares
|
||||
* Update Russian translation
|
||||
|
||||
## 1.63.0
|
||||
* Edit scan label in decoding screen
|
||||
* Add a button to add generated codes to the history
|
||||
* Improve scanning from images
|
||||
* Improve asking for camera permission
|
||||
* Fix margin for recreated barcodes
|
||||
* Limit duration of error beep to 1s
|
||||
* Update Ukrainian translation
|
||||
* Update Spanish translation
|
||||
|
||||
## 1.62.3
|
||||
* Extend selection with single taps in history listing
|
||||
* Update Brazilian Portuguese translation
|
||||
|
||||
## 1.62.2
|
||||
* Include rMQR Code for updates
|
||||
|
||||
## 1.62.1
|
||||
* Make rMQR Code selectable
|
||||
|
||||
## 1.62.0
|
||||
* Add support for rMQR (rectangular Micro QR) Code
|
||||
* Share history as a file
|
||||
* Only share selected scans from history
|
||||
|
||||
## 1.61.2
|
||||
* Fix quick settings tile for Android 14
|
||||
* Fix Xiaomi MIUI lockscreen barcode shortcut
|
||||
* Update French translation
|
||||
* Update Czech translation
|
||||
* Update Russian translation
|
||||
|
||||
## 1.61.1
|
||||
* Share scanned contents as a file
|
||||
* Recreate binary barcodes too
|
||||
* Add Bulgarian translation and Brave search engine
|
||||
* Update Russian translation
|
||||
* Fix general format restriction from preferences
|
||||
|
||||
## 1.61.0
|
||||
* Support encoding binary data
|
||||
* Add margin parameter for encoding
|
||||
* Add copying binary data as hex dump
|
||||
* Fix parsing TYPE argument in VCARDs
|
||||
* Update Russian translation
|
||||
* Update Brazilian Portuguese translation
|
||||
* Update Ukrainian translation
|
||||
* Update Chinese translation
|
||||
|
||||
## 1.60.3
|
||||
* Fix saving custom language
|
||||
* Update Brazilian Portuguese translation
|
||||
* Update Chinese translation
|
||||
* Update Czech localisation
|
||||
* Update Russian translation
|
||||
* Update Italian language
|
||||
|
||||
## 1.60.2
|
||||
* Expand escape sequences in encoding input
|
||||
* Update Taiwan translation
|
||||
|
||||
## 1.60.1
|
||||
* Support sending scans via bluetooth
|
||||
* Fix resolving error correction level for recreation
|
||||
* Improve asset image compression
|
||||
* Update ZXing C++
|
||||
* Update Italian translation
|
||||
* Update Russian translation
|
||||
|
||||
## 1.59.0
|
||||
* Add setting error correction for AZTEC/PDF417
|
||||
* Fix setting error correction level for QR Codes
|
||||
* Make error beeps follow beep settings too
|
||||
* Don't close the app when cameras is unavailable
|
||||
|
||||
## 1.58.2
|
||||
* Fix parsing WiFi network suggestions
|
||||
|
||||
## 1.58.1
|
||||
* Update zh-rCn translation
|
||||
* Fix monochrome launcher icon
|
||||
|
||||
## 1.58.0
|
||||
* Improve detection of inverted barcodes
|
||||
* Optionally allow duplicates in bulk scan
|
||||
* Add Danish translation
|
||||
* Update Italian translation
|
||||
* Update Japanese translation
|
||||
* Update Russian translation
|
||||
* Fix charset decoding for PDF 417
|
||||
* Fix monochrome icon
|
||||
|
||||
## 1.57.0
|
||||
* Add optional audio feedback for scanning
|
||||
* Allow removing individual WiFi network suggestions
|
||||
* Recognize incomplete and malformed WiFi barcodes
|
||||
* Fix using shared text for encoding
|
||||
* Add Korean language
|
||||
* Update Chinese translation
|
||||
* Update Português/Brasil translation
|
||||
* Update Russian translation
|
||||
|
||||
## 1.56.3
|
||||
* Fix parsing URLs in barcodes
|
||||
* Update French, Portuguese, Turkish and Taiwan translation
|
||||
|
||||
## 1.56.2
|
||||
* Fix generating 1D barcode text output
|
||||
|
||||
## 1.56.1
|
||||
* Allow pinching a barcode to its generated size
|
||||
* Fix view/frame transformation matrix
|
||||
* Fix handling of tab characters
|
||||
* Update Indonesian translation
|
||||
* Update Russian translation
|
||||
|
||||
## 1.56.0
|
||||
* Add an action item to lock/unlock free rotation
|
||||
* Support setting fore and background colors when creating barcodes
|
||||
* Fix creating barcodes with UTF-8 content
|
||||
|
||||
## 1.55.0
|
||||
* Make recreated barcode interactive
|
||||
* Update to latest ZXing C++
|
||||
|
||||
@@ -17,4 +17,4 @@ Then write a [good commit message][commit_messages] to keep the history
|
||||
meaningful and useful. One feature, one commit.
|
||||
|
||||
[android_best_practices]: https://developer.android.com/distribute/best-practices/develop/
|
||||
[commit_messages]: https://juffalow.com/other/write-good-git-commit-message
|
||||
[commit_messages]: https://git-scm.com/book/en/v2/Distributed-Git-Contributing-to-a-Project#_commit_guidelines
|
||||
|
||||
@@ -33,6 +33,11 @@ testurl:
|
||||
-c android.intent.category.BROWSABLE \
|
||||
-d 'http://markusfisch.de/BinaryEye?ret=http%3A%2F%2Fmarkusfisch.de%2F%3Fresult%3D{RESULT}'
|
||||
|
||||
testxiaomi:
|
||||
adb shell am start -a android.intent.action.VIEW \
|
||||
-c android.intent.category.BROWSABLE \
|
||||
-d 'market://details?id=com.xiaomi.scanner'
|
||||
|
||||
install:
|
||||
adb $(TARGET) install -r app/build/outputs/apk/debug/app-debug.apk
|
||||
|
||||
|
||||
@@ -12,11 +12,18 @@ hesitate to contact me.
|
||||
Required to add a WiFi network from a barcode (usually a QR Code).
|
||||
|
||||
Before Android Q, this feature requires using
|
||||
[WifiManager.getConfiguredNetworks][configuredNetworks]
|
||||
[WifiManager.getConfiguredNetworks][getConfiguredNetworks]
|
||||
which requires `ACCESS_FINE_LOCATION`.
|
||||
|
||||
On Android Q an better, this permission is not requested nor required.
|
||||
|
||||
### [BLUETOOTH][BLUETOOTH], [BLUETOOTH_ADMIN][BLUETOOTH_ADMIN] and [BLUETOOTH_CONNECT][BLUETOOTH_CONNECT]
|
||||
|
||||
Required to optionally forward scans to a Bluetooth device.
|
||||
|
||||
[BLUETOOTH][BLUETOOTH] and [BLUETOOTH_ADMIN][BLUETOOTH_ADMIN] are required
|
||||
before Android S. [BLUETOOTH_CONNECT][BLUETOOTH_CONNECT] from Android S on.
|
||||
|
||||
### [CAMERA][CAMERA]
|
||||
|
||||
Required to read a barcode from the camera image.
|
||||
@@ -46,15 +53,23 @@ which requires `CHANGE_WIFI_STATE`.
|
||||
Required to add a WiFi network from a barcode (usually a QR Code).
|
||||
|
||||
Before Android Q, this feature requires using
|
||||
[WifiManager.getConfiguredNetworks][configuredNetworks]
|
||||
[WifiManager.getConfiguredNetworks][getConfiguredNetworks]
|
||||
which requires `ACCESS_WIFI_STATE`.
|
||||
|
||||
From Android R, `ACCESS_WIFI_STATE` is required for
|
||||
[WifiManager.getNetworkSuggestions][getNetworkSuggestions]
|
||||
which is used to remove individual network suggestions.
|
||||
|
||||
[ACCESS_FINE_LOCATION]: https://developer.android.com/reference/android/Manifest.permission#ACCESS_FINE_LOCATION
|
||||
[BLUETOOTH]: https://developer.android.com/reference/android/Manifest.permission#BLUETOOTH
|
||||
[BLUETOOTH_ADMIN]: https://developer.android.com/reference/android/Manifest.permission#BLUETOOTH_ADMIN
|
||||
[BLUETOOTH_CONNECT]: https://developer.android.com/reference/android/Manifest.permission#BLUETOOTH_CONNECT
|
||||
[CAMERA]: https://developer.android.com/reference/android/Manifest.permission#CAMERA
|
||||
[INTERNET]: https://developer.android.com/reference/android/Manifest.permission#INTERNET
|
||||
[VIBRATE]: https://developer.android.com/reference/android/Manifest.permission#VIBRATE
|
||||
[WRITE_EXTERNAL_STORAGE]: https://developer.android.com/reference/android/Manifest.permission#WRITE_EXTERNAL_STORAGE
|
||||
[CHANGE_WIFI_STATE]: https://developer.android.com/reference/android/Manifest.permission#CHANGE_WIFI_STATE
|
||||
[ACCESS_WIFI_STATE]: https://developer.android.com/reference/android/Manifest.permission#ACCESS_FINE_LOCATION
|
||||
[configuredNetworks]: https://developer.android.com/reference/android/net/wifi/WifiManager#getConfiguredNetworks()
|
||||
[getConfiguredNetworks]: 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)
|
||||
[getNetworkSuggestions]: https://developer.android.com/reference/android/net/wifi/WifiManager#getNetworkSuggestions()
|
||||
|
||||
@@ -10,6 +10,15 @@ comes in Material Design and can also generate barcodes.
|
||||
Binary Eye uses the [ZXing-C++][zxing_cpp] ("Zebra Crossing") barcode
|
||||
scanning library.
|
||||
|
||||
If you find this app useful and wish to support its continued development,
|
||||
you can [buy me a coffee](https://www.buymeacoffee.com/markusfisch) or
|
||||
send some Bitcoin decimals to `bc1q2guk2rpll587aymrfadkdtpq32448x5khk5j8z`.
|
||||
|
||||
<a href="https://www.buymeacoffee.com/markusfisch" target="_blank"><img
|
||||
src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png"
|
||||
alt="Buy Me A Coffee"
|
||||
style="height: 60px !important;width: 217px !important;"/></a>
|
||||
|
||||
## Screenshots
|
||||
|
||||
<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/screencap-scanning.png"
|
||||
@@ -36,6 +45,7 @@ ZXing can read the following barcode formats:
|
||||
* [CODE 93][code_93]
|
||||
* [CODE 128][code_128]
|
||||
* [DATA MATRIX][data_matrix]
|
||||
* [DX FILM EDGE][dx_film_edge]
|
||||
* [EAN 8][ean_8]
|
||||
* [EAN 13][ean_13]
|
||||
* [ITF][itf]
|
||||
@@ -43,6 +53,7 @@ ZXing can read the following barcode formats:
|
||||
* [PDF417][pdf417]
|
||||
* [QR CODE][qr_code]
|
||||
* [Micro QR Code][micro_qr_code]
|
||||
* [rMQR Code][rmqr_code]
|
||||
* [RSS 14][rss]
|
||||
* [RSS EXPANDED][rss]
|
||||
* [UPC A][upc_a]
|
||||
@@ -84,7 +95,6 @@ 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
|
||||
|
||||
@@ -121,10 +131,12 @@ If you're using AndroidX, this would be the new,
|
||||
[recommended way][intent_result]:
|
||||
|
||||
```kotlin
|
||||
class YourActivity : Activity() {
|
||||
private val resultLauncher = registerForActivityResult(StartActivityForResult()) { result ->
|
||||
if (result.resultCode == Activity.RESULT_OK) {
|
||||
val scan = result.data?.getStringExtra("SCAN_RESULT")
|
||||
class YourActivity : AppCompatActivity() {
|
||||
private val resultLauncher = registerForActivityResult(
|
||||
ActivityResultContracts.StartActivityForResult()
|
||||
) { result ->
|
||||
if (result.resultCode == RESULT_OK) {
|
||||
val content = result.data?.getStringExtra("SCAN_RESULT")
|
||||
…
|
||||
}
|
||||
}
|
||||
@@ -144,6 +156,7 @@ class YourActivity : Activity() {
|
||||
[code_93]: https://en.wikipedia.org/wiki/Code_93
|
||||
[code_128]: https://en.wikipedia.org/wiki/Code_128
|
||||
[data_matrix]: https://en.wikipedia.org/wiki/Data_Matrix
|
||||
[dx_film_edge]: https://en.wikipedia.org/wiki/DX_encoding
|
||||
[ean_8]: https://en.wikipedia.org/wiki/EAN-8
|
||||
[ean_13]: https://en.wikipedia.org/wiki/International_Article_Number
|
||||
[itf]: https://en.wikipedia.org/wiki/Interleaved_2_of_5
|
||||
@@ -151,6 +164,7 @@ class YourActivity : Activity() {
|
||||
[pdf417]: https://en.wikipedia.org/wiki/PDF417
|
||||
[qr_code]: https://en.wikipedia.org/wiki/QR_code
|
||||
[micro_qr_code]: https://en.wikipedia.org/wiki/QR_code#Micro_QR_code
|
||||
[rmqr_code]: https://www.qrcode.com/en/codes/rmqr.html
|
||||
[rss]: https://en.wikipedia.org/wiki/GS1_DataBar
|
||||
[upc_a]: https://en.wikipedia.org/wiki/Universal_Product_Code
|
||||
[upc_e]: https://en.wikipedia.org/wiki/Universal_Product_Code#UPC-E
|
||||
|
||||
@@ -2,15 +2,15 @@ apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
|
||||
android {
|
||||
namespace 'de.markusfisch.android.binaryeye'
|
||||
compileSdkVersion sdk_version
|
||||
namespace "de.markusfisch.android.binaryeye"
|
||||
compileSdk sdk_version
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion 9
|
||||
targetSdkVersion sdk_version
|
||||
minSdk 9
|
||||
targetSdk sdk_version
|
||||
|
||||
versionCode 105
|
||||
versionName '1.55.0'
|
||||
versionCode 132
|
||||
versionName '1.63.4'
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
@@ -53,18 +53,30 @@ android {
|
||||
enableSplit = false
|
||||
}
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
// Required for Gradle 8.
|
||||
sourceCompatibility = 17
|
||||
targetCompatibility = 17
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
testImplementation 'junit:junit:4.13.2'
|
||||
|
||||
// Kotlin
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.1"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4"
|
||||
|
||||
// Support
|
||||
implementation "com.android.support:appcompat-v7:$support_version"
|
||||
implementation "com.android.support:design:$support_version"
|
||||
implementation "com.android.support:preference-v7:$support_version"
|
||||
implementation "com.android.support:preference-v14:$support_version"
|
||||
implementation 'com.github.markusfisch:CameraView:1.9.1'
|
||||
implementation 'com.github.markusfisch:ScalingImageView:1.4.0'
|
||||
implementation 'com.github.markusfisch:zxing-cpp:v1.4.0.6'
|
||||
|
||||
// External
|
||||
implementation 'com.github.markusfisch:CameraView:1.10.0'
|
||||
implementation 'com.github.markusfisch:ScalingImageView:1.4.1'
|
||||
implementation 'com.github.markusfisch:zxing-cpp:v2.2.0.4'
|
||||
|
||||
// Testing
|
||||
testImplementation 'junit:junit:4.13.2'
|
||||
}
|
||||
|
||||
@@ -3,5 +3,3 @@
|
||||
-dontwarn android.support.v8.**
|
||||
|
||||
-keep class android.support.v7.widget.SearchView { *; }
|
||||
|
||||
-keep class de.markusfisch.android.zxingcpp.** { *; }
|
||||
|
||||
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 5.4 KiB After Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 8.2 KiB After Width: | Height: | Size: 6.0 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 8.2 KiB |
@@ -6,14 +6,18 @@
|
||||
android:xlargeScreens="true"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"
|
||||
android:maxSdkVersion="28"/>
|
||||
<uses-permission android:name="android.permission.BLUETOOTH"
|
||||
android:maxSdkVersion="30"/>
|
||||
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"
|
||||
android:maxSdkVersion="30"/>
|
||||
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT"/>
|
||||
<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"
|
||||
android:maxSdkVersion="28"/>
|
||||
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"
|
||||
android:maxSdkVersion="28"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
|
||||
<uses-feature android:name="android.hardware.camera"/>
|
||||
<uses-feature android:name="android.hardware.camera.autofocus"/>
|
||||
<uses-sdk tools:overrideLibrary="android.support.v14.preference"/>
|
||||
@@ -68,10 +72,8 @@
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<category android:name="android.intent.category.BROWSABLE"/>
|
||||
<data android:host="markusfisch.de"/>
|
||||
<data android:pathPrefix="/BinaryEye"/>
|
||||
<data android:scheme="http"/>
|
||||
<data android:scheme="https"/>
|
||||
<data android:host="details" android:scheme="market"
|
||||
android:pathSuffix="id=com.xiaomi.scanner"/>
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
|
||||
@@ -23,7 +23,7 @@ object OpenOrSearchAction : IAction {
|
||||
url: String,
|
||||
search: Boolean
|
||||
) {
|
||||
if (!context.openUrl(url) && search) {
|
||||
if (!context.openUrl(url, silent = true) && search) {
|
||||
openSearch(context, url)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package de.markusfisch.android.binaryeye.actions.vtype
|
||||
|
||||
import java.util.*
|
||||
import java.util.Locale
|
||||
|
||||
object VTypeParser {
|
||||
private const val BACKSLASH_R_LEGACY =
|
||||
@@ -39,6 +39,6 @@ data class VTypeProperty(
|
||||
} ?: this.info.firstOrNull()
|
||||
|
||||
companion object {
|
||||
private val typeRegex = """^(?:TYPE=)(.+?)[:;]?.*$""".toRegex(RegexOption.IGNORE_CASE)
|
||||
private val typeRegex = """^TYPE=([A-Z_]+)[:;]?.*$""".toRegex(RegexOption.IGNORE_CASE)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import android.provider.ContactsContract
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.actions.IntentAction
|
||||
import de.markusfisch.android.binaryeye.actions.vtype.VTypeParser
|
||||
import java.util.*
|
||||
import java.util.Locale
|
||||
|
||||
object VCardAction : IntentAction() {
|
||||
override val iconResId: Int
|
||||
@@ -45,10 +45,13 @@ object VCardAction : IntentAction() {
|
||||
val (extraPhoneType, extraPhone) = when (index) {
|
||||
0 -> ContactsContract.Intents.Insert.PHONE_TYPE to
|
||||
ContactsContract.Intents.Insert.PHONE
|
||||
|
||||
1 -> ContactsContract.Intents.Insert.SECONDARY_PHONE_TYPE to
|
||||
ContactsContract.Intents.Insert.SECONDARY_PHONE
|
||||
|
||||
2 -> ContactsContract.Intents.Insert.TERTIARY_PHONE_TYPE to
|
||||
ContactsContract.Intents.Insert.TERTIARY_PHONE_TYPE
|
||||
|
||||
else -> return@forEachIndexed
|
||||
}
|
||||
phoneProperty.firstTypeOrFirstInfo?.also {
|
||||
@@ -65,10 +68,13 @@ object VCardAction : IntentAction() {
|
||||
val (extraMailType, extraMail) = when (index) {
|
||||
0 -> ContactsContract.Intents.Insert.EMAIL_TYPE to
|
||||
ContactsContract.Intents.Insert.EMAIL
|
||||
|
||||
1 -> ContactsContract.Intents.Insert.SECONDARY_EMAIL_TYPE to
|
||||
ContactsContract.Intents.Insert.SECONDARY_EMAIL
|
||||
|
||||
2 -> ContactsContract.Intents.Insert.TERTIARY_EMAIL_TYPE to
|
||||
ContactsContract.Intents.Insert.TERTIARY_EMAIL
|
||||
|
||||
else -> return@forEachIndexed
|
||||
}
|
||||
mailProperty.firstTypeOrFirstInfo?.also {
|
||||
@@ -184,7 +190,7 @@ object VCardAction : IntentAction() {
|
||||
}
|
||||
|
||||
private val String.addressType: Int?
|
||||
get() = when (this.toUpperCase(Locale.US)) {
|
||||
get() = when (this.uppercase()) {
|
||||
"HOME" -> ContactsContract.CommonDataKinds.StructuredPostal.TYPE_HOME
|
||||
"WORK" -> ContactsContract.CommonDataKinds.StructuredPostal.TYPE_WORK
|
||||
"OTHER" -> ContactsContract.CommonDataKinds.StructuredPostal.TYPE_OTHER
|
||||
|
||||
@@ -7,9 +7,7 @@ import de.markusfisch.android.binaryeye.content.openUrl
|
||||
|
||||
object WebAction : IAction {
|
||||
private val colloquialRegex =
|
||||
"""^(http[s]*://)*([a-z0-9]+[a-z0-9-]*[a-z0-9]+[.])+[a-z]{2,}([?#/]+[\w/=&;._-]*)*$""".toRegex(
|
||||
RegexOption.IGNORE_CASE
|
||||
)
|
||||
"^(http[s]*://)*[A-Za-z0-9]{3,}\\.[A-Za-z]{2,}[^ \t\r\n]*$".toRegex()
|
||||
|
||||
override val iconResId: Int = R.drawable.ic_action_open
|
||||
override val titleResId: Int = R.string.open_url
|
||||
|
||||
@@ -22,10 +22,9 @@ object WifiAction : IAction {
|
||||
context: Context,
|
||||
data: ByteArray
|
||||
) = withContext(Dispatchers.IO) {
|
||||
val wifiConfig = WifiConnector.parse(
|
||||
String(data)
|
||||
) ?: return@withContext
|
||||
val message = WifiConnector.addNetwork(context, wifiConfig)
|
||||
val message = WifiConnector.parse(String(data))?.let {
|
||||
WifiConnector.addNetwork(context, it)
|
||||
} ?: R.string.wifi_config_failed
|
||||
withContext(Dispatchers.Main) {
|
||||
context.toast(message)
|
||||
}
|
||||
|
||||
@@ -10,12 +10,6 @@ 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|>;;
|
||||
@@ -50,12 +44,21 @@ object WifiConnector {
|
||||
passwordBlock?.apply {
|
||||
invoke(parsedData.password)
|
||||
}
|
||||
return if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
||||
// WifiConfiguration is deprecated in Android Q.
|
||||
@Suppress("DEPRECATION")
|
||||
WifiConfiguration().apply(parsedData)
|
||||
} else {
|
||||
WifiNetworkSuggestion.Builder().apply(parsedData)
|
||||
return try {
|
||||
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q) {
|
||||
// WifiConfiguration is deprecated in Android Q but
|
||||
// because it's only possible in R to query network
|
||||
// suggestions, we still use the deprecated API on Q.
|
||||
@Suppress("DEPRECATION")
|
||||
WifiConfiguration().apply(parsedData)
|
||||
} else {
|
||||
WifiNetworkSuggestion.Builder().apply(parsedData)
|
||||
}
|
||||
} catch (e: IllegalArgumentException) {
|
||||
// Some arguments may be out of range, e.g. SSIDs cannot
|
||||
// be longer than 32 characters in some versions of Android.
|
||||
// See: https://github.com/markusfisch/BinaryEye/issues/402
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,33 +66,23 @@ object WifiConnector {
|
||||
val wifiManager = context.applicationContext.getSystemService(
|
||||
Context.WIFI_SERVICE
|
||||
) as WifiManager
|
||||
return if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
||||
// WifiConfiguration is deprecated in Android Q.
|
||||
@Suppress("DEPRECATION")
|
||||
val wifiConfig = config as WifiConfiguration
|
||||
if (wifiManager.enableWifi() &&
|
||||
wifiManager.removeOldNetwork(wifiConfig) &&
|
||||
wifiManager.enableNewNetwork(wifiConfig)
|
||||
) {
|
||||
R.string.wifi_added
|
||||
} else {
|
||||
R.string.wifi_config_failed
|
||||
}
|
||||
// WifiConfiguration is deprecated in Android Q.
|
||||
@Suppress("DEPRECATION")
|
||||
return if ((config is WifiConfiguration &&
|
||||
wifiManager.enableWifi() &&
|
||||
wifiManager.removeOldNetwork(config) &&
|
||||
wifiManager.enableNewNetwork(config)) ||
|
||||
(Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q &&
|
||||
config is WifiNetworkSuggestion.Builder &&
|
||||
wifiManager.addNetworkFromBuilder(config))
|
||||
) {
|
||||
R.string.wifi_added
|
||||
} else {
|
||||
val suggestion = (config as WifiNetworkSuggestion.Builder).build()
|
||||
val suggestions = listOf(suggestion)
|
||||
// Remove previous conflicting network suggestion.
|
||||
wifiManager.removeNetworkSuggestions(suggestions)
|
||||
val result = wifiManager.addNetworkSuggestions(suggestions)
|
||||
if (result == WifiManager.STATUS_NETWORK_SUGGESTIONS_SUCCESS) {
|
||||
R.string.wifi_added
|
||||
} else {
|
||||
R.string.wifi_config_failed
|
||||
}
|
||||
R.string.wifi_config_failed
|
||||
}
|
||||
}
|
||||
|
||||
internal fun parseMap(string: String): Map<String, String>? {
|
||||
internal fun parseMap(input: String): Map<String, String>? {
|
||||
// Normally those codes should have the last semicolon, but many
|
||||
// generators don't add it.
|
||||
val wifiRegex = """^WIFI:((?:.+?:(?:[^\\;]|\\.)*;)+);?$""".toRegex()
|
||||
@@ -97,7 +90,7 @@ object WifiConnector {
|
||||
// and : because many QR Code creators don't escape properly.
|
||||
val pairRegex = """(.+?):((?:[^\\;]|\\.)*);""".toRegex()
|
||||
return wifiRegex.matchEntire(
|
||||
string
|
||||
input.trimAndTerminate()
|
||||
)?.groupValues?.get(1)?.let { pairs ->
|
||||
pairRegex.findAll(pairs).map { pair ->
|
||||
pair.groupValues[1].uppercase(Locale.US) to pair.groupValues[2]
|
||||
@@ -126,7 +119,7 @@ object WifiConnector {
|
||||
requireSdk(Build.VERSION_CODES.JELLY_BEAN_MR2) {
|
||||
WifiEnterpriseConfig.Eap.NONE
|
||||
}
|
||||
} else when (inputMap["E"]) {
|
||||
} else when (inputMap["E"]?.uppercase()) {
|
||||
"AKA" -> requireSdk(Build.VERSION_CODES.LOLLIPOP) { WifiEnterpriseConfig.Eap.AKA }
|
||||
"AKA_PRIME" -> requireSdk(Build.VERSION_CODES.M) { WifiEnterpriseConfig.Eap.AKA_PRIME }
|
||||
"NONE" -> requireSdk(Build.VERSION_CODES.JELLY_BEAN_MR2) { WifiEnterpriseConfig.Eap.NONE }
|
||||
@@ -143,7 +136,7 @@ object WifiConnector {
|
||||
requireSdk(Build.VERSION_CODES.JELLY_BEAN_MR2) {
|
||||
WifiEnterpriseConfig.Phase2.NONE
|
||||
}
|
||||
} else when (inputMap["PH2"]) {
|
||||
} else when (inputMap["PH2"]?.uppercase()) {
|
||||
"AKA" -> requireSdk(Build.VERSION_CODES.O) { WifiEnterpriseConfig.Phase2.AKA }
|
||||
"AKA_PRIME" -> requireSdk(Build.VERSION_CODES.O) { WifiEnterpriseConfig.Phase2.AKA_PRIME }
|
||||
"GTC" -> requireSdk(Build.VERSION_CODES.JELLY_BEAN_MR2) { WifiEnterpriseConfig.Phase2.GTC }
|
||||
@@ -179,10 +172,11 @@ object WifiConnector {
|
||||
data: SimpleDataAccessor
|
||||
): WifiNetworkSuggestion.Builder? {
|
||||
try {
|
||||
when (data.securityType) {
|
||||
when (data.securityType.uppercase()) {
|
||||
"WPA", "WPA2" -> {
|
||||
data.password?.let { setWpa2Passphrase(it) }
|
||||
}
|
||||
|
||||
"WPA2-EAP" -> {
|
||||
setWpa2EnterpriseConfig(WifiEnterpriseConfig().apply {
|
||||
identity = data.identity
|
||||
@@ -192,9 +186,11 @@ object WifiConnector {
|
||||
data.phase2Method?.let { phase2Method = it }
|
||||
})
|
||||
}
|
||||
|
||||
"WPA3" -> {
|
||||
data.password?.let { setWpa3Passphrase(it) }
|
||||
}
|
||||
|
||||
"WPA3-EAP" -> {
|
||||
setWpa3EnterpriseConfig(WifiEnterpriseConfig().apply {
|
||||
identity = data.identity
|
||||
@@ -240,8 +236,8 @@ object WifiConnector {
|
||||
fun WifiConfiguration.applySecurity(
|
||||
data: SimpleDataAccessor
|
||||
): WifiConfiguration? {
|
||||
when (data.securityType) {
|
||||
"", "nopass" -> allowedKeyManagement.set(WifiConfiguration.KeyMgmt.NONE)
|
||||
when (data.securityType.uppercase()) {
|
||||
"", "NOPASS" -> allowedKeyManagement.set(WifiConfiguration.KeyMgmt.NONE)
|
||||
"WEP" -> @Suppress("DEPRECATION") /* WEP as insecure */ {
|
||||
passwordWithQuotes?.let {
|
||||
wepKeys[0] = it
|
||||
@@ -254,6 +250,7 @@ object WifiConnector {
|
||||
allowedGroupCiphers.set(WifiConfiguration.GroupCipher.WEP40)
|
||||
allowedGroupCiphers.set(WifiConfiguration.GroupCipher.WEP104)
|
||||
}
|
||||
|
||||
"WPA" -> {
|
||||
passwordWithQuotes?.let {
|
||||
preSharedKey = it
|
||||
@@ -270,6 +267,7 @@ object WifiConnector {
|
||||
allowedGroupCiphers.set(WifiConfiguration.GroupCipher.TKIP)
|
||||
allowedGroupCiphers.set(WifiConfiguration.GroupCipher.CCMP)
|
||||
}
|
||||
|
||||
"WPA2-EAP" -> requireSdk(Build.VERSION_CODES.JELLY_BEAN_MR2) {
|
||||
passwordWithQuotes?.let {
|
||||
preSharedKey = it
|
||||
@@ -303,11 +301,32 @@ object WifiConnector {
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.Q)
|
||||
private fun WifiManager.addNetworkFromBuilder(
|
||||
builder: WifiNetworkSuggestion.Builder
|
||||
): Boolean {
|
||||
val suggestion = builder.build()
|
||||
val suggestions = listOf(suggestion)
|
||||
// Remove previous conflicting network suggestion.
|
||||
removeNetworkSuggestions(suggestions)
|
||||
val result = addNetworkSuggestions(suggestions)
|
||||
return result == WifiManager.STATUS_NETWORK_SUGGESTIONS_SUCCESS
|
||||
}
|
||||
|
||||
// Make sure the string ends with a semicolon.
|
||||
private fun String.trimAndTerminate(): String {
|
||||
var trimmed = trim()
|
||||
if (trimmed.isNotEmpty() && trimmed.last() != ';') {
|
||||
trimmed += ";"
|
||||
}
|
||||
return trimmed
|
||||
}
|
||||
|
||||
// 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 ->
|
||||
get() = replace(escapedRegex) { escaped ->
|
||||
escaped.groupValues[1]
|
||||
}
|
||||
|
||||
|
||||
@@ -8,11 +8,8 @@ import android.content.pm.PackageManager
|
||||
import android.graphics.Matrix
|
||||
import android.graphics.Rect
|
||||
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
|
||||
@@ -23,7 +20,13 @@ import android.view.View
|
||||
import android.widget.SeekBar
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.adapter.prettifyFormatName
|
||||
import de.markusfisch.android.binaryeye.app.*
|
||||
import de.markusfisch.android.binaryeye.app.PERMISSION_CAMERA
|
||||
import de.markusfisch.android.binaryeye.app.applyLocale
|
||||
import de.markusfisch.android.binaryeye.app.db
|
||||
import de.markusfisch.android.binaryeye.app.hasBluetoothPermission
|
||||
import de.markusfisch.android.binaryeye.app.hasCameraPermission
|
||||
import de.markusfisch.android.binaryeye.app.prefs
|
||||
import de.markusfisch.android.binaryeye.bluetooth.sendBluetoothAsync
|
||||
import de.markusfisch.android.binaryeye.content.copyToClipboard
|
||||
import de.markusfisch.android.binaryeye.content.execShareIntent
|
||||
import de.markusfisch.android.binaryeye.content.openUrl
|
||||
@@ -33,20 +36,21 @@ import de.markusfisch.android.binaryeye.graphics.FrameMetrics
|
||||
import de.markusfisch.android.binaryeye.graphics.mapPosition
|
||||
import de.markusfisch.android.binaryeye.graphics.setFrameRoi
|
||||
import de.markusfisch.android.binaryeye.graphics.setFrameToView
|
||||
import de.markusfisch.android.binaryeye.media.releaseToneGenerators
|
||||
import de.markusfisch.android.binaryeye.net.sendAsync
|
||||
import de.markusfisch.android.binaryeye.net.urlEncode
|
||||
import de.markusfisch.android.binaryeye.os.error
|
||||
import de.markusfisch.android.binaryeye.os.getVibrator
|
||||
import de.markusfisch.android.binaryeye.os.vibrate
|
||||
import de.markusfisch.android.binaryeye.view.errorFeedback
|
||||
import de.markusfisch.android.binaryeye.view.initSystemBars
|
||||
import de.markusfisch.android.binaryeye.view.scanFeedback
|
||||
import de.markusfisch.android.binaryeye.view.setPaddingFromWindowInsets
|
||||
import de.markusfisch.android.binaryeye.widget.DetectorView
|
||||
import de.markusfisch.android.binaryeye.widget.toast
|
||||
import de.markusfisch.android.cameraview.widget.CameraView
|
||||
import de.markusfisch.android.zxingcpp.ZxingCpp
|
||||
import de.markusfisch.android.zxingcpp.ZxingCpp.Format
|
||||
import de.markusfisch.android.zxingcpp.ZxingCpp.BarcodeFormat
|
||||
import de.markusfisch.android.zxingcpp.ZxingCpp.Binarizer
|
||||
import de.markusfisch.android.zxingcpp.ZxingCpp.ReaderOptions
|
||||
import de.markusfisch.android.zxingcpp.ZxingCpp.Result
|
||||
import java.nio.charset.Charset
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
import kotlin.math.roundToInt
|
||||
@@ -55,13 +59,12 @@ class CameraActivity : AppCompatActivity() {
|
||||
private val frameRoi = Rect()
|
||||
private val matrix = Matrix()
|
||||
|
||||
private lateinit var vibrator: Vibrator
|
||||
private lateinit var cameraView: CameraView
|
||||
private lateinit var detectorView: DetectorView
|
||||
private lateinit var zoomBar: SeekBar
|
||||
private lateinit var flashFab: FloatingActionButton
|
||||
|
||||
private var formats = setOf<String>()
|
||||
private var formatsToRead = setOf<BarcodeFormat>()
|
||||
private var frameMetrics = FrameMetrics()
|
||||
private var decoding = true
|
||||
private var returnResult = false
|
||||
@@ -72,6 +75,7 @@ class CameraActivity : AppCompatActivity() {
|
||||
private var restrictFormat: String? = null
|
||||
private var ignoreNext: String? = null
|
||||
private var fallbackBuffer: IntArray? = null
|
||||
private var requestCameraPermission = true
|
||||
|
||||
override fun onRequestPermissionsResult(
|
||||
requestCode: Int,
|
||||
@@ -82,8 +86,7 @@ class CameraActivity : AppCompatActivity() {
|
||||
PERMISSION_CAMERA -> if (grantResults.isNotEmpty() &&
|
||||
grantResults[0] != PackageManager.PERMISSION_GRANTED
|
||||
) {
|
||||
toast(R.string.no_camera_no_fun)
|
||||
finish()
|
||||
toast(R.string.camera_error)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -118,8 +121,6 @@ class CameraActivity : AppCompatActivity() {
|
||||
// custom locale.
|
||||
setTitle(R.string.scan_code)
|
||||
|
||||
vibrator = getVibrator()
|
||||
|
||||
initSystemBars(this)
|
||||
setSupportActionBar(findViewById(R.id.toolbar) as Toolbar)
|
||||
|
||||
@@ -144,6 +145,7 @@ class CameraActivity : AppCompatActivity() {
|
||||
fallbackBuffer = null
|
||||
saveZoom()
|
||||
detectorView.saveCropHandlePos()
|
||||
releaseToneGenerators()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
@@ -156,14 +158,18 @@ class CameraActivity : AppCompatActivity() {
|
||||
ignoreNext = null
|
||||
}
|
||||
setReturnTarget(intent)
|
||||
if (hasCameraPermission()) {
|
||||
// Avoid asking multiple times when the user has denied access
|
||||
// for this session. Otherwise ActivityCompat.requestPermissions()
|
||||
// will trigger onResume() again and again.
|
||||
if (hasCameraPermission(requestCameraPermission)) {
|
||||
openCamera()
|
||||
}
|
||||
requestCameraPermission = false
|
||||
}
|
||||
|
||||
private fun updateHints() {
|
||||
val restriction = restrictFormat
|
||||
formats = if (restriction != null) {
|
||||
formatsToRead = if (restriction != null) {
|
||||
title = getString(
|
||||
R.string.scan_format,
|
||||
prettifyFormatName(restriction)
|
||||
@@ -172,7 +178,7 @@ class CameraActivity : AppCompatActivity() {
|
||||
} else {
|
||||
setTitle(R.string.scan_code)
|
||||
prefs.barcodeFormats
|
||||
}
|
||||
}.toFormatSet()
|
||||
}
|
||||
|
||||
private fun setReturnTarget(intent: Intent?) {
|
||||
@@ -180,6 +186,7 @@ class CameraActivity : AppCompatActivity() {
|
||||
intent?.action == "com.google.zxing.client.android.SCAN" -> {
|
||||
returnResult = true
|
||||
}
|
||||
|
||||
intent?.dataString?.isReturnUrl() == true -> {
|
||||
finishAfterShowingResult = true
|
||||
returnUrlTemplate = intent.data?.getQueryParameter("ret")
|
||||
@@ -248,44 +255,52 @@ class CameraActivity : AppCompatActivity() {
|
||||
createBarcode()
|
||||
true
|
||||
}
|
||||
|
||||
R.id.history -> {
|
||||
startActivity(MainActivity.getHistoryIntent(this))
|
||||
true
|
||||
}
|
||||
|
||||
R.id.pick_file -> {
|
||||
startActivityForResult(
|
||||
Intent.createChooser(
|
||||
Intent(Intent.ACTION_GET_CONTENT).apply {
|
||||
type = "image/*"
|
||||
},
|
||||
getString(R.string.pick_file)
|
||||
getString(R.string.pick_image_file)
|
||||
),
|
||||
PICK_FILE_RESULT_CODE
|
||||
)
|
||||
true
|
||||
}
|
||||
|
||||
R.id.switch_camera -> {
|
||||
switchCamera()
|
||||
true
|
||||
}
|
||||
|
||||
R.id.bulk_mode -> {
|
||||
bulkMode = bulkMode xor true
|
||||
item.isChecked = bulkMode
|
||||
ignoreNext = null
|
||||
true
|
||||
}
|
||||
|
||||
R.id.restrict_format -> {
|
||||
showRestrictionDialog()
|
||||
true
|
||||
}
|
||||
|
||||
R.id.preferences -> {
|
||||
startActivity(MainActivity.getPreferencesIntent(this))
|
||||
true
|
||||
}
|
||||
|
||||
R.id.info -> {
|
||||
openReadme()
|
||||
true
|
||||
}
|
||||
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
}
|
||||
@@ -354,6 +369,7 @@ class CameraActivity : AppCompatActivity() {
|
||||
progress = zoomBar.progress
|
||||
return true
|
||||
}
|
||||
|
||||
MotionEvent.ACTION_MOVE -> {
|
||||
if (prefs.zoomBySwiping) {
|
||||
v ?: return false
|
||||
@@ -367,6 +383,7 @@ class CameraActivity : AppCompatActivity() {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
MotionEvent.ACTION_UP -> {
|
||||
// Stop calling focusTo() as soon as it returns false
|
||||
// to avoid throwing and catching future exceptions.
|
||||
@@ -414,7 +431,6 @@ class CameraActivity : AppCompatActivity() {
|
||||
|
||||
override fun onCameraError() {
|
||||
this@CameraActivity.toast(R.string.camera_error)
|
||||
finish()
|
||||
}
|
||||
|
||||
override fun onCameraReady(camera: Camera) {
|
||||
@@ -426,22 +442,39 @@ class CameraActivity : AppCompatActivity() {
|
||||
updateFrameRoiAndMappingMatrix()
|
||||
ignoreNext = null
|
||||
decoding = true
|
||||
// These settings can't change while the camera is open.
|
||||
val options = ReaderOptions(
|
||||
tryHarder = prefs.tryHarder,
|
||||
tryRotate = prefs.autoRotate,
|
||||
tryInvert = true,
|
||||
tryDownscale = true,
|
||||
maxNumberOfSymbols = 1
|
||||
)
|
||||
var useLocalAverage = false
|
||||
camera.setPreviewCallback { frameData, _ ->
|
||||
if (decoding) {
|
||||
useLocalAverage = useLocalAverage xor true
|
||||
ZxingCpp.readByteArray(
|
||||
frameData,
|
||||
frameMetrics.width,
|
||||
frameRoi.left, frameRoi.top,
|
||||
frameRoi.width(), frameRoi.height(),
|
||||
frameMetrics.orientation,
|
||||
formats.joinToString(),
|
||||
prefs.tryHarder,
|
||||
prefs.autoRotate,
|
||||
tryInvert = true,
|
||||
tryDownscale = true
|
||||
)?.let { result ->
|
||||
options.apply {
|
||||
// By default, ZXing uses LOCAL_AVERAGE, but
|
||||
// this does not work well with inverted
|
||||
// barcodes on low-contrast backgrounds.
|
||||
binarizer = if (useLocalAverage) {
|
||||
Binarizer.LOCAL_AVERAGE
|
||||
} else {
|
||||
Binarizer.GLOBAL_HISTOGRAM
|
||||
}
|
||||
formats = formatsToRead
|
||||
}
|
||||
)?.let { results ->
|
||||
val result = results.first()
|
||||
if (result.text != ignoreNext) {
|
||||
postResult(result.redact())
|
||||
postResult(result)
|
||||
decoding = false
|
||||
}
|
||||
}
|
||||
@@ -561,7 +594,7 @@ class CameraActivity : AppCompatActivity() {
|
||||
detectorView.coordinates
|
||||
)
|
||||
)
|
||||
vibrator.vibrate()
|
||||
scanFeedback()
|
||||
val returnUri = returnUrlTemplate?.let {
|
||||
try {
|
||||
completeUrl(it, result)
|
||||
@@ -577,16 +610,13 @@ class CameraActivity : AppCompatActivity() {
|
||||
setResult(Activity.RESULT_OK, getReturnIntent(result))
|
||||
finish()
|
||||
}
|
||||
|
||||
returnUri != null -> execShareIntent(
|
||||
Intent(Intent.ACTION_VIEW, returnUri)
|
||||
)
|
||||
|
||||
else -> {
|
||||
showResult(
|
||||
this@CameraActivity,
|
||||
result,
|
||||
vibrator,
|
||||
bulkMode
|
||||
)
|
||||
showResult(result, bulkMode)
|
||||
// If this app was invoked via a deep link but without
|
||||
// a return URI, we probably don't want to return to
|
||||
// the camera screen after scanning, but to the caller.
|
||||
@@ -596,7 +626,9 @@ class CameraActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
if (bulkMode) {
|
||||
ignoreNext = result.text
|
||||
if (prefs.ignoreConsecutiveDuplicates) {
|
||||
ignoreNext = result.text
|
||||
}
|
||||
if (prefs.showToastInBulkMode) {
|
||||
toast(result.text)
|
||||
}
|
||||
@@ -617,42 +649,16 @@ class CameraActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
fun Result.redact() = if (
|
||||
format == Format.PDF_417.name &&
|
||||
text.isNotEmpty()
|
||||
) {
|
||||
Result(
|
||||
format,
|
||||
contentType,
|
||||
String(
|
||||
text.toByteArray(Charset.forName("ISO-8859-1")),
|
||||
Charset.forName("UTF-8")
|
||||
),
|
||||
position,
|
||||
orientation,
|
||||
rawBytes,
|
||||
ecLevel,
|
||||
symbologyIdentifier,
|
||||
sequenceSize,
|
||||
sequenceIndex,
|
||||
sequenceId,
|
||||
readerInit,
|
||||
lineCount,
|
||||
versionNumber,
|
||||
gtin
|
||||
)
|
||||
} else {
|
||||
this
|
||||
}
|
||||
fun Set<String>.toFormatSet(): Set<BarcodeFormat> = map {
|
||||
BarcodeFormat.valueOf(it)
|
||||
}.toSet()
|
||||
|
||||
fun showResult(
|
||||
activity: Activity,
|
||||
fun Activity.showResult(
|
||||
result: Result,
|
||||
vibrator: Vibrator,
|
||||
bulkMode: Boolean = false,
|
||||
) {
|
||||
if (prefs.copyImmediately) {
|
||||
activity.copyToClipboard(result.text)
|
||||
copyToClipboard(result.text)
|
||||
}
|
||||
val scan = result.toScan()
|
||||
if (prefs.useHistory) {
|
||||
@@ -660,7 +666,7 @@ fun showResult(
|
||||
}
|
||||
if (prefs.sendScanActive && prefs.sendScanUrl.isNotEmpty()) {
|
||||
if (prefs.sendScanType == "4") {
|
||||
activity.openUrl(
|
||||
openUrl(
|
||||
prefs.sendScanUrl + scan.content.urlEncode()
|
||||
)
|
||||
return
|
||||
@@ -670,28 +676,49 @@ fun showResult(
|
||||
prefs.sendScanType
|
||||
) { code, body ->
|
||||
if (code == null || code < 200 || code > 299) {
|
||||
vibrator.error()
|
||||
if (!activity.isSilent()) {
|
||||
beepBeepBeep()
|
||||
}
|
||||
errorFeedback()
|
||||
}
|
||||
if (body != null && body.isNotEmpty()) {
|
||||
activity.toast(body)
|
||||
if (!body.isNullOrEmpty()) {
|
||||
toast(body)
|
||||
} else if (code == null || code > 299) {
|
||||
activity.toast(R.string.background_request_failed)
|
||||
toast(R.string.background_request_failed)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (prefs.sendScanBluetooth &&
|
||||
prefs.sendScanBluetoothHost.isNotEmpty() &&
|
||||
hasBluetoothPermission()
|
||||
) {
|
||||
scan.sendBluetoothAsync(
|
||||
prefs.sendScanBluetoothHost
|
||||
) { connected, sent ->
|
||||
toast(
|
||||
when {
|
||||
!connected -> {
|
||||
errorFeedback()
|
||||
R.string.bluetooth_connect_fail
|
||||
}
|
||||
|
||||
!sent -> {
|
||||
errorFeedback()
|
||||
R.string.bluetooth_send_fail
|
||||
}
|
||||
|
||||
else -> R.string.bluetooth_send_success
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
if (!bulkMode) {
|
||||
activity.startActivity(
|
||||
MainActivity.getDecodeIntent(activity, scan)
|
||||
startActivity(
|
||||
MainActivity.getDecodeIntent(this, scan)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getReturnIntent(result: Result) = Intent().apply {
|
||||
putExtra("SCAN_RESULT", result.text)
|
||||
putExtra("SCAN_RESULT_FORMAT", result.format)
|
||||
putExtra("SCAN_RESULT_FORMAT", result.format.toString())
|
||||
putExtra("SCAN_RESULT_ORIENTATION", result.orientation)
|
||||
putExtra("SCAN_RESULT_ERROR_CORRECTION_LEVEL", result.ecLevel)
|
||||
if (result.rawBytes.isNotEmpty()) {
|
||||
@@ -709,25 +736,7 @@ private fun completeUrl(urlTemplate: String, result: Result) = Uri.parse(
|
||||
urlTemplate
|
||||
.replace("{RESULT}", result.text.urlEncode())
|
||||
.replace("{RESULT_BYTES}", result.rawBytes.toHexString())
|
||||
.replace(
|
||||
"{FORMAT}", result.format.urlEncode()
|
||||
)
|
||||
.replace("{FORMAT}", result.format.name.urlEncode())
|
||||
// And support {CODE} from the old ZXing app, too.
|
||||
.replace("{CODE}", result.text.urlEncode())
|
||||
)
|
||||
|
||||
private fun Context.isSilent(): Boolean {
|
||||
val am = getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
||||
return when (am.ringerMode) {
|
||||
AudioManager.RINGER_MODE_SILENT,
|
||||
AudioManager.RINGER_MODE_VIBRATE -> true
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
private fun beepBeepBeep() {
|
||||
ToneGenerator(AudioManager.STREAM_ALARM, 100).startTone(
|
||||
ToneGenerator.TONE_CDMA_CONFIRM,
|
||||
1000
|
||||
)
|
||||
}
|
||||
|
||||
@@ -9,7 +9,12 @@ 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.*
|
||||
import de.markusfisch.android.binaryeye.app.PERMISSION_LOCATION
|
||||
import de.markusfisch.android.binaryeye.app.PERMISSION_WRITE
|
||||
import de.markusfisch.android.binaryeye.app.applyLocale
|
||||
import de.markusfisch.android.binaryeye.app.permissionGrantedCallback
|
||||
import de.markusfisch.android.binaryeye.app.prefs
|
||||
import de.markusfisch.android.binaryeye.app.setFragment
|
||||
import de.markusfisch.android.binaryeye.database.Scan
|
||||
import de.markusfisch.android.binaryeye.fragment.DecodeFragment
|
||||
import de.markusfisch.android.binaryeye.fragment.EncodeFragment
|
||||
@@ -80,10 +85,14 @@ class MainActivity : AppCompatActivity() {
|
||||
return when {
|
||||
intent.hasExtra(PREFERENCES) -> PreferencesFragment()
|
||||
intent.hasExtra(HISTORY) -> HistoryFragment()
|
||||
intent.hasExtra(ENCODE) -> EncodeFragment.newInstance()
|
||||
intent.hasExtra(DECODED) -> DecodeFragment.newInstance(
|
||||
intent.getParcelableExtra(DECODED)!!
|
||||
intent.hasExtra(ENCODE) -> EncodeFragment.newInstance(
|
||||
intent.getStringExtra(ENCODE)
|
||||
)
|
||||
|
||||
intent.hasExtra(DECODED) -> intent.getScanExtra(DECODED)?.let {
|
||||
DecodeFragment.newInstance(it)
|
||||
} ?: DecodeFragment()
|
||||
|
||||
else -> PreferencesFragment()
|
||||
}
|
||||
}
|
||||
@@ -128,3 +137,12 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun Intent.getScanExtra(name: String): Scan? = if (
|
||||
Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU
|
||||
) {
|
||||
@Suppress("DEPRECATION")
|
||||
getParcelableExtra(name)
|
||||
} else {
|
||||
getParcelableExtra(name, Scan::class.java)
|
||||
}
|
||||
|
||||
@@ -7,9 +7,8 @@ import android.graphics.Matrix
|
||||
import android.graphics.Rect
|
||||
import android.graphics.RectF
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Parcelable
|
||||
import android.os.Vibrator
|
||||
import android.support.v7.app.AppCompatActivity
|
||||
import android.support.v7.widget.Toolbar
|
||||
import android.view.Menu
|
||||
@@ -21,18 +20,24 @@ import de.markusfisch.android.binaryeye.graphics.crop
|
||||
import de.markusfisch.android.binaryeye.graphics.fixTransparency
|
||||
import de.markusfisch.android.binaryeye.graphics.loadImageUri
|
||||
import de.markusfisch.android.binaryeye.graphics.mapPosition
|
||||
import de.markusfisch.android.binaryeye.os.getVibrator
|
||||
import de.markusfisch.android.binaryeye.os.vibrate
|
||||
import de.markusfisch.android.binaryeye.media.releaseToneGenerators
|
||||
import de.markusfisch.android.binaryeye.view.colorSystemAndToolBars
|
||||
import de.markusfisch.android.binaryeye.view.initSystemBars
|
||||
import de.markusfisch.android.binaryeye.view.recordToolbarHeight
|
||||
import de.markusfisch.android.binaryeye.view.scanFeedback
|
||||
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.zxingcpp.ZxingCpp
|
||||
import de.markusfisch.android.zxingcpp.ZxingCpp.Binarizer
|
||||
import de.markusfisch.android.zxingcpp.ZxingCpp.ReaderOptions
|
||||
import de.markusfisch.android.zxingcpp.ZxingCpp.Result
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
import kotlin.math.roundToInt
|
||||
@@ -41,10 +46,18 @@ class PickActivity : AppCompatActivity() {
|
||||
private val matrix = Matrix()
|
||||
private val parentJob = Job()
|
||||
private val scope = CoroutineScope(Dispatchers.IO + parentJob)
|
||||
private val options = ReaderOptions(
|
||||
tryHarder = true,
|
||||
tryRotate = true,
|
||||
tryInvert = true,
|
||||
tryDownscale = true,
|
||||
maxNumberOfSymbols = 1,
|
||||
formats = prefs.barcodeFormats.toFormatSet()
|
||||
)
|
||||
|
||||
private lateinit var vibrator: Vibrator
|
||||
private lateinit var cropImageView: CropImageView
|
||||
private lateinit var detectorView: DetectorView
|
||||
private lateinit var freeRotationItem: MenuItem
|
||||
|
||||
private var result: Result? = null
|
||||
|
||||
@@ -61,8 +74,6 @@ class PickActivity : AppCompatActivity() {
|
||||
// locale.
|
||||
setTitle(R.string.pick_code_to_scan)
|
||||
|
||||
vibrator = getVibrator()
|
||||
|
||||
initSystemBars(this)
|
||||
val toolbar = findViewById(R.id.toolbar) as Toolbar
|
||||
recordToolbarHeight(toolbar)
|
||||
@@ -81,7 +92,7 @@ class PickActivity : AppCompatActivity() {
|
||||
|
||||
cropImageView = findViewById(R.id.image) as CropImageView
|
||||
cropImageView.restrictTranslation = false
|
||||
cropImageView.freeRotation = true
|
||||
cropImageView.freeRotation = prefs.freeRotation
|
||||
cropImageView.setImageBitmap(bitmap)
|
||||
cropImageView.onScan = {
|
||||
scanWithinBounds(bitmap)
|
||||
@@ -114,52 +125,47 @@ class PickActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
private fun scanWithinBounds(bitmap: Bitmap) {
|
||||
val viewRoi = if (detectorView.roi.width() < 1) {
|
||||
cropImageView.getBoundsRect()
|
||||
} else {
|
||||
detectorView.roi
|
||||
}
|
||||
val mappedRect = cropImageView.mappedRect
|
||||
val cropped = bitmap.crop(
|
||||
getNormalizedRoi(mappedRect, viewRoi),
|
||||
cropImageView.imageRotation,
|
||||
cropImageView.pivotX,
|
||||
cropImageView.pivotY
|
||||
) ?: return
|
||||
val croppedInView = Rect(
|
||||
max(viewRoi.left, mappedRect.left.roundToInt()),
|
||||
max(viewRoi.top, mappedRect.top.roundToInt()),
|
||||
min(viewRoi.right, mappedRect.right.roundToInt()),
|
||||
min(viewRoi.bottom, mappedRect.bottom.roundToInt())
|
||||
val viewRoi = Rect(
|
||||
if (detectorView.roi.width() < 1) {
|
||||
cropImageView.getBoundsRect()
|
||||
} else {
|
||||
detectorView.roi
|
||||
}
|
||||
)
|
||||
matrix.apply {
|
||||
setScale(
|
||||
croppedInView.width().toFloat() / cropped.width,
|
||||
croppedInView.height().toFloat() / cropped.height
|
||||
)
|
||||
postTranslate(
|
||||
croppedInView.left.toFloat(),
|
||||
croppedInView.top.toFloat()
|
||||
)
|
||||
}
|
||||
val mappedRect = RectF(cropImageView.mappedRect)
|
||||
val imageRotation = cropImageView.imageRotation
|
||||
val pivotX = cropImageView.pivotX
|
||||
val pivotY = cropImageView.pivotY
|
||||
scope.launch {
|
||||
ZxingCpp.readBitmap(
|
||||
cropped,
|
||||
0, 0,
|
||||
cropped.width, cropped.height,
|
||||
0,
|
||||
prefs.barcodeFormats.joinToString(),
|
||||
tryHarder = true,
|
||||
tryRotate = true,
|
||||
tryInvert = true,
|
||||
tryDownscale = true
|
||||
)?.let {
|
||||
val cropped = bitmap.crop(
|
||||
getNormalizedRoi(mappedRect, viewRoi),
|
||||
imageRotation,
|
||||
pivotX,
|
||||
pivotY
|
||||
) ?: return@launch
|
||||
val croppedInView = Rect(
|
||||
max(viewRoi.left, mappedRect.left.roundToInt()),
|
||||
max(viewRoi.top, mappedRect.top.roundToInt()),
|
||||
min(viewRoi.right, mappedRect.right.roundToInt()),
|
||||
min(viewRoi.bottom, mappedRect.bottom.roundToInt())
|
||||
)
|
||||
matrix.apply {
|
||||
setScale(
|
||||
croppedInView.width().toFloat() / cropped.width,
|
||||
croppedInView.height().toFloat() / cropped.height
|
||||
)
|
||||
postTranslate(
|
||||
croppedInView.left.toFloat(),
|
||||
croppedInView.top.toFloat()
|
||||
)
|
||||
}
|
||||
cropped.decode()?.first()?.let {
|
||||
withContext(Dispatchers.Main) {
|
||||
if (isFinishing) {
|
||||
return@withContext
|
||||
}
|
||||
result = it
|
||||
vibrator.vibrate()
|
||||
scanFeedback()
|
||||
detectorView.update(
|
||||
matrix.mapPosition(
|
||||
it.position,
|
||||
@@ -171,14 +177,38 @@ class PickActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
// By default, ZXing uses LOCAL_AVERAGE, but this does not work
|
||||
// well with inverted barcodes on low-contrast backgrounds.
|
||||
private fun Bitmap.decode() = ZxingCpp.readBitmap(
|
||||
this,
|
||||
0, 0,
|
||||
width, height,
|
||||
0,
|
||||
options.apply {
|
||||
binarizer = Binarizer.LOCAL_AVERAGE
|
||||
}
|
||||
) ?: ZxingCpp.readBitmap(
|
||||
this,
|
||||
0, 0,
|
||||
width, height,
|
||||
0,
|
||||
options.apply {
|
||||
binarizer = Binarizer.GLOBAL_HISTOGRAM
|
||||
}
|
||||
)
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
detectorView.saveCropHandlePos()
|
||||
parentJob.cancel()
|
||||
releaseToneGenerators()
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||
menuInflater.inflate(R.menu.activity_pick, menu)
|
||||
freeRotationItem = menu.findItem(R.id.toggle_free).apply {
|
||||
updateFreeRotationIcon()
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -188,18 +218,34 @@ class PickActivity : AppCompatActivity() {
|
||||
rotateClockwise()
|
||||
true
|
||||
}
|
||||
|
||||
R.id.toggle_free -> {
|
||||
prefs.freeRotation = prefs.freeRotation xor true
|
||||
cropImageView.freeRotation = prefs.freeRotation
|
||||
freeRotationItem.updateFreeRotationIcon()
|
||||
true
|
||||
}
|
||||
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
}
|
||||
|
||||
private fun MenuItem.updateFreeRotationIcon() {
|
||||
setIcon(
|
||||
if (prefs.freeRotation) {
|
||||
R.drawable.ic_action_rotation_unlocked
|
||||
} else {
|
||||
R.drawable.ic_action_rotation_locked
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun rotateClockwise() {
|
||||
cropImageView.imageRotation = cropImageView.imageRotation + 90 % 360
|
||||
}
|
||||
|
||||
private fun loadSentImage(intent: Intent): Bitmap? {
|
||||
val uri = intent.getParcelableExtra<Parcelable>(
|
||||
Intent.EXTRA_STREAM
|
||||
) as? Uri ?: return null
|
||||
val uri = intent.getUriExtra() ?: return null
|
||||
return contentResolver.loadImageUri(uri)
|
||||
}
|
||||
|
||||
@@ -211,7 +257,7 @@ class PickActivity : AppCompatActivity() {
|
||||
private fun showResult() {
|
||||
val r = result
|
||||
if (r != null) {
|
||||
showResult(this, r.redact(), vibrator)
|
||||
showResult(r)
|
||||
finish()
|
||||
} else {
|
||||
applicationContext.toast(R.string.no_barcode_found)
|
||||
@@ -229,3 +275,12 @@ private fun getNormalizedRoi(imageRect: RectF, roi: Rect): RectF {
|
||||
(roi.bottom - imageRect.top) / h
|
||||
)
|
||||
}
|
||||
|
||||
private fun Intent.getUriExtra(): Uri? = if (
|
||||
Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU
|
||||
) {
|
||||
@Suppress("DEPRECATION")
|
||||
getParcelableExtra(Intent.EXTRA_STREAM)
|
||||
} else {
|
||||
getParcelableExtra(Intent.EXTRA_STREAM, Uri::class.java)
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ import de.markusfisch.android.binaryeye.database.Database
|
||||
import java.text.DateFormat
|
||||
import java.text.ParseException
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
import java.util.Locale
|
||||
|
||||
class ScansAdapter(context: Context, cursor: Cursor) :
|
||||
CursorAdapter(context, cursor, false) {
|
||||
@@ -99,9 +99,11 @@ class ScansAdapter(context: Context, cursor: Cursor) :
|
||||
icon = R.drawable.ic_label
|
||||
name
|
||||
}
|
||||
content?.isEmpty() == true -> context.getString(
|
||||
|
||||
content.isNullOrEmpty() -> context.getString(
|
||||
R.string.binary_data
|
||||
)
|
||||
|
||||
else -> content
|
||||
}
|
||||
contentView.setCompoundDrawablesWithIntrinsicBounds(
|
||||
|
||||
@@ -2,7 +2,7 @@ package de.markusfisch.android.binaryeye.app
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import java.util.*
|
||||
import java.util.Locale
|
||||
|
||||
fun Context.applyLocale(localeName: String) {
|
||||
if (localeName.isEmpty()) {
|
||||
|
||||
@@ -3,15 +3,16 @@ package de.markusfisch.android.binaryeye.app
|
||||
import android.Manifest
|
||||
import android.app.Activity
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import android.support.v4.app.ActivityCompat
|
||||
import android.support.v4.content.ContextCompat
|
||||
|
||||
var permissionGrantedCallback: (() -> Any)? = null
|
||||
|
||||
const val PERMISSION_CAMERA = 1
|
||||
fun Activity.hasCameraPermission() = hasPermission(
|
||||
fun Activity.hasCameraPermission(request: Boolean = true) = hasPermission(
|
||||
Manifest.permission.CAMERA,
|
||||
PERMISSION_CAMERA
|
||||
if (request) PERMISSION_CAMERA else 0
|
||||
)
|
||||
|
||||
const val PERMISSION_WRITE = 2
|
||||
@@ -32,6 +33,18 @@ fun Activity.hasLocationPermission(callback: () -> Any): Boolean {
|
||||
)
|
||||
}
|
||||
|
||||
const val PERMISSION_BLUETOOTH = 4
|
||||
fun Activity.hasBluetoothPermission() = if (
|
||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
|
||||
) {
|
||||
hasPermission(
|
||||
Manifest.permission.BLUETOOTH_CONNECT,
|
||||
PERMISSION_BLUETOOTH
|
||||
)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
|
||||
private fun Activity.hasPermission(
|
||||
permission: String,
|
||||
requestCode: Int
|
||||
@@ -40,11 +53,13 @@ private fun Activity.hasPermission(
|
||||
permission
|
||||
) != PackageManager.PERMISSION_GRANTED
|
||||
) {
|
||||
ActivityCompat.requestPermissions(
|
||||
this,
|
||||
arrayOf(permission),
|
||||
requestCode
|
||||
)
|
||||
if (requestCode > 0) {
|
||||
ActivityCompat.requestPermissions(
|
||||
this,
|
||||
arrayOf(permission),
|
||||
requestCode
|
||||
)
|
||||
}
|
||||
false
|
||||
} else {
|
||||
true
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
package de.markusfisch.android.binaryeye.bluetooth
|
||||
|
||||
import android.bluetooth.BluetoothAdapter
|
||||
import android.bluetooth.BluetoothDevice
|
||||
import android.bluetooth.BluetoothSocket
|
||||
import android.support.v7.preference.ListPreference
|
||||
import de.markusfisch.android.binaryeye.database.Scan
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.IOException
|
||||
import java.io.OutputStreamWriter
|
||||
import java.util.UUID
|
||||
|
||||
fun Scan.sendBluetoothAsync(
|
||||
host: String,
|
||||
callback: (Boolean, Boolean) -> Unit
|
||||
) {
|
||||
CoroutineScope(Dispatchers.IO).launch(Dispatchers.IO) {
|
||||
val connected = if (isConnected) {
|
||||
true
|
||||
} else {
|
||||
connect(host)
|
||||
}
|
||||
val sent = if (connected) {
|
||||
send(content)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
callback(connected, sent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun setBluetoothHosts(listPref: ListPreference) {
|
||||
val devices = try {
|
||||
BluetoothAdapter.getDefaultAdapter().bondedDevices
|
||||
} catch (e: SecurityException) {
|
||||
// Do nothing, either the user has denied Bluetooth access
|
||||
// or the permission was removed by the system. We're catching
|
||||
// the exception to keep the app from crashing.
|
||||
null
|
||||
} ?: return
|
||||
listPref.entries = devices.map {
|
||||
it.name
|
||||
}.toTypedArray()
|
||||
listPref.entryValues = devices.map {
|
||||
it.address
|
||||
}.toTypedArray()
|
||||
listPref.callChangeListener(listPref.value)
|
||||
}
|
||||
|
||||
private var socket: BluetoothSocket? = null
|
||||
private var writer: OutputStreamWriter? = null
|
||||
|
||||
private val blue = BluetoothAdapter.getDefaultAdapter()
|
||||
private val uuid = UUID.fromString(
|
||||
"8a8478c9-2ca8-404b-a0de-101f34ab71ae"
|
||||
)
|
||||
|
||||
private var isConnected = false
|
||||
|
||||
private fun connect(deviceName: String): Boolean = try {
|
||||
val device = findByName(deviceName) ?: throw RuntimeException(
|
||||
"Bluetooth device not found"
|
||||
)
|
||||
socket = device.createRfcommSocketToServiceRecord(uuid)
|
||||
socket?.connect()
|
||||
writer = socket?.outputStream?.writer()
|
||||
isConnected = true
|
||||
true
|
||||
} catch (e: Exception) {
|
||||
close()
|
||||
false
|
||||
}
|
||||
|
||||
private fun send(message: String): Boolean = try {
|
||||
writer?.apply {
|
||||
write(message)
|
||||
write("\n")
|
||||
flush()
|
||||
}
|
||||
true
|
||||
} catch (e: Exception) {
|
||||
close()
|
||||
false
|
||||
}
|
||||
|
||||
private fun close() {
|
||||
try {
|
||||
writer?.close()
|
||||
} catch (e: IOException) {
|
||||
// Catch exception if writer wasn't initialized.
|
||||
}
|
||||
try {
|
||||
socket?.close()
|
||||
} catch (e: IOException) {
|
||||
// Nothing we can do about this but keep
|
||||
// the app from crashing.
|
||||
}
|
||||
writer = null
|
||||
socket = null
|
||||
isConnected = false
|
||||
}
|
||||
|
||||
private fun findByName(findableName: String): BluetoothDevice? {
|
||||
val deviceList = blue.bondedDevices
|
||||
for (device in deviceList) {
|
||||
if (device.address == findableName) {
|
||||
return device
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
@@ -10,6 +10,8 @@ import de.markusfisch.android.binaryeye.BuildConfig
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.widget.toast
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
|
||||
fun Context.execShareIntent(intent: Intent): Boolean = if (
|
||||
!startIntent(intent)
|
||||
@@ -35,11 +37,14 @@ fun Context.startIntent(intent: Intent): Boolean = try {
|
||||
false
|
||||
}
|
||||
|
||||
fun Context.openUrl(url: String): Boolean = openUri(url.parseAndNormalizeUri())
|
||||
|
||||
fun Context.openUri(uri: Uri): Boolean = execShareIntent(
|
||||
Intent(Intent.ACTION_VIEW, uri)
|
||||
)
|
||||
fun Context.openUrl(url: String, silent: Boolean = false): Boolean {
|
||||
val intent = Intent(Intent.ACTION_VIEW, url.parseAndNormalizeUri())
|
||||
return if (silent) {
|
||||
startIntent(intent)
|
||||
} else {
|
||||
execShareIntent(intent)
|
||||
}
|
||||
}
|
||||
|
||||
fun String.parseAndNormalizeUri(): Uri = Uri.parse(this).let {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
|
||||
@@ -56,6 +61,44 @@ fun Context.shareText(text: String, mimeType: String = "text/plain") {
|
||||
})
|
||||
}
|
||||
|
||||
private var lastShareFile: File? = null
|
||||
fun wipeLastShareFile() {
|
||||
lastShareFile?.let {
|
||||
it.delete()
|
||||
lastShareFile = null
|
||||
}
|
||||
}
|
||||
|
||||
fun <T> Context.shareAsFile(content: T, fileName: String) {
|
||||
val (bytes, mimeType) = when (content) {
|
||||
is ByteArray -> Pair(
|
||||
content,
|
||||
"text/plain"
|
||||
)
|
||||
|
||||
is String -> Pair(
|
||||
content.toByteArray(),
|
||||
"application/octet-stream"
|
||||
)
|
||||
|
||||
else -> throw IllegalArgumentException(
|
||||
"Unsupported content text for shareAsFile()"
|
||||
)
|
||||
}
|
||||
wipeLastShareFile()
|
||||
val file = File(externalCacheDir, fileName)
|
||||
try {
|
||||
FileOutputStream(file).use {
|
||||
it.write(bytes)
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
toast(R.string.error_saving_file)
|
||||
return
|
||||
}
|
||||
lastShareFile = file
|
||||
shareFile(file, mimeType)
|
||||
}
|
||||
|
||||
fun Context.shareFile(file: File, mimeType: String) {
|
||||
getUriForFile(file)?.let {
|
||||
shareUri(it, mimeType)
|
||||
|
||||
@@ -32,9 +32,10 @@ private fun exportCsv(
|
||||
val columns = arrayOf(
|
||||
Database.SCANS_DATETIME,
|
||||
Database.SCANS_FORMAT,
|
||||
Database.SCANS_NAME,
|
||||
Database.SCANS_CONTENT,
|
||||
Database.SCANS_ERROR_CORRECTION_LEVEL,
|
||||
Database.SCANS_VERSION_NUMBER,
|
||||
Database.SCANS_VERSION,
|
||||
Database.SCANS_SEQUENCE_SIZE,
|
||||
Database.SCANS_SEQUENCE_INDEX,
|
||||
Database.SCANS_SEQUENCE_ID,
|
||||
|
||||
@@ -6,6 +6,7 @@ import android.database.Cursor
|
||||
import android.database.sqlite.SQLiteDatabase
|
||||
import android.database.sqlite.SQLiteOpenHelper
|
||||
import de.markusfisch.android.binaryeye.app.prefs
|
||||
import de.markusfisch.android.zxingcpp.ZxingCpp.BarcodeFormat
|
||||
|
||||
class Database {
|
||||
private lateinit var db: SQLiteDatabase
|
||||
@@ -36,7 +37,7 @@ class Database {
|
||||
$SCANS_RAW,
|
||||
$SCANS_FORMAT,
|
||||
$SCANS_ERROR_CORRECTION_LEVEL,
|
||||
$SCANS_VERSION_NUMBER,
|
||||
$SCANS_VERSION,
|
||||
$SCANS_SEQUENCE_SIZE,
|
||||
$SCANS_SEQUENCE_INDEX,
|
||||
$SCANS_SEQUENCE_ID,
|
||||
@@ -69,6 +70,29 @@ class Database {
|
||||
null
|
||||
}
|
||||
|
||||
fun getScansDetailed(ids: LongArray): Cursor? = db.rawQuery(
|
||||
"""SELECT
|
||||
$SCANS_ID,
|
||||
$SCANS_DATETIME,
|
||||
$SCANS_NAME,
|
||||
$SCANS_CONTENT,
|
||||
$SCANS_RAW,
|
||||
$SCANS_FORMAT,
|
||||
$SCANS_ERROR_CORRECTION_LEVEL,
|
||||
$SCANS_VERSION,
|
||||
$SCANS_SEQUENCE_SIZE,
|
||||
$SCANS_SEQUENCE_INDEX,
|
||||
$SCANS_SEQUENCE_ID,
|
||||
$SCANS_GTIN_COUNTRY,
|
||||
$SCANS_GTIN_ADD_ON,
|
||||
$SCANS_GTIN_PRICE,
|
||||
$SCANS_GTIN_ISSUE_NUMBER
|
||||
FROM $SCANS
|
||||
WHERE $SCANS_ID IN (?)
|
||||
ORDER BY $SCANS_DATETIME DESC
|
||||
""".trimMargin(), arrayOf(ids.joinToString(","))
|
||||
)
|
||||
|
||||
fun getScan(id: Long): Scan? = db.rawQuery(
|
||||
"""SELECT
|
||||
$SCANS_ID,
|
||||
@@ -78,7 +102,7 @@ class Database {
|
||||
$SCANS_RAW,
|
||||
$SCANS_FORMAT,
|
||||
$SCANS_ERROR_CORRECTION_LEVEL,
|
||||
$SCANS_VERSION_NUMBER,
|
||||
$SCANS_VERSION,
|
||||
$SCANS_SEQUENCE_SIZE,
|
||||
$SCANS_SEQUENCE_INDEX,
|
||||
$SCANS_SEQUENCE_ID,
|
||||
@@ -94,9 +118,9 @@ class Database {
|
||||
Scan(
|
||||
it.getString(SCANS_CONTENT),
|
||||
it.getBlob(SCANS_RAW),
|
||||
it.getString(SCANS_FORMAT),
|
||||
BarcodeFormat.valueOf(it.getString(SCANS_FORMAT)),
|
||||
it.getString(SCANS_ERROR_CORRECTION_LEVEL),
|
||||
it.getInt(SCANS_VERSION_NUMBER),
|
||||
it.getString(SCANS_VERSION),
|
||||
it.getInt(SCANS_SEQUENCE_SIZE),
|
||||
it.getInt(SCANS_SEQUENCE_INDEX),
|
||||
it.getString(SCANS_SEQUENCE_ID),
|
||||
@@ -105,7 +129,8 @@ class Database {
|
||||
it.getString(SCANS_GTIN_PRICE),
|
||||
it.getString(SCANS_GTIN_ISSUE_NUMBER),
|
||||
it.getString(SCANS_DATETIME),
|
||||
it.getLong(SCANS_ID)
|
||||
it.getLong(SCANS_ID),
|
||||
it.getString(SCANS_NAME),
|
||||
)
|
||||
} else {
|
||||
null
|
||||
@@ -117,18 +142,15 @@ class Database {
|
||||
null,
|
||||
ContentValues().apply {
|
||||
put(SCANS_DATETIME, scan.dateTime)
|
||||
val isRaw = scan.raw != null
|
||||
if (isRaw) {
|
||||
put(SCANS_CONTENT, "")
|
||||
put(SCANS_CONTENT, scan.content)
|
||||
if (scan.raw != null) {
|
||||
put(SCANS_RAW, scan.raw)
|
||||
} else {
|
||||
put(SCANS_CONTENT, scan.content)
|
||||
}
|
||||
put(SCANS_FORMAT, scan.format)
|
||||
put(SCANS_FORMAT, scan.format.name)
|
||||
scan.errorCorrectionLevel?.let {
|
||||
put(SCANS_ERROR_CORRECTION_LEVEL, it)
|
||||
}
|
||||
put(SCANS_VERSION_NUMBER, scan.versionNumber)
|
||||
put(SCANS_VERSION, scan.version)
|
||||
put(SCANS_SEQUENCE_SIZE, scan.sequenceSize)
|
||||
put(SCANS_SEQUENCE_INDEX, scan.sequenceIndex)
|
||||
put(SCANS_SEQUENCE_ID, scan.sequenceId)
|
||||
@@ -139,7 +161,7 @@ class Database {
|
||||
if (prefs.ignoreConsecutiveDuplicates) {
|
||||
val id = getIdOfLastScan(
|
||||
get(SCANS_CONTENT) as String,
|
||||
if (isRaw) get(SCANS_RAW) as ByteArray else null,
|
||||
get(SCANS_RAW) as ByteArray?,
|
||||
scan.format
|
||||
)
|
||||
if (id > 0L) {
|
||||
@@ -152,7 +174,7 @@ class Database {
|
||||
private fun getIdOfLastScan(
|
||||
content: String,
|
||||
raw: ByteArray?,
|
||||
format: String
|
||||
format: BarcodeFormat
|
||||
): Long = db.rawQuery(
|
||||
"""SELECT
|
||||
$SCANS_ID,
|
||||
@@ -169,7 +191,7 @@ class Database {
|
||||
it.getString(SCANS_CONTENT) == content &&
|
||||
(raw == null || it.getBlob(SCANS_RAW)
|
||||
?.contentEquals(raw) == true) &&
|
||||
it.getString(SCANS_FORMAT) == format
|
||||
it.getString(SCANS_FORMAT) == format.name
|
||||
) {
|
||||
it.getLong(SCANS_ID)
|
||||
} else {
|
||||
@@ -192,9 +214,9 @@ class Database {
|
||||
}
|
||||
|
||||
private class OpenHelper(context: Context) :
|
||||
SQLiteOpenHelper(context, FILE_NAME, null, 5) {
|
||||
SQLiteOpenHelper(context, FILE_NAME, null, 6) {
|
||||
override fun onCreate(db: SQLiteDatabase) {
|
||||
createScans(db)
|
||||
db.createScans()
|
||||
}
|
||||
|
||||
override fun onUpgrade(
|
||||
@@ -203,16 +225,19 @@ class Database {
|
||||
newVersion: Int
|
||||
) {
|
||||
if (oldVersion < 2) {
|
||||
addRawColumn(db)
|
||||
db.addRawColumn()
|
||||
}
|
||||
if (oldVersion < 3) {
|
||||
addMetaDataColumns(db)
|
||||
db.addMetaDataColumns()
|
||||
}
|
||||
if (oldVersion < 4) {
|
||||
addNameColumn(db)
|
||||
db.addNameColumn()
|
||||
}
|
||||
if (oldVersion < 5) {
|
||||
migrateToZxingCpp(db)
|
||||
db.migrateToZxingCpp()
|
||||
}
|
||||
if (oldVersion < 6) {
|
||||
db.migrateToVersionString()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -228,6 +253,7 @@ class Database {
|
||||
const val SCANS_FORMAT = "format"
|
||||
const val SCANS_ERROR_CORRECTION_LEVEL = "error_correction_level"
|
||||
const val SCANS_VERSION_NUMBER = "version_number"
|
||||
const val SCANS_VERSION = "version"
|
||||
const val SCANS_ISSUE_NUMBER = "issue_number"
|
||||
const val SCANS_ORIENTATION = "orientation"
|
||||
const val SCANS_OTHER_META_DATA = "other_meta_data"
|
||||
@@ -243,9 +269,9 @@ class Database {
|
||||
const val SCANS_GTIN_PRICE = "gtin_price"
|
||||
const val SCANS_GTIN_ISSUE_NUMBER = "gtin_issue_number"
|
||||
|
||||
private fun createScans(db: SQLiteDatabase) {
|
||||
db.execSQL("DROP TABLE IF EXISTS $SCANS".trimMargin())
|
||||
db.execSQL(
|
||||
private fun SQLiteDatabase.createScans() {
|
||||
execSQL("DROP TABLE IF EXISTS $SCANS".trimMargin())
|
||||
execSQL(
|
||||
"""CREATE TABLE $SCANS (
|
||||
$SCANS_ID INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
$SCANS_DATETIME DATETIME NOT NULL,
|
||||
@@ -254,7 +280,7 @@ class Database {
|
||||
$SCANS_RAW BLOB,
|
||||
$SCANS_FORMAT TEXT NOT NULL,
|
||||
$SCANS_ERROR_CORRECTION_LEVEL TEXT,
|
||||
$SCANS_VERSION_NUMBER INTEGER,
|
||||
$SCANS_VERSION TEXT,
|
||||
$SCANS_SEQUENCE_SIZE INTEGER,
|
||||
$SCANS_SEQUENCE_INDEX INTEGER,
|
||||
$SCANS_SEQUENCE_ID TEXT,
|
||||
@@ -266,44 +292,45 @@ class Database {
|
||||
)
|
||||
}
|
||||
|
||||
private fun addRawColumn(db: SQLiteDatabase) {
|
||||
db.execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_RAW BLOB".trimMargin())
|
||||
private fun SQLiteDatabase.addRawColumn() {
|
||||
execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_RAW BLOB".trimMargin())
|
||||
}
|
||||
|
||||
private fun addMetaDataColumns(db: SQLiteDatabase) {
|
||||
db.apply {
|
||||
execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_ERROR_CORRECTION_LEVEL TEXT".trimMargin())
|
||||
execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_ISSUE_NUMBER INT".trimMargin())
|
||||
execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_ORIENTATION INT".trimMargin())
|
||||
execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_OTHER_META_DATA TEXT".trimMargin())
|
||||
execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_PDF417_EXTRA_METADATA TEXT".trimMargin())
|
||||
execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_POSSIBLE_COUNTRY TEXT".trimMargin())
|
||||
execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_SUGGESTED_PRICE TEXT".trimMargin())
|
||||
execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_UPC_EAN_EXTENSION TEXT".trimMargin())
|
||||
}
|
||||
private fun SQLiteDatabase.addMetaDataColumns() {
|
||||
execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_ERROR_CORRECTION_LEVEL TEXT".trimMargin())
|
||||
execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_ISSUE_NUMBER INT".trimMargin())
|
||||
execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_ORIENTATION INT".trimMargin())
|
||||
execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_OTHER_META_DATA TEXT".trimMargin())
|
||||
execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_PDF417_EXTRA_METADATA TEXT".trimMargin())
|
||||
execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_POSSIBLE_COUNTRY TEXT".trimMargin())
|
||||
execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_SUGGESTED_PRICE TEXT".trimMargin())
|
||||
execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_UPC_EAN_EXTENSION TEXT".trimMargin())
|
||||
}
|
||||
|
||||
private fun addNameColumn(db: SQLiteDatabase) {
|
||||
db.execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_NAME TEXT".trimMargin())
|
||||
private fun SQLiteDatabase.addNameColumn() {
|
||||
execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_NAME TEXT".trimMargin())
|
||||
}
|
||||
|
||||
private fun migrateToZxingCpp(db: SQLiteDatabase) {
|
||||
db.apply {
|
||||
execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_VERSION_NUMBER INTEGER".trimMargin())
|
||||
execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_SEQUENCE_SIZE INTEGER".trimMargin())
|
||||
execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_SEQUENCE_INDEX INTEGER".trimMargin())
|
||||
execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_SEQUENCE_ID TEXT".trimMargin())
|
||||
execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_GTIN_COUNTRY TEXT".trimMargin())
|
||||
execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_GTIN_ADD_ON TEXT".trimMargin())
|
||||
execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_GTIN_PRICE TEXT".trimMargin())
|
||||
execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_GTIN_ISSUE_NUMBER TEXT".trimMargin())
|
||||
execSQL("UPDATE $SCANS SET $SCANS_SEQUENCE_SIZE = -1")
|
||||
execSQL("UPDATE $SCANS SET $SCANS_SEQUENCE_INDEX = -1")
|
||||
execSQL("UPDATE $SCANS SET $SCANS_GTIN_COUNTRY = $SCANS_POSSIBLE_COUNTRY")
|
||||
execSQL("UPDATE $SCANS SET $SCANS_GTIN_ADD_ON = $SCANS_UPC_EAN_EXTENSION")
|
||||
execSQL("UPDATE $SCANS SET $SCANS_GTIN_PRICE = $SCANS_SUGGESTED_PRICE")
|
||||
execSQL("UPDATE $SCANS SET $SCANS_GTIN_ISSUE_NUMBER = $SCANS_ISSUE_NUMBER")
|
||||
}
|
||||
private fun SQLiteDatabase.migrateToZxingCpp() {
|
||||
execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_VERSION_NUMBER INTEGER".trimMargin())
|
||||
execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_SEQUENCE_SIZE INTEGER".trimMargin())
|
||||
execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_SEQUENCE_INDEX INTEGER".trimMargin())
|
||||
execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_SEQUENCE_ID TEXT".trimMargin())
|
||||
execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_GTIN_COUNTRY TEXT".trimMargin())
|
||||
execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_GTIN_ADD_ON TEXT".trimMargin())
|
||||
execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_GTIN_PRICE TEXT".trimMargin())
|
||||
execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_GTIN_ISSUE_NUMBER TEXT".trimMargin())
|
||||
execSQL("UPDATE $SCANS SET $SCANS_SEQUENCE_SIZE = -1")
|
||||
execSQL("UPDATE $SCANS SET $SCANS_SEQUENCE_INDEX = -1")
|
||||
execSQL("UPDATE $SCANS SET $SCANS_GTIN_COUNTRY = $SCANS_POSSIBLE_COUNTRY")
|
||||
execSQL("UPDATE $SCANS SET $SCANS_GTIN_ADD_ON = $SCANS_UPC_EAN_EXTENSION")
|
||||
execSQL("UPDATE $SCANS SET $SCANS_GTIN_PRICE = $SCANS_SUGGESTED_PRICE")
|
||||
execSQL("UPDATE $SCANS SET $SCANS_GTIN_ISSUE_NUMBER = $SCANS_ISSUE_NUMBER")
|
||||
}
|
||||
|
||||
private fun SQLiteDatabase.migrateToVersionString() {
|
||||
execSQL("ALTER TABLE $SCANS ADD $SCANS_VERSION TEXT")
|
||||
execSQL("UPDATE $SCANS SET $SCANS_VERSION = $SCANS_VERSION_NUMBER")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,9 +24,10 @@ fun Cursor.exportJson(): String? {
|
||||
val columns = arrayOf(
|
||||
Database.SCANS_DATETIME,
|
||||
Database.SCANS_FORMAT,
|
||||
Database.SCANS_NAME,
|
||||
Database.SCANS_CONTENT,
|
||||
Database.SCANS_ERROR_CORRECTION_LEVEL,
|
||||
Database.SCANS_VERSION_NUMBER,
|
||||
Database.SCANS_VERSION,
|
||||
Database.SCANS_SEQUENCE_SIZE,
|
||||
Database.SCANS_SEQUENCE_INDEX,
|
||||
Database.SCANS_SEQUENCE_ID,
|
||||
|
||||
@@ -6,16 +6,16 @@ import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
import android.text.format.DateFormat
|
||||
import de.markusfisch.android.zxingcpp.ZxingCpp
|
||||
import de.markusfisch.android.zxingcpp.ZxingCpp.BarcodeFormat
|
||||
import de.markusfisch.android.zxingcpp.ZxingCpp.ContentType
|
||||
import de.markusfisch.android.zxingcpp.ZxingCpp.Format
|
||||
import de.markusfisch.android.zxingcpp.ZxingCpp.Result
|
||||
|
||||
data class Scan(
|
||||
val content: String,
|
||||
val raw: ByteArray?,
|
||||
val format: String,
|
||||
val format: BarcodeFormat,
|
||||
val errorCorrectionLevel: String? = null,
|
||||
val versionNumber: Int = 0,
|
||||
val version: String? = null,
|
||||
val sequenceSize: Int = -1,
|
||||
val sequenceIndex: Int = -1,
|
||||
val sequenceId: String = "",
|
||||
@@ -24,7 +24,8 @@ data class Scan(
|
||||
val price: String? = null,
|
||||
val issueNumber: String? = null,
|
||||
val dateTime: String = getDateTime(),
|
||||
var id: Long = 0L
|
||||
var id: Long = 0L,
|
||||
val label: String? = null
|
||||
) : Parcelable {
|
||||
// Needs to be overwritten manually, as ByteArray is an array and
|
||||
// this isn't handled well by Kotlin.
|
||||
@@ -45,7 +46,7 @@ data class Scan(
|
||||
(raw != null && other.raw != null && raw.contentEquals(other.raw))) &&
|
||||
format == other.format &&
|
||||
errorCorrectionLevel == other.errorCorrectionLevel &&
|
||||
versionNumber == other.versionNumber &&
|
||||
version == other.version &&
|
||||
sequenceSize == other.sequenceSize &&
|
||||
sequenceIndex == other.sequenceIndex &&
|
||||
sequenceId == other.sequenceId &&
|
||||
@@ -64,7 +65,7 @@ data class Scan(
|
||||
result = 31 * result + (raw?.contentHashCode() ?: 0)
|
||||
result = 31 * result + format.hashCode()
|
||||
result = 31 * result + (errorCorrectionLevel?.hashCode() ?: 0)
|
||||
result = 31 * result + versionNumber
|
||||
result = 31 * result + version.hashCode()
|
||||
result = 31 * result + sequenceSize
|
||||
result = 31 * result + sequenceIndex
|
||||
result = 31 * result + sequenceId.hashCode()
|
||||
@@ -78,9 +79,9 @@ data class Scan(
|
||||
private constructor(parcel: Parcel) : this(
|
||||
content = parcel.readString() ?: "",
|
||||
raw = parcel.readSizedByteArray(),
|
||||
format = parcel.readString() ?: "",
|
||||
format = BarcodeFormat.valueOf(parcel.readString() ?: ""),
|
||||
errorCorrectionLevel = parcel.readString(),
|
||||
versionNumber = parcel.readInt(),
|
||||
version = parcel.readString(),
|
||||
sequenceSize = parcel.readInt(),
|
||||
sequenceIndex = parcel.readInt(),
|
||||
sequenceId = parcel.readString() ?: "",
|
||||
@@ -89,16 +90,17 @@ data class Scan(
|
||||
price = parcel.readString(),
|
||||
issueNumber = parcel.readString(),
|
||||
dateTime = parcel.readString() ?: "",
|
||||
id = parcel.readLong()
|
||||
id = parcel.readLong(),
|
||||
label = parcel.readString()
|
||||
)
|
||||
|
||||
override fun writeToParcel(parcel: Parcel, flags: Int) {
|
||||
parcel.apply {
|
||||
writeString(content)
|
||||
writeSizedByteArray(raw)
|
||||
writeString(format)
|
||||
writeString(format.name)
|
||||
writeString(errorCorrectionLevel)
|
||||
writeInt(versionNumber)
|
||||
writeString(version)
|
||||
writeInt(sequenceSize)
|
||||
writeInt(sequenceIndex)
|
||||
writeString(sequenceId)
|
||||
@@ -108,6 +110,7 @@ data class Scan(
|
||||
writeString(issueNumber)
|
||||
writeString(dateTime)
|
||||
writeLong(id)
|
||||
writeString(label)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,26 +126,18 @@ data class Scan(
|
||||
}
|
||||
|
||||
fun Result.toScan(): Scan {
|
||||
val content: String
|
||||
val raw: ByteArray?
|
||||
when (contentType) {
|
||||
ContentType.GS1,
|
||||
ContentType.ISO15434,
|
||||
ContentType.TEXT -> {
|
||||
content = text
|
||||
raw = null
|
||||
}
|
||||
else -> {
|
||||
content = ""
|
||||
raw = rawBytes
|
||||
}
|
||||
}
|
||||
return Scan(
|
||||
content,
|
||||
raw,
|
||||
when (contentType) {
|
||||
ContentType.GS1,
|
||||
ContentType.ISO15434,
|
||||
ContentType.TEXT -> text
|
||||
|
||||
else -> ""
|
||||
},
|
||||
rawBytes,
|
||||
format,
|
||||
ecLevel,
|
||||
versionNumber,
|
||||
version,
|
||||
sequenceSize,
|
||||
sequenceIndex,
|
||||
sequenceId,
|
||||
@@ -154,12 +149,12 @@ fun Result.toScan(): Scan {
|
||||
}
|
||||
|
||||
data class Recreation(
|
||||
val format: Format,
|
||||
val format: BarcodeFormat,
|
||||
val ecLevel: Int,
|
||||
val size: Int,
|
||||
val margin: Int
|
||||
) {
|
||||
fun encode(content: String): Bitmap? = try {
|
||||
fun <T> encode(content: T): Bitmap? = try {
|
||||
ZxingCpp.encodeAsBitmap(
|
||||
content, format, size, size, margin, ecLevel
|
||||
)
|
||||
@@ -171,22 +166,18 @@ data class Recreation(
|
||||
fun Scan.toRecreation(
|
||||
size: Int = 128,
|
||||
margin: Int = -1
|
||||
) = if (content.isEmpty()) {
|
||||
null
|
||||
} else {
|
||||
Recreation(
|
||||
Format.valueOf(format),
|
||||
when (errorCorrectionLevel) {
|
||||
"L" -> 0
|
||||
"M" -> 1
|
||||
"Q" -> 2
|
||||
"H" -> 3
|
||||
else -> -1
|
||||
},
|
||||
size,
|
||||
margin
|
||||
)
|
||||
}
|
||||
) = Recreation(
|
||||
format,
|
||||
when (errorCorrectionLevel) {
|
||||
"L" -> 0
|
||||
"M" -> 4
|
||||
"Q" -> 6
|
||||
"H" -> 8
|
||||
else -> -1
|
||||
},
|
||||
size,
|
||||
margin
|
||||
)
|
||||
|
||||
private fun getDateTime(
|
||||
time: Long = System.currentTimeMillis()
|
||||
|
||||
@@ -10,10 +10,14 @@ import android.support.v4.app.Fragment
|
||||
import android.view.*
|
||||
import android.widget.EditText
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.app.db
|
||||
import de.markusfisch.android.binaryeye.app.hasWritePermission
|
||||
import de.markusfisch.android.binaryeye.content.copyToClipboard
|
||||
import de.markusfisch.android.binaryeye.content.shareFile
|
||||
import de.markusfisch.android.binaryeye.content.shareText
|
||||
import de.markusfisch.android.binaryeye.database.toScan
|
||||
import de.markusfisch.android.binaryeye.graphics.COLOR_BLACK
|
||||
import de.markusfisch.android.binaryeye.graphics.COLOR_WHITE
|
||||
import de.markusfisch.android.binaryeye.io.addSuffixIfNotGiven
|
||||
import de.markusfisch.android.binaryeye.io.toSaveResult
|
||||
import de.markusfisch.android.binaryeye.io.writeExternalFile
|
||||
@@ -22,13 +26,14 @@ import de.markusfisch.android.binaryeye.view.setPaddingFromWindowInsets
|
||||
import de.markusfisch.android.binaryeye.widget.ConfinedScalingImageView
|
||||
import de.markusfisch.android.binaryeye.widget.toast
|
||||
import de.markusfisch.android.zxingcpp.ZxingCpp
|
||||
import de.markusfisch.android.zxingcpp.ZxingCpp.Format
|
||||
import de.markusfisch.android.zxingcpp.ZxingCpp.BarcodeFormat
|
||||
import kotlinx.coroutines.*
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
import java.io.OutputStream
|
||||
import java.util.*
|
||||
import kotlin.math.min
|
||||
|
||||
class BarcodeFragment : Fragment() {
|
||||
private enum class FileType {
|
||||
@@ -38,7 +43,8 @@ class BarcodeFragment : Fragment() {
|
||||
private val parentJob = Job()
|
||||
private val scope = CoroutineScope(Dispatchers.IO + parentJob)
|
||||
|
||||
private lateinit var barcode: Barcode
|
||||
private lateinit var barcode: Barcode<*>
|
||||
private lateinit var addToHistoryItem: MenuItem
|
||||
|
||||
override fun onCreate(state: Bundle?) {
|
||||
super.onCreate(state)
|
||||
@@ -61,14 +67,14 @@ class BarcodeFragment : Fragment() {
|
||||
|
||||
val bitmap: Bitmap
|
||||
try {
|
||||
barcode = arguments?.toProperties() ?: throw IllegalArgumentException(
|
||||
barcode = arguments?.toBarcode() ?: throw IllegalArgumentException(
|
||||
"Illegal arguments"
|
||||
)
|
||||
// Catch exceptions from encoding.
|
||||
bitmap = barcode.bitmap
|
||||
bitmap = barcode.bitmap()
|
||||
} catch (e: Exception) {
|
||||
var message = e.message
|
||||
if (message == null || message.isEmpty()) {
|
||||
if (message.isNullOrEmpty()) {
|
||||
message = getString(R.string.error_encoding_barcode)
|
||||
}
|
||||
message?.let {
|
||||
@@ -84,7 +90,10 @@ class BarcodeFragment : Fragment() {
|
||||
imageView.setImageBitmap(bitmap)
|
||||
imageView.post {
|
||||
// Make sure to invoke this after ScalingImageView.onLayout().
|
||||
imageView.minWidth /= 2f
|
||||
imageView.minWidth = min(
|
||||
imageView.minWidth / 2f,
|
||||
barcode.size.toFloat()
|
||||
)
|
||||
}
|
||||
|
||||
view.findViewById<View>(R.id.share).setOnClickListener {
|
||||
@@ -103,17 +112,19 @@ class BarcodeFragment : Fragment() {
|
||||
return view
|
||||
}
|
||||
|
||||
private fun Bundle.toProperties() = Barcode(
|
||||
getString(CONTENT) ?: throw IllegalArgumentException(
|
||||
private fun Bundle.toBarcode() = Barcode(
|
||||
getString(CONTENT_TEXT) ?: getByteArray(CONTENT_RAW) ?: throw IllegalArgumentException(
|
||||
"content cannot be null"
|
||||
),
|
||||
Format.valueOf(
|
||||
BarcodeFormat.valueOf(
|
||||
getString(FORMAT) ?: throw IllegalArgumentException(
|
||||
"format cannot be null"
|
||||
)
|
||||
),
|
||||
getInt(SIZE),
|
||||
getInt(EC_LEVEL)
|
||||
getInt(MARGIN),
|
||||
getInt(EC_LEVEL),
|
||||
Colors.entries[getInt(COLORS)]
|
||||
)
|
||||
|
||||
override fun onDestroyView() {
|
||||
@@ -123,23 +134,33 @@ class BarcodeFragment : Fragment() {
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
inflater.inflate(R.menu.fragment_barcode, menu)
|
||||
addToHistoryItem = menu.findItem(R.id.add_to_history)
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
return when (item.itemId) {
|
||||
R.id.add_to_history -> {
|
||||
readAndAddToHistory(barcode.bitmap(), barcode.format)
|
||||
addToHistoryItem.isVisible = false
|
||||
context.toast(R.string.added_to_history)
|
||||
true
|
||||
}
|
||||
|
||||
R.id.copy_to_clipboard -> {
|
||||
context.apply {
|
||||
copyToClipboard(barcode.text)
|
||||
copyToClipboard(barcode.text())
|
||||
toast(R.string.copied_to_clipboard)
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
R.id.export_to_file -> {
|
||||
context.pickFileType(R.string.export_as) {
|
||||
askForFileNameAndSave(it)
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
}
|
||||
@@ -148,7 +169,7 @@ class BarcodeFragment : Fragment() {
|
||||
title: Int,
|
||||
action: (FileType) -> Unit
|
||||
) {
|
||||
val fileTypes = FileType.values()
|
||||
val fileTypes = FileType.entries.toTypedArray()
|
||||
AlertDialog.Builder(this)
|
||||
.setTitle(title)
|
||||
.setItems(fileTypes.map { it.name }.toTypedArray()) { _, which ->
|
||||
@@ -181,19 +202,21 @@ class BarcodeFragment : Fragment() {
|
||||
addSuffixIfNotGiven(fileName, ".png"),
|
||||
MIME_PNG
|
||||
) {
|
||||
barcode.bitmap.saveAsPng(it)
|
||||
barcode.bitmap().saveAsPng(it)
|
||||
}
|
||||
|
||||
FileType.SVG -> saveAs(
|
||||
addSuffixIfNotGiven(fileName, ".svg"),
|
||||
MIME_SVG
|
||||
) { outputStream ->
|
||||
outputStream.write(barcode.svg.toByteArray())
|
||||
outputStream.write(barcode.svg().toByteArray())
|
||||
}
|
||||
|
||||
FileType.TXT -> saveAs(
|
||||
addSuffixIfNotGiven(fileName, ".txt"),
|
||||
MIME_TXT
|
||||
) { outputStream ->
|
||||
outputStream.write(barcode.text.toByteArray())
|
||||
outputStream.write(barcode.text().toByteArray())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -218,9 +241,9 @@ class BarcodeFragment : Fragment() {
|
||||
|
||||
private fun Context.shareAs(fileType: FileType) {
|
||||
when (fileType) {
|
||||
FileType.PNG -> share(barcode.bitmap)
|
||||
FileType.SVG -> shareText(barcode.svg, MIME_SVG)
|
||||
FileType.TXT -> shareText(barcode.text)
|
||||
FileType.PNG -> share(barcode.bitmap())
|
||||
FileType.SVG -> shareText(barcode.svg(), MIME_SVG)
|
||||
FileType.TXT -> shareText(barcode.text())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -249,25 +272,46 @@ class BarcodeFragment : Fragment() {
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val CONTENT = "content"
|
||||
private const val CONTENT_TEXT = "content_text"
|
||||
private const val CONTENT_RAW = "content_raw"
|
||||
private const val FORMAT = "format"
|
||||
private const val SIZE = "size"
|
||||
private const val MARGIN = "margin"
|
||||
private const val EC_LEVEL = "ec_level"
|
||||
private const val COLORS = "colors"
|
||||
private const val MIME_PNG = "image/png"
|
||||
private const val MIME_SVG = "image/svg+xmg"
|
||||
private const val MIME_TXT = "text/plain"
|
||||
|
||||
fun newInstance(
|
||||
content: String,
|
||||
format: Format,
|
||||
fun <T> newInstance(
|
||||
content: T,
|
||||
format: BarcodeFormat,
|
||||
size: Int,
|
||||
ecLevel: Int = -1
|
||||
margin: Int,
|
||||
ecLevel: Int = -1,
|
||||
colors: Int = 0
|
||||
): Fragment {
|
||||
val args = Bundle()
|
||||
args.putString(CONTENT, content)
|
||||
when (content) {
|
||||
is String -> {
|
||||
args.putString(CONTENT_TEXT, content)
|
||||
}
|
||||
|
||||
is ByteArray -> {
|
||||
args.putByteArray(CONTENT_RAW, content)
|
||||
}
|
||||
|
||||
else -> {
|
||||
throw IllegalArgumentException(
|
||||
"content must be a String of a ByteArray"
|
||||
)
|
||||
}
|
||||
}
|
||||
args.putString(FORMAT, format.name)
|
||||
args.putInt(SIZE, size)
|
||||
args.putInt(MARGIN, margin)
|
||||
args.putInt(EC_LEVEL, ecLevel)
|
||||
args.putInt(COLORS, colors)
|
||||
val fragment = BarcodeFragment()
|
||||
fragment.arguments = args
|
||||
return fragment
|
||||
@@ -275,29 +319,96 @@ class BarcodeFragment : Fragment() {
|
||||
}
|
||||
}
|
||||
|
||||
private data class Barcode(
|
||||
val content: String,
|
||||
val format: Format,
|
||||
private data class Barcode<T>(
|
||||
val content: T,
|
||||
val format: BarcodeFormat,
|
||||
val size: Int,
|
||||
val ecLevel: Int
|
||||
val margin: Int,
|
||||
val ecLevel: Int,
|
||||
val colors: Colors
|
||||
) {
|
||||
val bitmap: Bitmap
|
||||
get() = ZxingCpp.encodeAsBitmap(
|
||||
content, format, size, size, -1, ecLevel
|
||||
private var _bitmap: Bitmap? = null
|
||||
fun bitmap(): Bitmap {
|
||||
val b = _bitmap ?: ZxingCpp.encodeAsBitmap(
|
||||
content, format, size, size, margin, ecLevel,
|
||||
setColor = colors.foregroundColor(),
|
||||
unsetColor = colors.backgroundColor()
|
||||
)
|
||||
val svg: String
|
||||
get() = ZxingCpp.encodeAsSvg(
|
||||
content, format, -1, ecLevel
|
||||
_bitmap = b
|
||||
return b
|
||||
}
|
||||
|
||||
private var _svg: String? = null
|
||||
fun svg(): String {
|
||||
val s = _svg ?: ZxingCpp.encodeAsSvg(
|
||||
content, format, margin, ecLevel
|
||||
)
|
||||
val text: String
|
||||
get() = ZxingCpp.encodeAsText(
|
||||
content, format, -1, ecLevel
|
||||
_svg = s
|
||||
return s
|
||||
}
|
||||
|
||||
private var _text: String? = null
|
||||
fun text(): String {
|
||||
val t = _text ?: ZxingCpp.encodeAsText(
|
||||
content, format, margin, ecLevel,
|
||||
inverted = colors == Colors.BLACK_ON_WHITE
|
||||
)
|
||||
_text = t
|
||||
return t
|
||||
}
|
||||
}
|
||||
|
||||
private enum class Colors {
|
||||
BLACK_ON_WHITE,
|
||||
WHITE_ON_BLACK,
|
||||
BLACK_ON_TRANSPARENT,
|
||||
WHITE_ON_TRANSPARENT;
|
||||
|
||||
fun foregroundColor(): Int = when (this) {
|
||||
BLACK_ON_WHITE,
|
||||
BLACK_ON_TRANSPARENT -> COLOR_BLACK
|
||||
|
||||
WHITE_ON_BLACK,
|
||||
WHITE_ON_TRANSPARENT -> COLOR_WHITE
|
||||
}
|
||||
|
||||
fun backgroundColor(): Int = when (this) {
|
||||
BLACK_ON_WHITE -> COLOR_WHITE
|
||||
WHITE_ON_BLACK -> COLOR_BLACK
|
||||
BLACK_ON_TRANSPARENT,
|
||||
WHITE_ON_TRANSPARENT -> 0
|
||||
}
|
||||
}
|
||||
|
||||
private fun Bitmap.saveAsPng(outputStream: OutputStream, quality: Int = 90) =
|
||||
compress(Bitmap.CompressFormat.PNG, quality, outputStream)
|
||||
|
||||
private val fileNameCharacters = "[^A-Za-z0-9]".toRegex()
|
||||
private fun encodeFileName(name: String): String =
|
||||
fileNameCharacters.replace(name, "_").take(16).trim('_').lowercase(Locale.getDefault())
|
||||
private fun encodeFileName(name: String): String = fileNameCharacters
|
||||
.replace(name, "_")
|
||||
.take(16)
|
||||
.trim('_')
|
||||
.lowercase(Locale.getDefault())
|
||||
|
||||
private fun readAndAddToHistory(
|
||||
bitmap: Bitmap,
|
||||
format: BarcodeFormat
|
||||
) {
|
||||
ZxingCpp.readBitmap(
|
||||
bitmap,
|
||||
0, 0,
|
||||
bitmap.width, bitmap.height,
|
||||
0,
|
||||
ZxingCpp.ReaderOptions(
|
||||
tryHarder = true,
|
||||
tryRotate = true,
|
||||
tryInvert = true,
|
||||
tryDownscale = true,
|
||||
maxNumberOfSymbols = 1,
|
||||
formats = setOf(format)
|
||||
)
|
||||
)?.first {
|
||||
db.insertScan(it.toScan())
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,8 +8,17 @@ import android.text.Editable
|
||||
import android.text.Html
|
||||
import android.text.TextWatcher
|
||||
import android.text.method.LinkMovementMethod
|
||||
import android.view.*
|
||||
import android.widget.*
|
||||
import android.view.LayoutInflater
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.EditText
|
||||
import android.widget.ImageView
|
||||
import android.widget.TableLayout
|
||||
import android.widget.TableRow
|
||||
import android.widget.TextView
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.actions.ActionRegistry
|
||||
import de.markusfisch.android.binaryeye.actions.IAction
|
||||
@@ -17,10 +26,16 @@ import de.markusfisch.android.binaryeye.actions.wifi.WifiAction
|
||||
import de.markusfisch.android.binaryeye.actions.wifi.WifiConnector
|
||||
import de.markusfisch.android.binaryeye.activity.MainActivity
|
||||
import de.markusfisch.android.binaryeye.adapter.prettifyFormatName
|
||||
import de.markusfisch.android.binaryeye.app.*
|
||||
import de.markusfisch.android.binaryeye.app.addFragment
|
||||
import de.markusfisch.android.binaryeye.app.db
|
||||
import de.markusfisch.android.binaryeye.app.hasLocationPermission
|
||||
import de.markusfisch.android.binaryeye.app.hasWritePermission
|
||||
import de.markusfisch.android.binaryeye.app.prefs
|
||||
import de.markusfisch.android.binaryeye.content.copyToClipboard
|
||||
import de.markusfisch.android.binaryeye.content.shareAsFile
|
||||
import de.markusfisch.android.binaryeye.content.shareText
|
||||
import de.markusfisch.android.binaryeye.content.toHexString
|
||||
import de.markusfisch.android.binaryeye.content.wipeLastShareFile
|
||||
import de.markusfisch.android.binaryeye.database.Recreation
|
||||
import de.markusfisch.android.binaryeye.database.Scan
|
||||
import de.markusfisch.android.binaryeye.database.toRecreation
|
||||
@@ -29,7 +44,11 @@ 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.*
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
class DecodeFragment : Fragment() {
|
||||
@@ -40,6 +59,7 @@ class DecodeFragment : Fragment() {
|
||||
private lateinit var hexView: TextView
|
||||
private lateinit var stampView: TextView
|
||||
private lateinit var recreationView: ImageView
|
||||
private lateinit var labelView: EditText
|
||||
private lateinit var fab: FloatingActionButton
|
||||
private lateinit var format: String
|
||||
|
||||
@@ -56,6 +76,7 @@ class DecodeFragment : Fragment() {
|
||||
private var originalBytes: ByteArray = ByteArray(0)
|
||||
private var id = 0L
|
||||
private var recreation: Recreation? = null
|
||||
private var label: String? = null
|
||||
|
||||
override fun onCreate(state: Bundle?) {
|
||||
super.onCreate(state)
|
||||
@@ -86,9 +107,9 @@ class DecodeFragment : Fragment() {
|
||||
} ?: throw IllegalArgumentException("DecodeFragment needs a Scan")
|
||||
|
||||
val originalContent = scan.content
|
||||
isBinary = scan.raw != null
|
||||
isBinary = scan.content.isEmpty()
|
||||
originalBytes = scan.raw ?: originalContent.toByteArray()
|
||||
format = scan.format
|
||||
format = scan.format.name
|
||||
id = scan.id
|
||||
|
||||
contentView = view.findViewById(R.id.content)
|
||||
@@ -98,6 +119,7 @@ class DecodeFragment : Fragment() {
|
||||
hexView = view.findViewById(R.id.hex)
|
||||
stampView = view.findViewById(R.id.stamp)
|
||||
recreationView = view.findViewById(R.id.recreation)
|
||||
labelView = view.findViewById(R.id.label)
|
||||
fab = view.findViewById(R.id.open)
|
||||
|
||||
initContentAndFab(originalContent)
|
||||
@@ -112,6 +134,14 @@ class DecodeFragment : Fragment() {
|
||||
(200f * dp).roundToInt()
|
||||
)
|
||||
}
|
||||
if (id > 0) {
|
||||
scan.label?.let {
|
||||
labelView.setText(it)
|
||||
label = it
|
||||
}
|
||||
} else {
|
||||
labelView.visibility = View.GONE
|
||||
}
|
||||
|
||||
updateViewsAndFab(originalContent, originalBytes)
|
||||
|
||||
@@ -121,9 +151,21 @@ class DecodeFragment : Fragment() {
|
||||
return view
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
if (id > 0) {
|
||||
val newLabel = labelView.text.toString().trim()
|
||||
if (newLabel != (label ?: "")) {
|
||||
db.renameScan(id, newLabel)
|
||||
label = newLabel
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
parentJob.cancel()
|
||||
wipeLastShareFile()
|
||||
}
|
||||
|
||||
private fun initContentAndFab(originalContent: String) {
|
||||
@@ -204,13 +246,15 @@ class DecodeFragment : Fragment() {
|
||||
}
|
||||
recreationView.showIf(prefs.showRecreation) { v ->
|
||||
val r = recreation ?: return@showIf
|
||||
v.setImageBitmap(r.encode(text))
|
||||
val content = if (isBinary) bytes else text
|
||||
v.setImageBitmap(r.encode(content))
|
||||
v.setOnClickListener {
|
||||
fragmentManager?.addFragment(
|
||||
BarcodeFragment.newInstance(
|
||||
text,
|
||||
content,
|
||||
r.format,
|
||||
r.size,
|
||||
r.margin,
|
||||
r.ecLevel
|
||||
)
|
||||
)
|
||||
@@ -253,15 +297,15 @@ class DecodeFragment : Fragment() {
|
||||
R.string.gtin_price to scan.price,
|
||||
R.string.gtin_issue_number to scan.issueNumber,
|
||||
)
|
||||
if (scan.versionNumber > 0) {
|
||||
val versionString = if (scan.format == "QR_CODE") {
|
||||
if (!scan.version.isNullOrBlank()) {
|
||||
val versionString = if (scan.format.name == "QR_CODE") {
|
||||
getString(
|
||||
R.string.qr_version_and_modules,
|
||||
scan.versionNumber,
|
||||
scan.versionNumber * 4 + 17
|
||||
scan.version,
|
||||
(scan.version.toIntOrNull() ?: 0) * 4 + 17
|
||||
)
|
||||
} else {
|
||||
scan.versionNumber.toString()
|
||||
scan.version
|
||||
}
|
||||
items.putAll(
|
||||
linkedMapOf(
|
||||
@@ -299,10 +343,6 @@ class DecodeFragment : Fragment() {
|
||||
|
||||
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
|
||||
}
|
||||
@@ -318,11 +358,13 @@ class DecodeFragment : Fragment() {
|
||||
maybeBackOrFinish()
|
||||
true
|
||||
}
|
||||
|
||||
R.id.copy_to_clipboard -> {
|
||||
copyToClipboard(textOrHex())
|
||||
maybeBackOrFinish()
|
||||
true
|
||||
}
|
||||
|
||||
R.id.share -> {
|
||||
context?.apply {
|
||||
shareText(textOrHex())
|
||||
@@ -330,17 +372,36 @@ class DecodeFragment : Fragment() {
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
R.id.share_file -> {
|
||||
context?.apply {
|
||||
val (data, name) = if (isBinary) {
|
||||
Pair(originalBytes, "barcode_content.bin")
|
||||
} else {
|
||||
Pair(content, "barcode_content.txt")
|
||||
}
|
||||
shareAsFile(data, name)
|
||||
maybeBackOrFinish()
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
R.id.create -> {
|
||||
fragmentManager?.addFragment(
|
||||
EncodeFragment.newInstance(content, format)
|
||||
EncodeFragment.newInstance(
|
||||
if (isBinary) originalBytes else content,
|
||||
format
|
||||
)
|
||||
)
|
||||
true
|
||||
}
|
||||
|
||||
R.id.remove -> {
|
||||
db.removeScan(id)
|
||||
backOrFinish()
|
||||
true
|
||||
}
|
||||
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,41 +1,96 @@
|
||||
package de.markusfisch.android.binaryeye.fragment
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.support.v4.app.Fragment
|
||||
import android.view.LayoutInflater
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.*
|
||||
import android.widget.AdapterView
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.CheckBox
|
||||
import android.widget.EditText
|
||||
import android.widget.SeekBar
|
||||
import android.widget.Spinner
|
||||
import android.widget.TextView
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.adapter.prettifyFormatName
|
||||
import de.markusfisch.android.binaryeye.app.addFragment
|
||||
import de.markusfisch.android.binaryeye.app.prefs
|
||||
import de.markusfisch.android.binaryeye.text.unescape
|
||||
import de.markusfisch.android.binaryeye.view.hideSoftKeyboard
|
||||
import de.markusfisch.android.binaryeye.view.setPaddingFromWindowInsets
|
||||
import de.markusfisch.android.zxingcpp.ZxingCpp.Format
|
||||
import de.markusfisch.android.binaryeye.widget.toast
|
||||
import de.markusfisch.android.zxingcpp.ZxingCpp.BarcodeFormat
|
||||
import java.io.InputStream
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
|
||||
class EncodeFragment : Fragment() {
|
||||
private lateinit var formatView: Spinner
|
||||
private lateinit var errorCorrectionLabel: TextView
|
||||
private lateinit var errorCorrectionLevel: Spinner
|
||||
private lateinit var ecLabel: TextView
|
||||
private lateinit var ecSpinner: Spinner
|
||||
private lateinit var colorsLabel: TextView
|
||||
private lateinit var colorsSpinner: Spinner
|
||||
private lateinit var sizeView: TextView
|
||||
private lateinit var sizeBarView: SeekBar
|
||||
private lateinit var marginLayout: View
|
||||
private lateinit var marginView: TextView
|
||||
private lateinit var marginBarView: SeekBar
|
||||
private lateinit var contentView: EditText
|
||||
private lateinit var unescapeCheckBox: CheckBox
|
||||
|
||||
private val writers = arrayListOf(
|
||||
Format.AZTEC,
|
||||
Format.CODABAR,
|
||||
Format.CODE_39,
|
||||
Format.CODE_128,
|
||||
Format.DATA_MATRIX,
|
||||
Format.EAN_8,
|
||||
Format.EAN_13,
|
||||
Format.ITF,
|
||||
Format.PDF_417,
|
||||
Format.QR_CODE,
|
||||
Format.UPC_A
|
||||
private val formats = arrayListOf(
|
||||
BarcodeFormat.AZTEC,
|
||||
BarcodeFormat.CODABAR,
|
||||
BarcodeFormat.CODE_39,
|
||||
BarcodeFormat.CODE_128,
|
||||
BarcodeFormat.DATA_MATRIX,
|
||||
BarcodeFormat.EAN_8,
|
||||
BarcodeFormat.EAN_13,
|
||||
BarcodeFormat.ITF,
|
||||
BarcodeFormat.PDF_417,
|
||||
BarcodeFormat.QR_CODE,
|
||||
BarcodeFormat.UPC_A
|
||||
)
|
||||
|
||||
private var minMargin = 0
|
||||
private var bytes: ByteArray? = null
|
||||
|
||||
override fun onActivityResult(
|
||||
requestCode: Int,
|
||||
resultCode: Int,
|
||||
resultData: Intent?
|
||||
) {
|
||||
when (requestCode) {
|
||||
PICK_FILE_RESULT_CODE -> {
|
||||
if (resultCode == Activity.RESULT_OK &&
|
||||
resultData != null &&
|
||||
resultData.data != null
|
||||
) {
|
||||
val ac = activity ?: return
|
||||
ac.hideSoftKeyboard(contentView)
|
||||
val uri = resultData.data ?: return
|
||||
bytes = ac.contentResolver?.openInputStream(uri)?.use {
|
||||
it.readBytesMax(4096)
|
||||
} ?: return
|
||||
setEncodeByteArray()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate(state: Bundle?) {
|
||||
super.onCreate(state)
|
||||
setHasOptionsMenu(true)
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
@@ -54,7 +109,7 @@ class EncodeFragment : Fragment() {
|
||||
val formatAdapter = ArrayAdapter(
|
||||
ac,
|
||||
android.R.layout.simple_spinner_item,
|
||||
writers.map { prettifyFormatName(it.name) }
|
||||
formats.map { prettifyFormatName(it.name) }
|
||||
)
|
||||
formatAdapter.setDropDownViewResource(
|
||||
android.R.layout.simple_spinner_dropdown_item
|
||||
@@ -67,38 +122,88 @@ class EncodeFragment : Fragment() {
|
||||
position: Int,
|
||||
id: Long
|
||||
) {
|
||||
val visibility = if (
|
||||
writers[position] == Format.QR_CODE
|
||||
) {
|
||||
View.VISIBLE
|
||||
} else {
|
||||
View.GONE
|
||||
val format = formats[position]
|
||||
val arrayId = when (format) {
|
||||
BarcodeFormat.AZTEC -> R.array.aztec_error_correction_levels
|
||||
BarcodeFormat.QR_CODE -> R.array.qr_error_correction_levels
|
||||
BarcodeFormat.PDF_417 -> R.array.pdf417_error_correction_levels
|
||||
else -> 0
|
||||
}
|
||||
errorCorrectionLabel.visibility = visibility
|
||||
errorCorrectionLevel.visibility = visibility
|
||||
if (arrayId > 0) {
|
||||
ecSpinner.setEntries(arrayId)
|
||||
val idx = format.unpackEcLevel(
|
||||
prefs.indexOfLastSelectedEcLevel
|
||||
)
|
||||
if (idx < ecSpinner.adapter.count) {
|
||||
ecSpinner.setSelection(idx)
|
||||
}
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}.setVisibilityFor(ecLabel, ecSpinner)
|
||||
format.canBeInverted().setVisibilityFor(
|
||||
colorsLabel, colorsSpinner
|
||||
)
|
||||
when (format) {
|
||||
BarcodeFormat.AZTEC -> setMarginBar(4, 4)
|
||||
BarcodeFormat.DATA_MATRIX -> setMarginBar(1, 1)
|
||||
BarcodeFormat.QR_CODE -> setMarginBar(4, 4)
|
||||
BarcodeFormat.PDF_417 -> setMarginBar(0, 30)
|
||||
else -> false
|
||||
}.setVisibilityFor(marginLayout)
|
||||
}
|
||||
|
||||
override fun onNothingSelected(parentView: AdapterView<*>?) {}
|
||||
}
|
||||
|
||||
errorCorrectionLabel = view.findViewById(R.id.error_correction_label)
|
||||
errorCorrectionLevel = view.findViewById(R.id.error_correction_level)
|
||||
ecLabel = view.findViewById(R.id.error_correction_label)
|
||||
ecSpinner = view.findViewById(R.id.error_correction_level)
|
||||
ecSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
|
||||
override fun onItemSelected(
|
||||
parentView: AdapterView<*>?,
|
||||
selectedItemView: View?,
|
||||
position: Int,
|
||||
id: Long
|
||||
) {
|
||||
val format = formats[formatView.selectedItemPosition]
|
||||
prefs.indexOfLastSelectedEcLevel = format.packEcLevel(
|
||||
prefs.indexOfLastSelectedEcLevel,
|
||||
position
|
||||
)
|
||||
}
|
||||
|
||||
override fun onNothingSelected(parentView: AdapterView<*>?) {}
|
||||
}
|
||||
|
||||
colorsLabel = view.findViewById(R.id.colors_label)
|
||||
colorsSpinner = view.findViewById(R.id.colors)
|
||||
|
||||
sizeView = view.findViewById(R.id.size_display)
|
||||
sizeBarView = view.findViewById(R.id.size_bar)
|
||||
initSizeBar()
|
||||
|
||||
marginLayout = view.findViewById(R.id.margin)
|
||||
marginView = view.findViewById(R.id.margin_display)
|
||||
marginBarView = view.findViewById(R.id.margin_bar)
|
||||
initMarginBar()
|
||||
|
||||
contentView = view.findViewById(R.id.content)
|
||||
unescapeCheckBox = view.findViewById(R.id.unescape)
|
||||
unescapeCheckBox.isChecked = prefs.expandEscapeSequences
|
||||
|
||||
val args = arguments
|
||||
args?.getString(CONTENT)?.let {
|
||||
args?.getString(CONTENT_TEXT)?.let {
|
||||
contentView.setText(it)
|
||||
}
|
||||
args?.getByteArray(CONTENT_RAW)?.let {
|
||||
bytes = it
|
||||
setEncodeByteArray()
|
||||
}
|
||||
|
||||
val barcodeFormat = args?.getString(FORMAT)
|
||||
if (barcodeFormat != null) {
|
||||
formatView.setSelection(
|
||||
writers.indexOf(barcodeFormat.toFormat())
|
||||
formats.indexOf(barcodeFormat.toFormat())
|
||||
)
|
||||
} else if (state == null) {
|
||||
formatView.post {
|
||||
@@ -107,7 +212,7 @@ class EncodeFragment : Fragment() {
|
||||
}
|
||||
|
||||
view.findViewById<View>(R.id.encode).setOnClickListener {
|
||||
encode()
|
||||
it.context.encode()
|
||||
}
|
||||
|
||||
(view.findViewById(R.id.inset_layout) as View).setPaddingFromWindowInsets()
|
||||
@@ -116,30 +221,76 @@ class EncodeFragment : Fragment() {
|
||||
return view
|
||||
}
|
||||
|
||||
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.pick_file -> {
|
||||
startActivityForResult(
|
||||
Intent.createChooser(
|
||||
Intent(Intent.ACTION_GET_CONTENT).apply {
|
||||
type = "*/*"
|
||||
},
|
||||
getString(R.string.pick_file)
|
||||
),
|
||||
PICK_FILE_RESULT_CODE
|
||||
)
|
||||
true
|
||||
}
|
||||
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
prefs.indexOfLastSelectedFormat = formatView.selectedItemPosition
|
||||
prefs.expandEscapeSequences = unescapeCheckBox.isChecked
|
||||
prefs.lastMargin = marginBarView.progress
|
||||
}
|
||||
|
||||
private fun encode() {
|
||||
val content = contentView.text.toString()
|
||||
if (content.isEmpty()) {
|
||||
Toast.makeText(
|
||||
context,
|
||||
R.string.error_no_content,
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
private fun Context.encode() {
|
||||
hideSoftKeyboard(contentView)
|
||||
val content = if (bytes != null) {
|
||||
bytes
|
||||
} else {
|
||||
activity?.hideSoftKeyboard(contentView)
|
||||
fragmentManager?.addFragment(
|
||||
BarcodeFragment.newInstance(
|
||||
content,
|
||||
writers[formatView.selectedItemPosition],
|
||||
getSize(sizeBarView.progress),
|
||||
errorCorrectionLevel.selectedItemPosition
|
||||
)
|
||||
)
|
||||
var text = contentView.text.toString()
|
||||
if (unescapeCheckBox.isChecked) {
|
||||
try {
|
||||
text = text.unescape()
|
||||
} catch (e: IllegalArgumentException) {
|
||||
toast(e.message ?: "Invalid escape sequence")
|
||||
return
|
||||
}
|
||||
}
|
||||
if (text.isEmpty()) {
|
||||
toast(R.string.error_no_content)
|
||||
return
|
||||
}
|
||||
text
|
||||
}
|
||||
val format = formats[formatView.selectedItemPosition]
|
||||
fragmentManager?.addFragment(
|
||||
BarcodeFragment.newInstance(
|
||||
content,
|
||||
format,
|
||||
getSize(sizeBarView.progress),
|
||||
when (format) {
|
||||
BarcodeFormat.AZTEC,
|
||||
BarcodeFormat.DATA_MATRIX,
|
||||
BarcodeFormat.QR_CODE,
|
||||
BarcodeFormat.PDF_417 -> marginBarView.progress
|
||||
|
||||
else -> -1
|
||||
},
|
||||
format.getErrorCorrectionLevel(ecSpinner.selectedItemPosition),
|
||||
if (format.canBeInverted()) {
|
||||
colorsSpinner.selectedItemPosition
|
||||
} else 0
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun initSizeBar() {
|
||||
@@ -162,21 +313,77 @@ class EncodeFragment : Fragment() {
|
||||
|
||||
private fun updateSize(power: Int) {
|
||||
val size = getSize(power)
|
||||
sizeView.text = getString(R.string.width_by_height, size, size)
|
||||
sizeView.text = getString(R.string.size_width_by_height, size, size)
|
||||
}
|
||||
|
||||
private fun getSize(power: Int) = 128 * (power + 1)
|
||||
private fun initMarginBar() {
|
||||
updateMargin(marginBarView.progress)
|
||||
marginBarView.setOnSeekBarChangeListener(
|
||||
object : SeekBar.OnSeekBarChangeListener {
|
||||
override fun onProgressChanged(
|
||||
seekBar: SeekBar,
|
||||
progressValue: Int,
|
||||
fromUser: Boolean
|
||||
) {
|
||||
if (progressValue >= minMargin) {
|
||||
updateMargin(progressValue)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStartTrackingTouch(seekBar: SeekBar) {}
|
||||
|
||||
override fun onStopTrackingTouch(seekBar: SeekBar) {}
|
||||
})
|
||||
}
|
||||
|
||||
private fun updateMargin(margin: Int) {
|
||||
marginView.text = getString(R.string.margin_size, margin)
|
||||
}
|
||||
|
||||
private fun setMarginBar(minimum: Int, value: Int): Boolean {
|
||||
minMargin = minimum
|
||||
marginBarView.apply {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
min = minimum
|
||||
}
|
||||
progress = max(
|
||||
minimum, if (prefs.lastMargin > -1) {
|
||||
prefs.lastMargin
|
||||
} else {
|
||||
value
|
||||
}
|
||||
)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private fun setEncodeByteArray() {
|
||||
contentView.text = null
|
||||
contentView.hint = getString(R.string.binary_data)
|
||||
contentView.isEnabled = false
|
||||
unescapeCheckBox.isEnabled = false
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val CONTENT = "content"
|
||||
private const val CONTENT_TEXT = "content_text"
|
||||
private const val CONTENT_RAW = "content_raw"
|
||||
private const val FORMAT = "format"
|
||||
private const val PICK_FILE_RESULT_CODE = 1
|
||||
|
||||
fun newInstance(
|
||||
content: String? = null,
|
||||
fun <T> newInstance(
|
||||
content: T? = null,
|
||||
format: String? = null
|
||||
): Fragment {
|
||||
val args = Bundle()
|
||||
content?.let { args.putString(CONTENT, content) }
|
||||
content?.let {
|
||||
when (content) {
|
||||
is String -> args.putString(CONTENT_TEXT, content)
|
||||
is ByteArray -> args.putByteArray(CONTENT_RAW, content)
|
||||
else -> throw IllegalArgumentException(
|
||||
"content must be a String or a ByteArray"
|
||||
)
|
||||
}
|
||||
}
|
||||
format?.let { args.putString(FORMAT, it) }
|
||||
val fragment = EncodeFragment()
|
||||
fragment.arguments = args
|
||||
@@ -185,8 +392,73 @@ class EncodeFragment : Fragment() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun String.toFormat(default: Format = Format.QR_CODE): Format = try {
|
||||
Format.valueOf(this)
|
||||
private fun Boolean.setVisibilityFor(vararg views: View) {
|
||||
val visibility = if (this) View.VISIBLE else View.GONE
|
||||
for (view in views) {
|
||||
view.visibility = visibility
|
||||
}
|
||||
}
|
||||
|
||||
private fun BarcodeFormat.packEcLevel(packed: Int, level: Int): Int {
|
||||
val s = ecLevelShift()
|
||||
return (level shl s) or (packed and (15 shl s).inv())
|
||||
}
|
||||
|
||||
private fun BarcodeFormat.unpackEcLevel(packed: Int) =
|
||||
(packed shr ecLevelShift()) and 15
|
||||
|
||||
private fun BarcodeFormat.ecLevelShift() = when (this) {
|
||||
BarcodeFormat.AZTEC -> 0
|
||||
BarcodeFormat.QR_CODE -> 4
|
||||
BarcodeFormat.PDF_417 -> 8
|
||||
else -> throw IllegalArgumentException("$this does not have error levels")
|
||||
}
|
||||
|
||||
private fun BarcodeFormat.canBeInverted() = when (this) {
|
||||
BarcodeFormat.AZTEC,
|
||||
BarcodeFormat.DATA_MATRIX,
|
||||
BarcodeFormat.QR_CODE -> true
|
||||
|
||||
else -> false
|
||||
}
|
||||
|
||||
private fun String.toFormat(default: BarcodeFormat = BarcodeFormat.QR_CODE): BarcodeFormat = try {
|
||||
BarcodeFormat.valueOf(this)
|
||||
} catch (_: IllegalArgumentException) {
|
||||
default
|
||||
}
|
||||
|
||||
private fun BarcodeFormat.getErrorCorrectionLevel(position: Int) = when (this) {
|
||||
BarcodeFormat.AZTEC -> position
|
||||
BarcodeFormat.QR_CODE -> (position + 1) * 2
|
||||
BarcodeFormat.PDF_417 -> position
|
||||
else -> 0
|
||||
}.coerceIn(0, 8)
|
||||
|
||||
private fun Spinner.setEntries(resId: Int) = ArrayAdapter.createFromResource(
|
||||
this.context,
|
||||
resId,
|
||||
android.R.layout.simple_spinner_item
|
||||
).also { aa ->
|
||||
aa.setDropDownViewResource(
|
||||
android.R.layout.simple_spinner_dropdown_item
|
||||
)
|
||||
adapter = aa
|
||||
}
|
||||
|
||||
private fun getSize(power: Int) = 128 * (power + 1)
|
||||
|
||||
private fun InputStream.readBytesMax(max: Int): ByteArray {
|
||||
var offset = 0
|
||||
var remaining = min(available(), max)
|
||||
val result = ByteArray(remaining)
|
||||
while (remaining > 0) {
|
||||
val read = read(result, offset, remaining)
|
||||
if (read < 0) {
|
||||
break
|
||||
}
|
||||
remaining -= read
|
||||
offset += read
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -22,7 +22,9 @@ import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.adapter.ScansAdapter
|
||||
import de.markusfisch.android.binaryeye.app.*
|
||||
import de.markusfisch.android.binaryeye.content.copyToClipboard
|
||||
import de.markusfisch.android.binaryeye.content.shareAsFile
|
||||
import de.markusfisch.android.binaryeye.content.shareText
|
||||
import de.markusfisch.android.binaryeye.content.wipeLastShareFile
|
||||
import de.markusfisch.android.binaryeye.database.*
|
||||
import de.markusfisch.android.binaryeye.io.askForFileName
|
||||
import de.markusfisch.android.binaryeye.io.toSaveResult
|
||||
@@ -79,7 +81,8 @@ class HistoryFragment : Fragment() {
|
||||
closeActionMode()
|
||||
true
|
||||
}
|
||||
R.id.edit_scan -> {
|
||||
|
||||
R.id.edit_label -> {
|
||||
scansAdapter?.forSelection { id, position ->
|
||||
ac.askForName(
|
||||
id,
|
||||
@@ -90,6 +93,7 @@ class HistoryFragment : Fragment() {
|
||||
closeActionMode()
|
||||
true
|
||||
}
|
||||
|
||||
R.id.remove_scan -> {
|
||||
scansAdapter?.getSelectedIds()?.let {
|
||||
if (it.isNotEmpty()) {
|
||||
@@ -99,6 +103,7 @@ class HistoryFragment : Fragment() {
|
||||
closeActionMode()
|
||||
true
|
||||
}
|
||||
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
@@ -114,6 +119,7 @@ class HistoryFragment : Fragment() {
|
||||
private var filter: String? = null
|
||||
private var clearListMenuItem: MenuItem? = null
|
||||
private var exportHistoryMenuItem: MenuItem? = null
|
||||
private var shareAsFileMenuItem: MenuItem? = null
|
||||
|
||||
override fun onCreate(state: Bundle?) {
|
||||
super.onCreate(state)
|
||||
@@ -140,8 +146,12 @@ class HistoryFragment : Fragment() {
|
||||
initHistorySwitch(useHistorySwitch)
|
||||
|
||||
listView = view.findViewById(R.id.scans)
|
||||
listView.setOnItemClickListener { _, _, _, id ->
|
||||
showScan(id)
|
||||
listView.setOnItemClickListener { _, v, position, id ->
|
||||
if (actionMode != null) {
|
||||
scansAdapter?.select(v, id, position)
|
||||
} else {
|
||||
showScan(id)
|
||||
}
|
||||
}
|
||||
listView.setOnItemLongClickListener { _, v, position, id ->
|
||||
scansAdapter?.select(v, id, position)
|
||||
@@ -156,7 +166,7 @@ class HistoryFragment : Fragment() {
|
||||
|
||||
fab = view.findViewById(R.id.share)
|
||||
fab.setOnClickListener { v ->
|
||||
v.context.pickListSeparatorAndShare()
|
||||
v.context.pickListSeparatorAndShare(false)
|
||||
}
|
||||
|
||||
progressView = view.findViewById(R.id.progress_view)
|
||||
@@ -173,6 +183,7 @@ class HistoryFragment : Fragment() {
|
||||
super.onDestroy()
|
||||
scansAdapter?.changeCursor(null)
|
||||
parentJob.cancel()
|
||||
wipeLastShareFile()
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
@@ -182,10 +193,19 @@ class HistoryFragment : Fragment() {
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
inflater.inflate(R.menu.fragment_history, menu)
|
||||
initSearchView(menu.findItem(R.id.search))
|
||||
menu.setGroupVisible(R.id.scans_available, scansAdapter?.count != 0)
|
||||
clearListMenuItem = menu.findItem(R.id.clear)
|
||||
exportHistoryMenuItem = menu.findItem(R.id.export_history)
|
||||
val searchMenuItem = menu.findItem(R.id.search)
|
||||
initSearchView(searchMenuItem)
|
||||
val visible = scansAdapter?.count != 0
|
||||
searchMenuItem?.isVisible = visible
|
||||
exportHistoryMenuItem = menu.findItem(R.id.export_history)?.apply {
|
||||
isVisible = visible
|
||||
}
|
||||
shareAsFileMenuItem = menu.findItem(R.id.share_as_file)?.apply {
|
||||
isVisible = visible
|
||||
}
|
||||
clearListMenuItem = menu.findItem(R.id.clear)?.apply {
|
||||
isVisible = visible
|
||||
}
|
||||
}
|
||||
|
||||
private fun initSearchView(item: MenuItem?) {
|
||||
@@ -221,10 +241,17 @@ class HistoryFragment : Fragment() {
|
||||
context.askToRemoveScans()
|
||||
true
|
||||
}
|
||||
|
||||
R.id.export_history -> {
|
||||
askToExportToFile()
|
||||
true
|
||||
}
|
||||
|
||||
R.id.share_as_file -> {
|
||||
context.pickListSeparatorAndShare(true)
|
||||
true
|
||||
}
|
||||
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
}
|
||||
@@ -278,6 +305,7 @@ class HistoryFragment : Fragment() {
|
||||
private fun enableMenuItems(enabled: Boolean) {
|
||||
clearListMenuItem?.isEnabled = enabled
|
||||
exportHistoryMenuItem?.isEnabled = enabled
|
||||
shareAsFileMenuItem?.isEnabled = enabled
|
||||
}
|
||||
|
||||
private fun closeActionMode() {
|
||||
@@ -407,32 +435,44 @@ class HistoryFragment : Fragment() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun Context.pickListSeparatorAndShare() {
|
||||
private fun Context.pickListSeparatorAndShare(asFile: Boolean) {
|
||||
val separators = resources.getStringArray(
|
||||
R.array.list_separators_values
|
||||
)
|
||||
AlertDialog.Builder(this)
|
||||
.setTitle(R.string.pick_list_separator)
|
||||
.setItems(R.array.list_separators_names) { _, which ->
|
||||
shareScans(separators[which])
|
||||
shareScans(separators[which], asFile)
|
||||
}
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun shareScans(format: String) = scope.launch {
|
||||
private fun shareScans(format: String, asFile: Boolean) = scope.launch {
|
||||
progressView.useVisibility {
|
||||
var text: String? = null
|
||||
db.getScansDetailed(filter)?.use { cursor ->
|
||||
val selectedIds = scansAdapter?.getSelectedIds()
|
||||
if (selectedIds?.isNotEmpty() == true) {
|
||||
db.getScansDetailed(selectedIds.toLongArray())
|
||||
} else {
|
||||
db.getScansDetailed(filter)
|
||||
}?.use { cursor ->
|
||||
val details = format.split(":")
|
||||
text = when (details[0]) {
|
||||
"text" -> cursor.exportText(details[1])
|
||||
"csv" -> cursor.exportCsv(details[1])
|
||||
else -> cursor.exportJson()
|
||||
when (details[0]) {
|
||||
"text" -> Pair(cursor.exportText(details[1]), "txt")
|
||||
"csv" -> Pair(cursor.exportCsv(details[1]), "csv")
|
||||
else -> Pair(cursor.exportJson(), "json")
|
||||
}
|
||||
}
|
||||
text?.let {
|
||||
withContext(Dispatchers.Main) {
|
||||
context.shareText(it)
|
||||
}?.let { (text, ext) ->
|
||||
text?.let {
|
||||
withContext(Dispatchers.Main) {
|
||||
if (asFile) {
|
||||
context.shareAsFile(
|
||||
it,
|
||||
String.format("scans.%s", ext)
|
||||
)
|
||||
} else {
|
||||
context.shareText(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
package de.markusfisch.android.binaryeye.fragment
|
||||
|
||||
import android.content.Context
|
||||
import android.net.wifi.WifiManager
|
||||
import android.net.wifi.WifiNetworkSuggestion
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.support.annotation.RequiresApi
|
||||
import android.support.v4.app.Fragment
|
||||
import android.view.*
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.ListView
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.view.setPaddingFromWindowInsets
|
||||
import de.markusfisch.android.binaryeye.view.systemBarListViewScrollListener
|
||||
import de.markusfisch.android.binaryeye.widget.toast
|
||||
|
||||
class NetworkSuggestionsFragment : Fragment() {
|
||||
@RequiresApi(Build.VERSION_CODES.R)
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
state: Bundle?
|
||||
): View? {
|
||||
val ac = activity ?: return null
|
||||
ac.setTitle(R.string.network_suggestions)
|
||||
|
||||
val view = inflater.inflate(
|
||||
R.layout.fragment_network_suggestions,
|
||||
container,
|
||||
false
|
||||
)
|
||||
|
||||
val wm = ac.applicationContext.getSystemService(
|
||||
Context.WIFI_SERVICE
|
||||
) as WifiManager
|
||||
val suggestionArrayAdapter = ArrayAdapter(
|
||||
ac,
|
||||
android.R.layout.simple_list_item_checked,
|
||||
wm.networkSuggestions.map {
|
||||
Suggestion(
|
||||
it.ssid ?: it.toString(),
|
||||
it
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
val listView = view.findViewById<ListView>(R.id.suggestions)
|
||||
listView.emptyView = view.findViewById(R.id.no_suggestions)
|
||||
listView.adapter = suggestionArrayAdapter
|
||||
listView.setOnScrollListener(systemBarListViewScrollListener)
|
||||
(view.findViewById(R.id.inset_layout) as View).setPaddingFromWindowInsets()
|
||||
listView.setPaddingFromWindowInsets()
|
||||
|
||||
view.findViewById<View>(R.id.remove).setOnClickListener {
|
||||
val removeList = ArrayList<WifiNetworkSuggestion>()
|
||||
val removeFromAdapter = ArrayList<Suggestion>()
|
||||
val checked = listView.checkedItemPositions
|
||||
val size = checked.size()
|
||||
for (i in 0 until size) {
|
||||
if (checked.valueAt(i)) {
|
||||
val pos = checked.keyAt(i)
|
||||
listView.setItemChecked(pos, false)
|
||||
val suggestion = listView.getItemAtPosition(
|
||||
pos
|
||||
) as Suggestion
|
||||
removeList.add(suggestion.suggestion)
|
||||
removeFromAdapter.add(suggestion)
|
||||
}
|
||||
}
|
||||
if (removeList.isNotEmpty()) {
|
||||
removeFromAdapter.forEach {
|
||||
suggestionArrayAdapter.remove(it)
|
||||
}
|
||||
wm.removeNetworkSuggestions(removeList)
|
||||
} else {
|
||||
ac.toast(
|
||||
R.string.clear_network_suggestions_nothing_to_remove
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return view
|
||||
}
|
||||
}
|
||||
|
||||
private data class Suggestion(
|
||||
val label: String,
|
||||
val suggestion: WifiNetworkSuggestion
|
||||
) {
|
||||
override fun toString() = label
|
||||
}
|
||||
@@ -16,9 +16,12 @@ 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.addFragment
|
||||
import de.markusfisch.android.binaryeye.app.hasBluetoothPermission
|
||||
import de.markusfisch.android.binaryeye.app.prefs
|
||||
import de.markusfisch.android.binaryeye.bluetooth.setBluetoothHosts
|
||||
import de.markusfisch.android.binaryeye.media.beepConfirm
|
||||
import de.markusfisch.android.binaryeye.preference.UrlPreference
|
||||
import de.markusfisch.android.binaryeye.view.setPaddingFromWindowInsets
|
||||
import de.markusfisch.android.binaryeye.view.systemBarRecyclerViewScrollListener
|
||||
@@ -27,33 +30,70 @@ import de.markusfisch.android.binaryeye.widget.toast
|
||||
class PreferencesFragment : PreferenceFragmentCompat() {
|
||||
private val changeListener = object : OnSharedPreferenceChangeListener {
|
||||
override fun onSharedPreferenceChanged(
|
||||
sharedPreferences: SharedPreferences,
|
||||
key: String
|
||||
sharedPreferences: SharedPreferences?,
|
||||
key: String?
|
||||
) {
|
||||
key ?: return
|
||||
val preference = findPreference(key) ?: return
|
||||
prefs.update()
|
||||
if (preference.key == "custom_locale") {
|
||||
activity?.restartApp()
|
||||
} else {
|
||||
setSummary(preference)
|
||||
when (preference.key) {
|
||||
"custom_locale" -> activity?.restartApp()
|
||||
"beep_tone_name" -> {
|
||||
beepConfirm()
|
||||
setSummary(preference)
|
||||
}
|
||||
|
||||
"send_scan_bluetooth" -> {
|
||||
if (prefs.sendScanBluetooth &&
|
||||
activity?.hasBluetoothPermission() == false
|
||||
) {
|
||||
prefs.sendScanBluetooth = false
|
||||
}
|
||||
setSummary(preference)
|
||||
}
|
||||
|
||||
else -> setSummary(preference)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreatePreferences(state: Bundle?, rootKey: String?) {
|
||||
addPreferencesFromResource(R.xml.preferences)
|
||||
activity?.setTitle(R.string.preferences)
|
||||
setBluetoothResources()
|
||||
wireClearNetworkPreferences()
|
||||
}
|
||||
|
||||
private fun setBluetoothResources() {
|
||||
if (prefs.sendScanBluetooth &&
|
||||
activity?.hasBluetoothPermission() == true
|
||||
) {
|
||||
setBluetoothHosts(
|
||||
findPreference("send_scan_bluetooth_host") as ListPreference
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
context.askToClearNetworkSuggestions()
|
||||
true
|
||||
findPreference("clear_network_suggestions").apply {
|
||||
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.Q) {
|
||||
// From R+ we can query past network suggestions and
|
||||
// make them editable.
|
||||
setOnPreferenceClickListener {
|
||||
fragmentManager.addFragment(NetworkSuggestionsFragment())
|
||||
true
|
||||
}
|
||||
} else if (Build.VERSION.SDK_INT == Build.VERSION_CODES.Q) {
|
||||
// On Q, we can only clear *all* suggestions.
|
||||
// Note that previous versions of this app allowed
|
||||
// adding network suggestions on Q as well, so we
|
||||
// need to keep this option.
|
||||
setOnPreferenceClickListener {
|
||||
context.askToClearNetworkSuggestions()
|
||||
true
|
||||
}
|
||||
} else {
|
||||
// There are no network suggestions below Q.
|
||||
isVisible = false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -72,8 +112,8 @@ class PreferencesFragment : PreferenceFragmentCompat() {
|
||||
@RequiresApi(Build.VERSION_CODES.Q)
|
||||
private fun Context.clearNetworkSuggestions() {
|
||||
toast(
|
||||
if (
|
||||
removeNetworkSuggestions(this) == WifiManager.STATUS_NETWORK_SUGGESTIONS_SUCCESS
|
||||
if (removeAllNetworkSuggestions() ==
|
||||
WifiManager.STATUS_NETWORK_SUGGESTIONS_SUCCESS
|
||||
) {
|
||||
R.string.clear_network_suggestions_success
|
||||
} else {
|
||||
@@ -84,6 +124,7 @@ class PreferencesFragment : PreferenceFragmentCompat() {
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
activity?.setTitle(R.string.preferences)
|
||||
listView.setPaddingFromWindowInsets()
|
||||
listView.removeOnScrollListener(systemBarRecyclerViewScrollListener)
|
||||
listView.addOnScrollListener(systemBarRecyclerViewScrollListener)
|
||||
@@ -105,6 +146,12 @@ class PreferencesFragment : PreferenceFragmentCompat() {
|
||||
setTargetFragment(this@PreferencesFragment, 0)
|
||||
show(fm, null)
|
||||
}
|
||||
} else if (preference.key == "send_scan_bluetooth_host") {
|
||||
val ac = activity ?: return
|
||||
if (ac.hasBluetoothPermission()) {
|
||||
setBluetoothHosts(preference as ListPreference)
|
||||
}
|
||||
super.onDisplayPreferenceDialog(preference)
|
||||
} else {
|
||||
super.onDisplayPreferenceDialog(preference)
|
||||
}
|
||||
@@ -122,9 +169,11 @@ class PreferencesFragment : PreferenceFragmentCompat() {
|
||||
is UrlPreference -> {
|
||||
preference.setSummary(preference.getUrl())
|
||||
}
|
||||
|
||||
is ListPreference -> {
|
||||
preference.setSummary(preference.entry)
|
||||
}
|
||||
|
||||
is MultiSelectListPreference -> {
|
||||
preference.setSummary(
|
||||
preference.values.joinToString(", ") {
|
||||
@@ -132,6 +181,7 @@ class PreferencesFragment : PreferenceFragmentCompat() {
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
is PreferenceGroup -> {
|
||||
setSummaries(preference)
|
||||
}
|
||||
@@ -152,3 +202,9 @@ private fun Activity.restartApp() {
|
||||
// Restart to begin with an unmodified Locale to follow system settings.
|
||||
Runtime.getRuntime().exit(0)
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.Q)
|
||||
private fun Context.removeAllNetworkSuggestions(): Int =
|
||||
(applicationContext.getSystemService(
|
||||
Context.WIFI_SERVICE
|
||||
) as WifiManager).removeNetworkSuggestions(listOf())
|
||||
|
||||
@@ -9,6 +9,7 @@ import de.markusfisch.android.binaryeye.app.prefs
|
||||
import de.markusfisch.android.binaryeye.database.Scan
|
||||
import de.markusfisch.android.binaryeye.net.sendAsync
|
||||
import de.markusfisch.android.binaryeye.preference.UrlPreference
|
||||
import de.markusfisch.android.zxingcpp.ZxingCpp.BarcodeFormat
|
||||
|
||||
class UrlDialogFragment : PreferenceDialogFragmentCompat() {
|
||||
private var urlView: TextView? = null
|
||||
@@ -50,7 +51,7 @@ class UrlDialogFragment : PreferenceDialogFragmentCompat() {
|
||||
Scan(
|
||||
"test",
|
||||
null,
|
||||
"none"
|
||||
BarcodeFormat.NONE
|
||||
).sendAsync(url, prefs.sendScanType) { code, body ->
|
||||
textView.text = when {
|
||||
code != null -> "$code"
|
||||
|
||||
@@ -55,17 +55,17 @@ fun Bitmap.crop(
|
||||
pivotX: Float,
|
||||
pivotY: Float
|
||||
) = try {
|
||||
val erected = erect(rotation, pivotX, pivotY)
|
||||
val w = erected.width
|
||||
val h = erected.height
|
||||
val b = rotate(rotation, pivotX, pivotY)
|
||||
val w = b.width
|
||||
val h = b.height
|
||||
val x = max(0, (rect.left * w).roundToInt())
|
||||
val y = max(0, (rect.top * h).roundToInt())
|
||||
Bitmap.createBitmap(
|
||||
erected,
|
||||
b,
|
||||
x,
|
||||
y,
|
||||
min(w - x, (rect.right * w).roundToInt() - x),
|
||||
min(h - y, (rect.bottom * h).roundToInt() - y)
|
||||
min(w, (rect.right * w).roundToInt()) - x,
|
||||
min(h, (rect.bottom * h).roundToInt()) - y
|
||||
)
|
||||
} catch (e: OutOfMemoryError) {
|
||||
null
|
||||
@@ -73,13 +73,11 @@ fun Bitmap.crop(
|
||||
null
|
||||
}
|
||||
|
||||
private fun Bitmap.erect(
|
||||
private fun Bitmap.rotate(
|
||||
rotation: Float,
|
||||
pivotX: Float,
|
||||
pivotY: Float
|
||||
): Bitmap = if (
|
||||
rotation % 360f != 0f
|
||||
) {
|
||||
): Bitmap = if (rotation % 360f != 0f) {
|
||||
Bitmap.createBitmap(
|
||||
this,
|
||||
0,
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
package de.markusfisch.android.binaryeye.graphics
|
||||
|
||||
const val COLOR_BLACK = 0xff000000.toInt()
|
||||
const val COLOR_WHITE = 0xffffffff.toInt()
|
||||
@@ -19,10 +19,10 @@ fun Rect.setFrameRoi(
|
||||
) {
|
||||
Matrix().apply {
|
||||
// Map ROI from view coordinates to frame coordinates.
|
||||
setScale(1f / viewRect.width(), 1f / viewRect.height())
|
||||
setTranslate(-viewRect.left.toFloat(), -viewRect.top.toFloat())
|
||||
postScale(1f / viewRect.width(), 1f / viewRect.height())
|
||||
postRotate(-frameMetrics.orientation.toFloat(), .5f, .5f)
|
||||
postScale(frameMetrics.width.toFloat(), frameMetrics.height.toFloat())
|
||||
postTranslate(-viewRect.left.toFloat(), -viewRect.top.toFloat())
|
||||
val frameRoiF = RectF()
|
||||
val viewRoiF = RectF(
|
||||
viewRoi.left.toFloat(),
|
||||
@@ -54,6 +54,7 @@ fun Matrix.setFrameToView(
|
||||
uprightWidth = frameMetrics.height
|
||||
uprightHeight = frameMetrics.width
|
||||
}
|
||||
|
||||
else -> {
|
||||
uprightWidth = frameMetrics.width
|
||||
uprightHeight = frameMetrics.height
|
||||
|
||||
@@ -44,9 +44,9 @@ private fun Bitmap.analyzeTransparency(): Result {
|
||||
return Result(
|
||||
transparent > 0,
|
||||
if (bright > 0 && visible / bright < 2) {
|
||||
0xff000000.toInt()
|
||||
COLOR_BLACK
|
||||
} else {
|
||||
0xffffffff.toInt()
|
||||
COLOR_WHITE
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -61,7 +61,6 @@ private fun Context.openExternalOutputStream(
|
||||
fileName: String,
|
||||
mimeType: String
|
||||
): OutputStream = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
||||
@Suppress("DEPRECATION")
|
||||
val file = File(
|
||||
Environment.getExternalStoragePublicDirectory(
|
||||
Environment.DIRECTORY_DOWNLOADS
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
package de.markusfisch.android.binaryeye.media
|
||||
|
||||
import android.media.AudioManager
|
||||
import android.media.ToneGenerator
|
||||
import de.markusfisch.android.binaryeye.app.prefs
|
||||
|
||||
private var confirmToneGenerator: ToneGenerator? = null
|
||||
private var errorToneGenerator: ToneGenerator? = null
|
||||
|
||||
fun beepConfirm() {
|
||||
val tg = confirmToneGenerator ?: ToneGenerator(
|
||||
AudioManager.STREAM_NOTIFICATION,
|
||||
ToneGenerator.MAX_VOLUME
|
||||
)
|
||||
confirmToneGenerator = tg
|
||||
tg.startTone(prefs.beepTone())
|
||||
}
|
||||
|
||||
fun beepError() {
|
||||
val tg = errorToneGenerator ?: ToneGenerator(
|
||||
AudioManager.STREAM_ALARM,
|
||||
ToneGenerator.MAX_VOLUME
|
||||
)
|
||||
errorToneGenerator = tg
|
||||
tg.startTone(ToneGenerator.TONE_SUP_ERROR, 1000)
|
||||
}
|
||||
|
||||
fun releaseToneGenerators() {
|
||||
confirmToneGenerator?.release()
|
||||
confirmToneGenerator = null
|
||||
errorToneGenerator?.release()
|
||||
errorToneGenerator = null
|
||||
}
|
||||
@@ -33,6 +33,7 @@ private fun Scan.send(url: String, type: String): Response {
|
||||
"1" -> request(
|
||||
url + "?" + asUrlArguments()
|
||||
)
|
||||
|
||||
"2" -> request(url) { con ->
|
||||
con.requestMethod = "POST"
|
||||
con.setRequestProperty("Content-Type", "application/x-www-form-urlencoded")
|
||||
@@ -41,6 +42,7 @@ private fun Scan.send(url: String, type: String): Response {
|
||||
close()
|
||||
}
|
||||
}
|
||||
|
||||
"3" -> request(url) { con ->
|
||||
con.requestMethod = "POST"
|
||||
con.setRequestProperty("Content-Type", "application/json")
|
||||
@@ -49,6 +51,7 @@ private fun Scan.send(url: String, type: String): Response {
|
||||
close()
|
||||
}
|
||||
}
|
||||
|
||||
else -> request(
|
||||
url + content.urlEncode()
|
||||
)
|
||||
@@ -66,9 +69,9 @@ private fun Scan.asJson() = JSONObject().apply {
|
||||
private fun Scan.getMap(): Map<String, String> = mapOf(
|
||||
"content" to content,
|
||||
"raw" to raw?.toHexString(),
|
||||
"format" to format,
|
||||
"format" to format.name,
|
||||
"errorCorrectionLevel" to errorCorrectionLevel,
|
||||
"versionNumber" to versionNumber.toString(),
|
||||
"version" to version,
|
||||
"sequenceSize" to sequenceSize.toString(),
|
||||
"sequenceIndex" to sequenceIndex.toString(),
|
||||
"sequenceId" to sequenceId,
|
||||
|
||||
@@ -4,9 +4,8 @@ import android.content.Context
|
||||
import android.os.Build
|
||||
import android.os.VibrationEffect
|
||||
import android.os.Vibrator
|
||||
import de.markusfisch.android.binaryeye.app.prefs
|
||||
|
||||
private const val DURATION_IN_MS = 100L
|
||||
private const val DURATION_IN_MS = 250L
|
||||
|
||||
private val beforeO = Build.VERSION.SDK_INT < Build.VERSION_CODES.O
|
||||
private val errorPatternTimings = longArrayOf(
|
||||
@@ -38,9 +37,7 @@ fun Context.getVibrator(): Vibrator = getSystemService(
|
||||
) as Vibrator
|
||||
|
||||
fun Vibrator.vibrate() {
|
||||
if (!prefs.vibrate) {
|
||||
return
|
||||
} else if (beforeO) {
|
||||
if (beforeO) {
|
||||
@Suppress("DEPRECATION")
|
||||
vibrate(DURATION_IN_MS)
|
||||
} else {
|
||||
@@ -54,9 +51,7 @@ fun Vibrator.vibrate() {
|
||||
}
|
||||
|
||||
fun Vibrator.error() {
|
||||
if (!prefs.vibrate) {
|
||||
return
|
||||
} else if (beforeO) {
|
||||
if (beforeO) {
|
||||
@Suppress("DEPRECATION")
|
||||
vibrate(errorPatternTimings, -1)
|
||||
} else {
|
||||
|
||||
@@ -1,33 +1,37 @@
|
||||
package de.markusfisch.android.binaryeye.preference
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import android.media.ToneGenerator
|
||||
import android.os.Build
|
||||
import android.preference.PreferenceManager
|
||||
import android.support.annotation.RequiresApi
|
||||
import de.markusfisch.android.zxingcpp.ZxingCpp.Format
|
||||
import de.markusfisch.android.zxingcpp.ZxingCpp.BarcodeFormat
|
||||
|
||||
class Preferences {
|
||||
lateinit var preferences: SharedPreferences
|
||||
|
||||
var barcodeFormats = setOf(
|
||||
Format.AZTEC.name,
|
||||
Format.CODABAR.name,
|
||||
Format.CODE_39.name,
|
||||
Format.CODE_93.name,
|
||||
Format.CODE_128.name,
|
||||
Format.DATA_BAR.name,
|
||||
Format.DATA_BAR_EXPANDED.name,
|
||||
Format.DATA_MATRIX.name,
|
||||
Format.EAN_8.name,
|
||||
Format.EAN_13.name,
|
||||
Format.ITF.name,
|
||||
Format.MAXICODE.name,
|
||||
Format.PDF_417.name,
|
||||
Format.QR_CODE.name,
|
||||
Format.MICRO_QR_CODE.name,
|
||||
Format.UPC_A.name,
|
||||
Format.UPC_E.name,
|
||||
BarcodeFormat.AZTEC.name,
|
||||
BarcodeFormat.CODABAR.name,
|
||||
BarcodeFormat.CODE_39.name,
|
||||
BarcodeFormat.CODE_93.name,
|
||||
BarcodeFormat.CODE_128.name,
|
||||
BarcodeFormat.DATA_BAR.name,
|
||||
BarcodeFormat.DATA_BAR_EXPANDED.name,
|
||||
BarcodeFormat.DATA_MATRIX.name,
|
||||
BarcodeFormat.DX_FILM_EDGE.name,
|
||||
BarcodeFormat.EAN_8.name,
|
||||
BarcodeFormat.EAN_13.name,
|
||||
BarcodeFormat.ITF.name,
|
||||
BarcodeFormat.MAXICODE.name,
|
||||
BarcodeFormat.PDF_417.name,
|
||||
BarcodeFormat.QR_CODE.name,
|
||||
BarcodeFormat.MICRO_QR_CODE.name,
|
||||
BarcodeFormat.RMQR_CODE.name,
|
||||
BarcodeFormat.UPC_A.name,
|
||||
BarcodeFormat.UPC_E.name,
|
||||
)
|
||||
@RequiresApi(Build.VERSION_CODES.HONEYCOMB)
|
||||
set(value) {
|
||||
@@ -61,37 +65,27 @@ class Preferences {
|
||||
}
|
||||
var autoRotate = true
|
||||
set(value) {
|
||||
// Immediately save this setting before it shouldn't change
|
||||
// on the fly while scanning.
|
||||
commit(AUTO_ROTATE, value)
|
||||
apply(AUTO_ROTATE, value)
|
||||
field = value
|
||||
}
|
||||
var tryHarder = false
|
||||
set(value) {
|
||||
// Immediately save this setting because it's only ever read
|
||||
// before the camera is opened.
|
||||
commit(TRY_HARDER, value)
|
||||
apply(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)
|
||||
apply(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)
|
||||
apply(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)
|
||||
apply(SHOW_TOAST_IN_BULK_MODE, value)
|
||||
field = value
|
||||
}
|
||||
var vibrate = true
|
||||
@@ -99,6 +93,16 @@ class Preferences {
|
||||
apply(VIBRATE, value)
|
||||
field = value
|
||||
}
|
||||
var beep = false
|
||||
set(value) {
|
||||
apply(BEEP, value)
|
||||
field = value
|
||||
}
|
||||
var beepToneName = "tone_prop_beep"
|
||||
set(value) {
|
||||
apply(BEEP_TONE_NAME, value)
|
||||
field = value
|
||||
}
|
||||
var useHistory = false
|
||||
set(value) {
|
||||
apply(USE_HISTORY, value)
|
||||
@@ -149,7 +153,7 @@ class Preferences {
|
||||
apply(OPEN_WITH_URL, value)
|
||||
field = value
|
||||
}
|
||||
var sendScanActive = true
|
||||
var sendScanActive = false
|
||||
set(value) {
|
||||
apply(SEND_SCAN_ACTIVE, value)
|
||||
field = value
|
||||
@@ -164,9 +168,21 @@ class Preferences {
|
||||
apply(SEND_SCAN_TYPE, value)
|
||||
field = value
|
||||
}
|
||||
var sendScanBluetooth = false
|
||||
set(value) {
|
||||
apply(SEND_SCAN_BLUETOOTH, value)
|
||||
field = value
|
||||
}
|
||||
var sendScanBluetoothHost: String = ""
|
||||
set(value) {
|
||||
apply(SEND_SCAN_BLUETOOTH_HOST, value)
|
||||
field = value
|
||||
}
|
||||
var customLocale: String = ""
|
||||
set(value) {
|
||||
apply(CUSTOM_LOCALE, value)
|
||||
// Make sure this setting is written immediately because
|
||||
// the app is about to restart.
|
||||
commit(CUSTOM_LOCALE, value)
|
||||
field = value
|
||||
}
|
||||
var indexOfLastSelectedFormat: Int = 0
|
||||
@@ -174,11 +190,24 @@ class Preferences {
|
||||
apply(INDEX_OF_LAST_SELECTED_FORMAT, value)
|
||||
field = value
|
||||
}
|
||||
var forceCompat = false
|
||||
var indexOfLastSelectedEcLevel: Int = 0
|
||||
set(value) {
|
||||
// Since the app may be about to crash when forceCompat is set,
|
||||
// it's necessary to `commit()` this synchronously.
|
||||
commit(FORCE_COMPAT, value)
|
||||
apply(INDEX_OF_LAST_SELECTED_EC_LEVEL, value)
|
||||
field = value
|
||||
}
|
||||
var lastMargin: Int = -1
|
||||
set(value) {
|
||||
apply(LAST_MARGIN, value)
|
||||
field = value
|
||||
}
|
||||
var freeRotation = false
|
||||
set(value) {
|
||||
apply(FREE_ROTATION, value)
|
||||
field = value
|
||||
}
|
||||
var expandEscapeSequences = true
|
||||
set(value) {
|
||||
apply(EXPAND_ESCAPE_SEQUENCES, value)
|
||||
field = value
|
||||
}
|
||||
|
||||
@@ -190,7 +219,11 @@ class Preferences {
|
||||
fun update() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
|
||||
preferences.getStringSet(BARCODE_FORMATS, barcodeFormats)?.let {
|
||||
barcodeFormats = it
|
||||
barcodeFormats = addFormatsOnUpdate(
|
||||
it,
|
||||
BarcodeFormat.RMQR_CODE,
|
||||
BarcodeFormat.DX_FILM_EDGE
|
||||
)
|
||||
}
|
||||
}
|
||||
cropHandleX = preferences.getInt(CROP_HANDLE_X, cropHandleX)
|
||||
@@ -216,6 +249,10 @@ class Preferences {
|
||||
showToastInBulkMode
|
||||
)
|
||||
vibrate = preferences.getBoolean(VIBRATE, vibrate)
|
||||
beep = preferences.getBoolean(BEEP, beep)
|
||||
preferences.getString(BEEP_TONE_NAME, beepToneName)?.also {
|
||||
beepToneName = it
|
||||
}
|
||||
useHistory = preferences.getBoolean(USE_HISTORY, useHistory)
|
||||
ignoreConsecutiveDuplicates = preferences.getBoolean(
|
||||
IGNORE_CONSECUTIVE_DUPLICATES,
|
||||
@@ -252,6 +289,16 @@ class Preferences {
|
||||
preferences.getString(SEND_SCAN_TYPE, sendScanType)?.also {
|
||||
sendScanType = it
|
||||
}
|
||||
sendScanBluetooth = preferences.getBoolean(
|
||||
SEND_SCAN_BLUETOOTH,
|
||||
sendScanBluetooth
|
||||
)
|
||||
preferences.getString(
|
||||
SEND_SCAN_BLUETOOTH_HOST,
|
||||
sendScanBluetoothHost
|
||||
)?.also {
|
||||
sendScanBluetoothHost = it
|
||||
}
|
||||
preferences.getString(CUSTOM_LOCALE, customLocale)?.also {
|
||||
customLocale = it
|
||||
}
|
||||
@@ -259,7 +306,41 @@ class Preferences {
|
||||
INDEX_OF_LAST_SELECTED_FORMAT,
|
||||
indexOfLastSelectedFormat
|
||||
)
|
||||
forceCompat = preferences.getBoolean(FORCE_COMPAT, forceCompat)
|
||||
indexOfLastSelectedEcLevel = preferences.getInt(
|
||||
INDEX_OF_LAST_SELECTED_EC_LEVEL,
|
||||
indexOfLastSelectedEcLevel
|
||||
)
|
||||
lastMargin = preferences.getInt(
|
||||
LAST_MARGIN,
|
||||
lastMargin
|
||||
)
|
||||
freeRotation = preferences.getBoolean(FREE_ROTATION, freeRotation)
|
||||
expandEscapeSequences = preferences.getBoolean(
|
||||
EXPAND_ESCAPE_SEQUENCES,
|
||||
expandEscapeSequences
|
||||
)
|
||||
}
|
||||
|
||||
private fun addFormatsOnUpdate(
|
||||
restored: Set<String>,
|
||||
vararg formats: BarcodeFormat
|
||||
) = restored.toMutableSet().apply {
|
||||
for (format in formats) {
|
||||
val name = "${format.name}_added"
|
||||
if (!preferences.getBoolean(name, false)) {
|
||||
preferences.edit().putBoolean(name, true).apply()
|
||||
add(format.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun beepTone() = when (beepToneName) {
|
||||
"tone_cdma_confirm" -> ToneGenerator.TONE_CDMA_CONFIRM
|
||||
"tone_sup_radio_ack" -> ToneGenerator.TONE_SUP_RADIO_ACK
|
||||
"tone_prop_ack" -> ToneGenerator.TONE_PROP_ACK
|
||||
"tone_prop_beep" -> ToneGenerator.TONE_PROP_BEEP
|
||||
"tone_prop_beep2" -> ToneGenerator.TONE_PROP_BEEP2
|
||||
else -> ToneGenerator.TONE_PROP_BEEP
|
||||
}
|
||||
|
||||
private fun put(label: String, value: Boolean) =
|
||||
@@ -268,14 +349,6 @@ class Preferences {
|
||||
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()
|
||||
}
|
||||
@@ -288,6 +361,11 @@ class Preferences {
|
||||
preferences.edit().putInt(label, value).apply()
|
||||
}
|
||||
|
||||
@SuppressLint("ApplySharedPref")
|
||||
private fun commit(label: String, value: String) {
|
||||
preferences.edit().putString(label, value).commit()
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.HONEYCOMB)
|
||||
private fun apply(label: String, value: Set<String>) {
|
||||
preferences.edit().putStringSet(label, value).apply()
|
||||
@@ -306,6 +384,8 @@ class Preferences {
|
||||
private const val BULK_MODE_DELAY = "bulk_mode_delay"
|
||||
private const val SHOW_TOAST_IN_BULK_MODE = "show_toast_in_bulk_mode"
|
||||
private const val VIBRATE = "vibrate"
|
||||
private const val BEEP = "beep"
|
||||
private const val BEEP_TONE_NAME = "beep_tone_name"
|
||||
private const val USE_HISTORY = "use_history"
|
||||
private const val IGNORE_CONSECUTIVE_DUPLICATES = "ignore_consecutive_duplicates"
|
||||
private const val OPEN_IMMEDIATELY = "open_immediately"
|
||||
@@ -319,8 +399,13 @@ class Preferences {
|
||||
private const val SEND_SCAN_ACTIVE = "send_scan_active"
|
||||
private const val SEND_SCAN_URL = "send_scan_url"
|
||||
private const val SEND_SCAN_TYPE = "send_scan_type"
|
||||
private const val SEND_SCAN_BLUETOOTH = "send_scan_bluetooth"
|
||||
private const val SEND_SCAN_BLUETOOTH_HOST = "send_scan_bluetooth_host"
|
||||
private const val CUSTOM_LOCALE = "custom_locale"
|
||||
private const val INDEX_OF_LAST_SELECTED_FORMAT = "index_of_last_selected_format"
|
||||
private const val FORCE_COMPAT = "force_compat"
|
||||
private const val INDEX_OF_LAST_SELECTED_EC_LEVEL = "index_of_last_selected_ec_level"
|
||||
private const val LAST_MARGIN = "last_margin"
|
||||
private const val FREE_ROTATION = "free_rotation"
|
||||
private const val EXPAND_ESCAPE_SEQUENCES = "expand_escape_sequences"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package de.markusfisch.android.binaryeye.service
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.PendingIntent
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.service.quicksettings.TileService
|
||||
@@ -12,6 +14,21 @@ class ScanTileService : TileService() {
|
||||
super.onClick()
|
||||
val intent = Intent(applicationContext, CameraActivity::class.java)
|
||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
startActivityAndCollapse(intent)
|
||||
// TileService#startActivityAndCollapse(Intent) is only ever run
|
||||
// below UpsideDownCake, but Lint doesn't understand this yet.
|
||||
@SuppressLint("StartActivityAndCollapseDeprecated")
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||
@Suppress("DEPRECATION")
|
||||
startActivityAndCollapse(intent)
|
||||
} else {
|
||||
startActivityAndCollapse(
|
||||
PendingIntent.getActivity(
|
||||
this,
|
||||
0,
|
||||
intent,
|
||||
PendingIntent.FLAG_IMMUTABLE
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
package de.markusfisch.android.binaryeye.text
|
||||
|
||||
/**
|
||||
* Copyright (c) 2000, Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
// This code is based on Google's own implementation:
|
||||
// http://www.java2s.com/example/android-utility-method/string-escape/javascriptunescape-string-s-dc359.html
|
||||
fun String.unescape(): String {
|
||||
val len = length
|
||||
var i = 0
|
||||
fun String.expand() = when (val ec = get(i++)) {
|
||||
'n' -> '\n'
|
||||
'r' -> '\r'
|
||||
't' -> '\t'
|
||||
'b' -> '\b'
|
||||
'\\', '\"', '\'', '<', '>', '|' -> ec
|
||||
'0', '1', '2', '3', '4', '5', '6', '7' -> {
|
||||
val limit = if (ec < '4') 3 else 2
|
||||
var l = 1
|
||||
--i // Back to index of first digit.
|
||||
while (l < limit &&
|
||||
i + l < len &&
|
||||
get(i + l) in '0'..'7'
|
||||
) {
|
||||
++l
|
||||
}
|
||||
val from = i
|
||||
i += l
|
||||
substring(from, i).toInt(8).toChar()
|
||||
}
|
||||
|
||||
'x', 'u' -> {
|
||||
val l = if (ec == 'u') 4 else 2
|
||||
val hexCode = try {
|
||||
substring(i, i + l)
|
||||
} catch (_: IndexOutOfBoundsException) {
|
||||
throw IllegalArgumentException(
|
||||
"Invalid unicode sequence [${substring(i)}] at index $i"
|
||||
)
|
||||
}
|
||||
val unicodeValue = try {
|
||||
hexCode.toInt(16)
|
||||
} catch (_: NumberFormatException) {
|
||||
throw IllegalArgumentException(
|
||||
"Invalid unicode sequence [$hexCode] at index $i"
|
||||
)
|
||||
}
|
||||
i += l
|
||||
unicodeValue.toChar()
|
||||
}
|
||||
|
||||
else -> throw IllegalArgumentException(
|
||||
"Unknown escape code [$ec] at index $i"
|
||||
)
|
||||
}
|
||||
|
||||
val sb = StringBuilder()
|
||||
while (i < len) {
|
||||
val ch = get(i++)
|
||||
sb.append(if (ch == '\\' && i < len) expand() else ch)
|
||||
}
|
||||
return sb.toString()
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package de.markusfisch.android.binaryeye.view
|
||||
|
||||
import android.content.Context
|
||||
import android.media.AudioManager
|
||||
import de.markusfisch.android.binaryeye.app.prefs
|
||||
import de.markusfisch.android.binaryeye.media.beepConfirm
|
||||
import de.markusfisch.android.binaryeye.media.beepError
|
||||
import de.markusfisch.android.binaryeye.os.error
|
||||
import de.markusfisch.android.binaryeye.os.getVibrator
|
||||
import de.markusfisch.android.binaryeye.os.vibrate
|
||||
|
||||
fun Context.scanFeedback() {
|
||||
if (prefs.vibrate) {
|
||||
getVibrator().vibrate()
|
||||
}
|
||||
if (prefs.beep && !isSilent()) {
|
||||
beepConfirm()
|
||||
}
|
||||
}
|
||||
|
||||
fun Context.errorFeedback() {
|
||||
if (prefs.vibrate) {
|
||||
getVibrator().error()
|
||||
}
|
||||
if (prefs.beep && !isSilent()) {
|
||||
beepError()
|
||||
}
|
||||
}
|
||||
|
||||
private fun Context.isSilent(): Boolean {
|
||||
val am = getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
||||
return when (am.ringerMode) {
|
||||
AudioManager.RINGER_MODE_SILENT,
|
||||
AudioManager.RINGER_MODE_VIBRATE -> true
|
||||
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
@@ -162,6 +162,7 @@ class DetectorView : View {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
MotionEvent.ACTION_MOVE -> {
|
||||
if (handleGrabbed) {
|
||||
handlePos.set(x, y)
|
||||
@@ -176,6 +177,7 @@ class DetectorView : View {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
MotionEvent.ACTION_CANCEL -> {
|
||||
if (handleGrabbed) {
|
||||
snap(x, y)
|
||||
@@ -183,6 +185,7 @@ class DetectorView : View {
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
MotionEvent.ACTION_UP -> {
|
||||
if (handleGrabbed) {
|
||||
if (!handleActive) {
|
||||
@@ -199,6 +202,7 @@ class DetectorView : View {
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
else -> super.onTouchEvent(event)
|
||||
}
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 5.7 KiB After Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 9.9 KiB After Width: | Height: | Size: 4.6 KiB |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 731 B |
|
Before Width: | Height: | Size: 899 B After Width: | Height: | Size: 583 B |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 755 B |
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 4.7 KiB After Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 899 B |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 698 B |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 6.3 KiB After Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 7.8 KiB After Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 6.0 KiB |
|
Before Width: | Height: | Size: 4.6 KiB After Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 4.9 KiB After Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 8.6 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 6.2 KiB After Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 6.7 KiB After Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 16 KiB |
@@ -0,0 +1,3 @@
|
||||
<vector android:height="24dp" android:tint="#FFFFFF" android:viewportHeight="24.0" android:viewportWidth="24.0" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#FF000000" android:pathData="M14.54,18.49L14.54,16.51L17.51,16.51L17.51,13.54L19.49,13.54L19.49,16.51L22.46,16.51L22.46,18.49L19.508,18.508L19.49,21.46L17.51,21.46L17.492,18.508L14.54,18.49ZM13.35,14.75L11,12.4L11,7L13,7L13,11.6L14.4,13L13.35,14.75ZM3,10L3,4L5,4L5,6.35C5.85,5.283 6.888,4.458 8.113,3.875C9.338,3.292 10.633,3 12,3C14.5,3 16.625,3.875 18.375,5.625C20.125,7.375 21,9.5 21,12L19,12C19,10.05 18.321,8.396 16.963,7.037C15.604,5.679 13.95,5 12,5C10.85,5 9.775,5.267 8.775,5.8C7.775,6.333 6.933,7.067 6.25,8L9,8L9,10L3,10ZM12,21C9.7,21 7.696,20.237 5.988,18.712C4.279,17.187 3.3,15.283 3.05,13L5.1,13C5.333,14.733 6.104,16.167 7.413,17.3C8.721,18.433 10.25,19 12,19C12.183,19 12.354,18.996 12.513,18.987C12.671,18.979 12.833,18.958 13,18.925L13,20.95C12.833,20.967 12.671,20.979 12.513,20.987C12.354,20.996 12.183,21 12,21Z"/>
|
||||
</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="M18 8h-1V6c0-2.76-2.24-5-5-5S7 3.24 7 6v2H6c-1.1 0-2 0.9-2 2v10c0 1.1 0.9 2 2 2h12c1.1 0 2-0.9 2-2V10c0-1.1-0.9-2-2-2zm-6 9c-1.1 0-2-0.9-2-2s0.9-2 2-2 2 0.9 2 2-0.9 2-2 2zm3.1-9H8.9V6c0-1.71 1.39-3.1 3.1-3.1 1.71 0 3.1 1.39 3.1 3.1v2z"/>
|
||||
</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 17c1.1 0 2-0.9 2-2s-0.9-2-2-2-2 0.9-2 2 0.9 2 2 2zm6-9h-1V6c0-2.76-2.24-5-5-5S7 3.24 7 6h1.9c0-1.71 1.39-3.1 3.1-3.1 1.71 0 3.1 1.39 3.1 3.1v2H6c-1.1 0-2 0.9-2 2v10c0 1.1 0.9 2 2 2h12c1.1 0 2-0.9 2-2V10c0-1.1-0.9-2-2-2zm0 12H6V10h12v10z"/>
|
||||
</vector>
|
||||
@@ -1,5 +1,4 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="90dp" android:height="90dp" android:viewportWidth="90" android:viewportHeight="90">
|
||||
<path android:fillColor="#FF000000" android:pathData="M27 45a18 18 0 1 1 36 0 18 18 0 1 1-36 0" android:strokeWidth="4"/>
|
||||
<path android:pathData="M31.738 45a13.262 13.262 0 1 1 26.524 0 13.262 13.262 0 1 1-26.524 0" android:strokeWidth="4" android:fillColor="#ffffff"/>
|
||||
<path android:fillColor="#FF000000" android:pathData="M58.262 45c0 7.324-5.938 13.262-13.262 13.262-7.324 0-13.262-5.938-13.262-13.262 0-7.324 5.938-13.262 13.262-13.262 7.324 0 13.262 5.938 13.262 13.262zM45 27c-9.941 0-18 8.059-18 18s8.059 18 18 18 18-8.059 18-18-8.059-18-18-18z" android:strokeWidth="4"/>
|
||||
<path android:pathData="M54.41 45c0 2.303-0.827 4.412-2.201 6.048V38.952c1.374 1.636 2.201 3.745 2.201 6.048zM45 54.41c-0.27 0-0.537-0.012-0.801-0.034V35.624C44.463 35.602 44.73 35.59 45 35.59s0.537 0.012 0.801 0.034v18.752C45.537 54.398 45.27 54.41 45 54.41zM35.59 45c0-2.303 0.827-4.412 2.201-6.048v12.096C36.417 49.412 35.59 47.303 35.59 45zm13.415 8.517c-0.511 0.241-1.046 0.437-1.602 0.583V35.9c0.556 0.146 1.091 0.342 1.602 0.583v17.034zM42.597 54.1c-1.173-0.309-2.257-0.839-3.204-1.542V37.442c0.947-0.703 2.031-1.233 3.204-1.542v18.2z" android:strokeWidth="4" android:fillColor="#000000"/>
|
||||
</vector>
|
||||
|
||||
@@ -17,6 +17,6 @@
|
||||
style="@style/FAB"
|
||||
android:id="@+id/scan"
|
||||
android:contentDescription="@string/scan_code"
|
||||
android:src="@drawable/ic_action_search"/>
|
||||
android:src="@drawable/ic_action_next"/>
|
||||
</android.support.design.widget.CoordinatorLayout>
|
||||
</merge>
|
||||
|
||||
@@ -13,6 +13,17 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
<EditText
|
||||
android:id="@+id/label"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:gravity="start|top"
|
||||
android:hint="@string/label"
|
||||
android:imeOptions="flagNoExtractUi"
|
||||
android:importantForAutofill="no"
|
||||
android:inputType="text"
|
||||
tools:ignore="UnusedAttribute"/>
|
||||
<EditText
|
||||
android:id="@+id/content"
|
||||
android:layout_width="match_parent"
|
||||
|
||||
@@ -17,11 +17,32 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:text="@string/format"/>
|
||||
<Spinner
|
||||
android:id="@+id/format"
|
||||
android:text="@string/content"/>
|
||||
<EditText
|
||||
tools:targetApi="o"
|
||||
android:id="@+id/content"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="start|top"
|
||||
android:typeface="monospace"
|
||||
android:imeOptions="flagNoExtractUi"
|
||||
android:inputType="textMultiLine"
|
||||
android:hint="@string/input_content_here"
|
||||
android:importantForAutofill="no"/>
|
||||
<CheckBox
|
||||
android:id="@+id/unescape"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:text="@string/unescape"/>
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:text="@string/format"/>
|
||||
<Spinner
|
||||
style="@style/Spinner"
|
||||
android:id="@+id/format"
|
||||
android:layout_marginBottom="16dp"/>
|
||||
<TextView
|
||||
android:id="@+id/error_correction_label"
|
||||
@@ -30,47 +51,56 @@
|
||||
android:layout_marginBottom="8dp"
|
||||
android:text="@string/error_correction_level"/>
|
||||
<Spinner
|
||||
style="@style/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"/>
|
||||
android:layout_marginBottom="16dp"/>
|
||||
<TextView
|
||||
android:id="@+id/colors_label"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:text="@string/size"/>
|
||||
android:text="@string/colors"/>
|
||||
<Spinner
|
||||
style="@style/Spinner"
|
||||
android:id="@+id/colors"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:entries="@array/colors"/>
|
||||
<TextView
|
||||
android:id="@+id/size_display"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:gravity="center"/>
|
||||
android:layout_marginBottom="8dp"/>
|
||||
<SeekBar
|
||||
style="@style/Widget.AppCompat.SeekBar.Discrete"
|
||||
android:id="@+id/size_bar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:paddingTop="16dp"
|
||||
android:paddingBottom="16dp"
|
||||
android:progress="4"
|
||||
android:max="7"/>
|
||||
<TextView
|
||||
<LinearLayout
|
||||
android:id="@+id/margin"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:text="@string/content"/>
|
||||
<EditText
|
||||
tools:targetApi="o"
|
||||
android:id="@+id/content"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:gravity="start|top"
|
||||
android:typeface="monospace"
|
||||
android:imeOptions="flagNoExtractUi"
|
||||
android:inputType="textMultiLine"
|
||||
android:hint="@string/input_content_here"
|
||||
android:importantForAutofill="no"/>
|
||||
android:orientation="vertical"
|
||||
android:visibility="gone">
|
||||
<TextView
|
||||
android:id="@+id/margin_display"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="8dp"/>
|
||||
<SeekBar
|
||||
style="@style/Widget.AppCompat.SeekBar.Discrete"
|
||||
android:id="@+id/margin_bar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:paddingTop="16dp"
|
||||
android:paddingBottom="16dp"
|
||||
android:max="32"/>
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
</de.markusfisch.android.binaryeye.widget.ConfinedScrollView>
|
||||
<android.support.design.widget.CoordinatorLayout
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
<FrameLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
<ListView
|
||||
android:id="@+id/suggestions"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:choiceMode="multipleChoice"
|
||||
android:clipToPadding="false"
|
||||
android:divider="@color/separator"
|
||||
android:dividerHeight="1px"/>
|
||||
<android.support.design.widget.CoordinatorLayout
|
||||
android:id="@+id/inset_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
<TextView
|
||||
android:visibility="gone"
|
||||
android:id="@+id/no_suggestions"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:gravity="center"
|
||||
android:padding="32dp"
|
||||
android:text="@string/clear_network_suggestions_nothing_to_remove"/>
|
||||
<android.support.design.widget.FloatingActionButton
|
||||
style="@style/FAB"
|
||||
android:id="@+id/remove"
|
||||
android:contentDescription="@string/remove_suggestion"
|
||||
android:src="@drawable/ic_action_remove"/>
|
||||
</android.support.design.widget.CoordinatorLayout>
|
||||
</FrameLayout>
|
||||
@@ -13,7 +13,7 @@
|
||||
material:showAsAction="ifRoom"/>
|
||||
<item
|
||||
android:id="@+id/pick_file"
|
||||
android:title="@string/pick_file"
|
||||
android:title="@string/pick_image_file"
|
||||
android:icon="@drawable/ic_action_pick_file"
|
||||
material:showAsAction="ifRoom"/>
|
||||
<item
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
<menu
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:material="http://schemas.android.com/apk/res-auto">
|
||||
<item
|
||||
android:id="@+id/toggle_free"
|
||||
android:title="@string/toggle_free_rotation"
|
||||
android:icon="@drawable/ic_action_rotation_unlocked"
|
||||
material:showAsAction="ifRoom"/>
|
||||
<item
|
||||
android:id="@+id/rotate"
|
||||
android:title="@string/rotate_image_cw"
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
<menu
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:material="http://schemas.android.com/apk/res-auto">
|
||||
<item
|
||||
android:id="@+id/add_to_history"
|
||||
android:title="@string/add_to_history"
|
||||
android:icon="@drawable/ic_action_history_add"
|
||||
material:showAsAction="ifRoom"/>
|
||||
<item
|
||||
android:id="@+id/copy_to_clipboard"
|
||||
android:title="@string/copy_to_clipboard"
|
||||
|
||||
@@ -23,6 +23,11 @@
|
||||
android:title="@string/share"
|
||||
android:icon="@drawable/ic_action_share"
|
||||
material:showAsAction="ifRoom"/>
|
||||
<item
|
||||
android:id="@+id/share_file"
|
||||
android:title="@string/share_file"
|
||||
android:icon="@drawable/ic_action_share"
|
||||
material:showAsAction="ifRoom"/>
|
||||
<item
|
||||
android:id="@+id/create"
|
||||
android:title="@string/compose_barcode"
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
<menu
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:material="http://schemas.android.com/apk/res-auto">
|
||||
<item
|
||||
android:id="@+id/pick_file"
|
||||
android:title="@string/pick_file"
|
||||
android:icon="@drawable/ic_action_pick_file"
|
||||
material:showAsAction="ifRoom"/>
|
||||
</menu>
|
||||
@@ -1,23 +1,25 @@
|
||||
<menu
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:material="http://schemas.android.com/apk/res-auto">
|
||||
<group android:id="@+id/scans_available"
|
||||
android:visible="false">
|
||||
<item
|
||||
android:id="@+id/search"
|
||||
android:icon="@drawable/ic_action_search"
|
||||
android:title="@string/search_scan"
|
||||
material:actionViewClass="android.support.v7.widget.SearchView"
|
||||
material:showAsAction="collapseActionView|always"/>
|
||||
<item
|
||||
android:id="@+id/export_history"
|
||||
android:icon="@drawable/ic_action_export"
|
||||
android:title="@string/export_to_file"
|
||||
material:showAsAction="ifRoom"/>
|
||||
<item
|
||||
android:id="@+id/clear"
|
||||
android:icon="@drawable/ic_action_remove"
|
||||
android:title="@string/clear_history"
|
||||
material:showAsAction="ifRoom"/>
|
||||
</group>
|
||||
<item
|
||||
android:id="@+id/search"
|
||||
android:icon="@drawable/ic_action_search"
|
||||
android:title="@string/search_scan"
|
||||
material:actionViewClass="android.support.v7.widget.SearchView"
|
||||
material:showAsAction="collapseActionView|always"/>
|
||||
<item
|
||||
android:id="@+id/export_history"
|
||||
android:icon="@drawable/ic_action_export"
|
||||
android:title="@string/export_to_file"
|
||||
material:showAsAction="ifRoom"/>
|
||||
<item
|
||||
android:id="@+id/share_as_file"
|
||||
android:icon="@drawable/ic_action_share"
|
||||
android:title="@string/share_file"
|
||||
material:showAsAction="ifRoom"/>
|
||||
<item
|
||||
android:id="@+id/clear"
|
||||
android:icon="@drawable/ic_action_remove"
|
||||
android:title="@string/clear_history"
|
||||
material:showAsAction="ifRoom"/>
|
||||
</menu>
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
android:icon="@drawable/ic_action_copy"
|
||||
material:showAsAction="ifRoom"/>
|
||||
<item
|
||||
android:id="@+id/edit_scan"
|
||||
android:title="@string/edit_scan"
|
||||
android:id="@+id/edit_label"
|
||||
android:title="@string/edit_label"
|
||||
android:icon="@drawable/ic_action_edit"
|
||||
material:showAsAction="ifRoom"/>
|
||||
<item
|
||||
|
||||
|
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 1.7 KiB |