Compare commits

...
115 Commits
Author SHA1 Message Date
Markus Fisch 7546cf25c0 Advance version number to 1.74.1 2026-06-09 21:40:48 +02:00
Markus Fisch bedb0a81d6 Update link to BinaryReceptor 2026-06-09 21:40:22 +02:00
Markus Fisch 658988525a Replace vector drawable background wallpaper
With a PNG because the vector drawable apparently causes
rendering issues on MediaTek devices.

See issue #655.
2026-06-09 19:40:41 +02:00
Markus Fisch 8298ce70ef Allow scanning while device is locked
Show the camera and newly scanned results over the lock screen
while keeping history, media selection, and settings protected.
2026-06-08 20:21:05 +02:00
Markus Fisch ae29a4d207 Advance version number to 1.74.0 2026-06-05 20:46:40 +02:00
Markus Fisch 7fe8126703 Run tests for bundle and release targets 2026-06-05 19:04:40 +02:00
Markus Fisch 4adce7c958 Fix splash lifecycle crash
Use a plain Activity for SplashActivity because it only redirects
to the camera screen and finishes immediately.

This avoids AppCompat lifecycle dispatch crashing on older Android
versions when the activity is destroyed before AndroidX moves it
out of INITIALIZED.
2026-06-05 18:20:07 +02:00
Markus Fisch d11dac7f13 Add content type in history rows
And use some of the parsed content for the content text view.
2026-06-05 18:20:07 +02:00
Markus Fisch f90aaac4f9 Show parsed content types in history
Store content types and use it in the history.
2026-06-05 18:20:07 +02:00
Markus Fisch d5ffcabdd2 Rename "Parsed Data" to "Encoded Data"
Which is a better description.
2026-06-04 19:36:01 +02:00
Markus Fisch 52ca14c9ba Move label input to the top of the screen
Which groups the parsed and the raw content together.
2026-06-04 19:30:23 +02:00
Markus Fisch 6c2c7bef90 Add a section header for meta data 2026-06-04 19:13:51 +02:00
Markus Fisch f285e1d200 Add an addField() extension
To simplify all this repetetive `items.add(Field(…))` calls.
2026-06-05 11:53:26 +02:00
Markus Fisch d7e687871c Make title of parsed data a data item
Which is more structured than using it as a headline/label.

Also simplify using the last parsed data when the user edits
the source.
2026-06-05 11:52:27 +02:00
Markus Fisch 5472993e19 Copy hex data from hex dump
Add a tap handler to the hex dump so binary content can be copied
as a continuous hex string directly from the decode screen.
2026-06-04 18:03:29 +02:00
Markus Fisch 702dc75ddd Show open icon for tracking links
Add the open icon to parsed data values that open an external
link so tracking numbers visibly indicate that tapping them opens
the tracking website.
2026-06-04 18:01:06 +02:00
Markus Fisch 45054c80f8 Avoid crop handle overlapping FAB
Cap the default crop handle position at the FAB-safe home
position so square displays do not place it behind the action
button.
2026-06-04 17:53:45 +02:00
bovirusandGitHub 5a899b5eb3 Update Italian language 2026-06-03 18:16:44 +02:00
Markus Fisch 1d002b645d Update ZXingCpp library to v3.0.2.7 2026-06-02 23:16:15 +02:00
Markus Fisch 456da10f84 Complete missing translations 2026-06-02 22:38:09 +02:00
Markus Fisch 7fbf89380c Use default onClick handler for tracking numbers
So the HTML link doesn't interfere with the copy to clipboard
handling.
2026-06-02 00:19:50 +02:00
Markus Fisch 91a549e96e Make Deutsche Post tracking link a data item 2026-06-02 00:12:09 +02:00
Markus Fisch 3ef6f874b8 Skip parsing data if it's a tracking link
If we already know the data contains a tracking link we
don't need to parse the data for something else.

Unfortunately, the Deutsche Post Matrixcode stamp also
parses as an IDL (International Driver License).
2026-06-01 23:55:16 +02:00
Markus Fisch 45661b75ba Use on-surface color for detector overlay
Match the detector overlay to the theme color used by default
for text and icons.
2026-06-01 23:34:40 +02:00
Markus Fisch cd30a86c69 Fix detector center coordinates
Use view-local coordinates for the detector center because
its drawing, touch handling, and crop ROI math all operate
in the view's own coordinate space.

This avoids offset crop overlays when the view is not laid
out at the parent's origin.
2026-06-01 23:23:30 +02:00
Markus Fisch 13eabaee19 Add an option to choose a custom beep tone
As requested in issue #642.
2026-06-01 22:39:11 +02:00
Markus Fisch 2035c935dc Use consts in PreferencesFragment
Instead of duplicated string literals.
2026-06-01 22:36:20 +02:00
Markus Fisch 1ff170a279 Fix FAB positioning
FAB placement was wrong for the traditional three-button
navigation after the Material You migration.

The bottom inset was applied twice.
2026-05-31 21:57:52 +02:00
Markus Fisch 4fab21c88d Support fido:/ URIs
Recognize spec-style fido:/ URIs and open them with ACTION_VIEW.

Note that FIDO:// URIs already worked before this just fine.
2026-05-31 21:46:56 +02:00
Markus Fisch 1ef9f39978 Remove appendUrlArguments tests
Since appendUrlArguments doesn't exist anymore.
2026-05-31 21:45:02 +02:00
NathanandGitHub 5378e50fe9 Update french translation 2026-05-31 19:01:59 +02:00
Markus Fisch c325713a8c Add a preference to set a custom scanner ID
So a device can be identified if desired.
2026-05-28 21:31:38 +02:00
bovirusandMarkus Fisch a043fba948 Update Italian translation 2026-05-27 21:35:53 +02:00
Markus Fisch 386f882944 Advance version number to 1.73.1 2026-05-27 21:28:49 +02:00
Markus Fisch 65979cbddd Fix stale history row highlighting
Remove ListView choice mode so regular history row taps
no longer persist as checked state after returning from
the scan detail screen.
2026-05-27 21:23:44 +02:00
Markus Fisch 26747d709f Update vdstools library 2026-05-27 21:20:19 +02:00
Markus Fisch 22b6a9f66e Add a note to not update the material library yet
Because the latest version would require minSDK 23, which
is silly, but the usual nonsense from Google, unfortunately.
2026-05-27 21:19:00 +02:00
Markus Fisch ded532c523 Update ZXing-C++ and support Telepen format 2026-05-27 21:17:48 +02:00
Markus Fisch 17c49a4a27 Advance version number to 1.73.0 2026-05-26 23:06:19 +02:00
Markus Fisch 7ff8db0880 Fix deeplink argument parsing
Merge intent extras with parsed deeplink query parameters
in EncodeActivity so app links populate content and format
even when Android supplies an empty extras bundle.

This fixes opened /encode links showing an empty content
field.
2026-05-26 22:48:10 +02:00
Markus Fisch d7837f5795 Migrate to Material You and support dynamic colors
And add a preference toggle to use the original colors
if a user likes that better.
2026-05-26 22:28:13 +02:00
Markus Fisch 1f5c77e802 Add preference to toggle tracking param stripping
Make the URL tracking parameter sanitization introduced opt-out
rather than always-on.
2026-05-26 21:35:58 +02:00
Markus Fisch a4da488a41 Refactor "execShareIntent" to "startIntentOrToast"
A better description of what it does.
2026-05-26 21:12:09 +02:00
Markus Fisch d177c08420 Refactor "fired" to "executed"
Which is a better fit, because we already have an "execute"
function.
2026-05-26 21:07:52 +02:00
Markus Fisch a6123ed233 Optimize imports
With Android Studio.
2026-05-26 21:07:17 +02:00
Markus Fisch 2ebb376a8b Remove redundant let scope function 2026-05-26 21:06:45 +02:00
Markus Fisch 6cac7e8ead Simplify sanitizing URIs
Make stripTrackingParams a top-level extension function,
which is a bit simpler and cleaner in my opinion.
2026-05-26 21:04:42 +02:00
Ashutosh TiwariandGitHub 59f6c9f654 Strip common tracking parameters before opening scanned URLs
Many QR codes in the wild contain analytics tracking parameters
(utm_*, fbclid, gclid, mc_*, _ga, igshid, …) that uniquely identify
the scanning user/session. These add nothing for the user and are
silently propagated to the destination site when openUri() launches
ACTION_VIEW.

Add a tiny content/UrlSanitizer helper that filters a curated set of
~30 well-known tracker parameter names from http(s) URIs, and call it
from openUri() so cleanup happens transparently on every URL launch.

The list is intentionally conservative: only parameters whose sole
purpose is third-party attribution. Functional/site parameters
(q, id, v, page, search, …) are left untouched. Non-http(s) URIs and
URIs without a query string are returned as-is, so there's no
behavioral change for tel:, mailto:, geo:, content:, etc.
2026-05-26 18:19:35 +02:00
Markus Fisch f0c1d2a936 Add a note for cameraX and core-ktx
To stay on these versions because the next ones won't
support minSDK 21 anymore.

Idiots at keyboards  ̄\_(ツ)_/ ̄
2026-05-11 21:54:35 +02:00
Markus Fisch e7aff150f6 Update bouncycastle library 2026-05-11 21:53:11 +02:00
Markus Fisch 10755555ff Update vdstools library 2026-05-11 21:53:01 +02:00
Markus Fisch a78a85df2c Update kotlinx-datetime library 2026-05-11 21:51:46 +02:00
Markus Fisch de9c490429 Update kotlin coroutines library 2026-05-11 21:51:17 +02:00
Markus Fisch 77e686af75 Update tools version 2026-05-11 21:46:14 +02:00
Markus Fisch 0ae13f22b6 Complete missing Latvian translations 2026-05-11 21:43:34 +02:00
Too Late (bot)andGitHub bedfcdec14 Translations update from Toolate 2026-05-11 21:37:34 +02:00
bovirusandGitHub a6ba968ea8 Italian language update 2026-05-06 17:21:28 +02:00
Markus Fisch 5301da5900 Add a blank line between parsers
Easier to read.
2026-05-05 17:03:42 +02:00
Markus Fisch cb7d64991b Parse and display otpauth:// code fields
Show type, account, issuer, algorithm, digits, and
period/counter when scanning an OTP code.
2026-05-05 17:03:42 +02:00
Markus Fisch 6194dc29d5 Update tools version 2026-05-05 17:03:42 +02:00
bovirusandGitHub 3fa39b9fe0 Update Italian language 2026-04-24 21:54:41 +02:00
Markus Fisch f37b846241 Advance version number to 1.72.2 2026-04-23 23:40:35 +02:00
Markus Fisch 8ce33c0071 Add predefined expert and simple profiles
So users can always switch between these profiles.
2026-04-23 18:28:51 +02:00
Markus Fisch e6823386b0 Move setCovered to Mapping.kt
It belongs alongside the other frame/view coordinate
helpers (setFrameRoi, setFrameToView).
2026-04-22 23:20:26 +02:00
Markus Fisch 3e57a59e16 Avoid shared camera mapping state
Move ROI and frame mapping data out of shared mutable activity
fields.

Capture view geometry on the UI thread, then build per-frame
mapping data inside the analyzer so decoding, crosshair checks,
and overlay updates use a consistent snapshot.
2026-04-22 22:58:07 +02:00
Markus Fisch 758220200e Fix isSilent() for media stream
Ringer mode is irrelevant for STREAM_MUSIC. Audio plays in
vibrate mode regardless. Check stream volume instead, so beeps
are only suppressed when the media volume is actually zero.
2026-04-22 22:42:59 +02:00
Markus Fisch b3584392b3 Set share QR listener once per ViewHolder
Not per bind. Allocating a new lambda on every getView()
call is wasteful.
2026-04-22 22:12:36 +02:00
Markus Fisch 3bb26f06d1 Import/export preferences using QR codes
Any profile can be exported/imported via a special QR code now.
2026-04-23 16:11:31 +02:00
Markus Fisch fe4a021cd0 Fix CameraX preview teardown leak 2026-04-22 22:02:59 +02:00
Markus Fisch 0de5565b78 Update tools version 2026-04-20 22:04:04 +02:00
Markus Fisch d9a9acd8c1 Update ZXingCpp library to v3.0.2.5 2026-04-21 13:01:01 +02:00
Markus Fisch 1be31f71ed Update ZXingCpp library to v3.0.2.4 2026-04-21 13:00:58 +02:00
Markus Fisch 8859c61119 Advance version number to 1.72.1 2026-04-11 13:06:04 +02:00
Andrew GunnersonandGitHub c52778f59d Fix crash on upgrades due to missing DATA_BAR entry
Commit 73047cdee6 merged the old
migrateToNativeFormatNames() and migrateBarcodeFormatName() mapping
functions, but the new map was based on the former, which happened to be
missing the entry for DATA_BAR.

Fixes: #636

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2026-04-11 12:53:08 +02:00
bovirusandGitHub 5bde3947e3 Italian language update 2026-04-10 22:19:16 +02:00
Markus Fisch 08be31814f Advance version number to 1.72.0 2026-04-09 23:05:29 +02:00
Markus Fisch c5db04b84e Add headlines for result sections
Makes result sections easier to digest.
2026-04-09 21:16:27 +02:00
Markus Fisch 9dccc6a7f5 Fix EPC QR parser tests 2026-04-09 16:46:20 +02:00
Markus Fisch a5deab656b Clamp invalid ROIs to prevent crashes
Fixes a crash path where rounding in the view-to-frame
mapping could produce crop rectangles outside the Y plane.
2026-04-09 16:46:20 +02:00
Markus Fisch b773dfa392 Remove double signature date item for IDB codes 2026-04-09 16:46:20 +02:00
Markus Fisch 7b5106f09d Parse JSON content and show pretty-printed dump 2026-04-09 16:46:16 +02:00
Markus Fisch 16ce45093e Use list selector for parsed data items
So there is some touch feedback.
2026-04-09 19:47:44 +02:00
Markus Fisch e6345f09b3 Reformat code 2026-04-09 12:59:04 +02:00
Markus Fisch de00ed779a Update vdstools to latest version
And migrate the code to use the public API only.

This version doesn't include the Kermit logging library
which would have required a minSdk raise.
2026-04-09 10:58:34 +02:00
Markus Fisch d740aa121b Advance version number to 1.71.0 2026-04-08 21:56:07 +02:00
Markus Fisch 38c40d36bb Preserve parsed data while editing
Keep the last successfully parsed data rows visible in the decode
view while the barcode content is being edited. Avoids jumping of
the layout.
2026-04-08 19:49:25 +02:00
Markus Fisch 3e51f70c7c Decode nested IDB messages
Flatten nested IDB messages into seal fields and decode child values
using the schema-defined coding from vdstools. This fixes nested
content such as MRZ fields inside IDB container messages being shown
as raw byte counts instead of decoded text.
2026-04-08 19:11:26 +02:00
Markus Fisch 502702c7a5 Remove unused indent fields 2026-04-08 19:10:03 +02:00
Markus Fisch 96ff9b6239 Change data table to a label/value list
So values have more space and can be read more easily.
2026-04-08 19:10:03 +02:00
Markus Fisch afd6c74c80 Verify IDB barcodes
Using the german CSCA master list.
2026-04-08 19:09:52 +02:00
Markus Fisch 5ffea2edd8 Fix detecting web URLs with a subdomain 2026-04-08 19:50:05 +02:00
Markus Fisch 78d62395e2 Add missing translations for IDL/EPC labels 2026-04-08 19:38:33 +02:00
Markus Fisch a5d1465528 Remove unnecessary EpcQrInfo wrapper class
And return the map directly.
2026-04-08 19:16:19 +02:00
Markus Fisch 14517bad17 Add vdstools to parse VDS and IDB barcodes
Without verifying yet.
2026-04-08 19:14:15 +02:00
Markus Fisch 7a5e07fb3c Handle deep link in onNewIntent for EncodeActivity
Calendar apps reuse the existing task instead of creating a
new one, so Android calls onNewIntent() rather than onCreate().
Without an onNewIntent() override the intent's query parameters
(format, content, execute) were silently ignored.

Extract intent application into applyIntent() and call it from
both onCreate() and a new onNewIntent() override.
2026-04-07 20:11:52 +02:00
Markus Fisch c6b3ca08f4 Restrict scans to horizontal crosshair
Only accept camera scan results that intersect the horizontal
crosshair when crosshairs are enabled. For 1D linear barcodes,
expand collapsed position lines into a narrow band first so the
same rule works reliably for formats that do not report full
symbol bounds.
2026-04-07 21:34:26 +02:00
Markus Fisch 73047cdee6 Simplify old to new format name translation
To have that list only once.
2026-04-07 19:23:56 +02:00
Markus Fisch bec4aa3120 Replace Code39 with the more specific Code39Std
So users get only Code39Std when they restrict reading to
this format.
2026-04-07 18:17:25 +02:00
Markus Fisch eaaaaf14c2 Add barcode format descriptions
To DecodeActivity and EncodeActivity.

Enables/Disables with meta data.
2026-04-07 20:59:10 +02:00
Markus Fisch 971742aff4 Fix barcode format preference summary
Sort the selected barcode formats alphabetically in the
preferences summary and map stored values back to their
display labels.
2026-04-07 15:46:47 +02:00
Markus Fisch 28915eacdb Simplify setting preference summaries 2026-04-06 23:37:35 +02:00
Markus Fisch a2c1d11b5b Include barcode format in history search
So we can use for barcode formats too.
2026-04-06 23:09:04 +02:00
Markus Fisch e9dd396f34 Add a dedicated barcode icon for PDF417 2026-04-07 12:02:08 +02:00
Markus Fisch c7edba746c Advance version number to 1.70.0 2026-04-03 23:39:44 +02:00
Markus Fisch 34c8dfd169 Add barcode format icons in history 2026-04-03 23:34:45 +02:00
bovirusandGitHub 459980899f Italian language update 2026-04-03 23:10:31 +02:00
Markus Fisch 2e0693cb45 Add beep audio stream preference
Let users choose whether confirmation beeps use the media or
notification audio stream, with media as the default.

Recreate the tone generator when the selected stream changes so the
new preference takes effect immediately, and add the required
localized labels for the new setting.
2026-04-03 12:00:14 +02:00
Markus Fisch 187825932e Parse EPC-QR codes and show the data 2026-04-01 21:30:51 +02:00
Markus Fisch a80802b4bc Improve layout of decode screen
Move the things a user cares most about to the top of the
screen, and everything else to the bottom.
2026-04-01 20:58:02 +02:00
Markus Fisch aa589a5d53 Improve size of generated PDF417 barcodes
PDF417 requires a bit more horizontal width because it
has many modules and doesn't fit into a square shape.
2026-04-01 20:20:46 +02:00
Markus Fisch 6a42e54f0b Fix parsing first field of subfile in IDLParser
Per the AAMVA spec, each subfile in the data section starts with its
2-char type marker (e.g. "ZV", "ZI") directly prepended to the first
field, with no separator between them. The key-value parser had no
awareness of this, so the marker was absorbed into the field key
(e.g. "ZVA" was parsed as "ZVZ" with value "VA01" instead of "01").
2026-04-01 19:55:30 +02:00
Markus Fisch e134b18bba Use quiet zone from library
Which uses zint for linear barcodes but avoids it for all
other types because zint doesn't generate reasonable margins
for other barcodes.

This fixes too thin margins for linear barcodes.
2026-03-31 22:27:59 +02:00
bovirusandGitHub a112484dcb Italian language update 2026-03-27 12:16:26 +01:00
Markus Fisch 4f36030e38 Advance version number to 1.69.1 2026-03-26 19:35:24 +01:00
Yurt PageandMarkus Fisch 90c2c03d12 Update translations 2026-03-26 19:30:24 +01:00
199 changed files with 7982 additions and 1588 deletions
+63
View File
@@ -1,5 +1,68 @@
# Change Log
## 1.74.1
* Allow scanning while device is locked
* Fix rendering bug for some GPUs
## 1.74.0
* Add an option to choose a custom beep tone
* Add an option to set a custom scanner ID
* Improve display of content type
* Support fido:/ URIs
* Update Italian and French translations
* Update ZXingCpp
* Fix FAB and crop handle positioning
* Fix splash screen for some Android versions
## 1.73.1
* Update ZXingCpp and support Telepen format
* Update vdstools
* Fix stale history row highlighting
## 1.73.0
* Support Material You and Dynamic Colors
* Strip common tracking parameters before opening scanned URLs
* Parse and display otpauth:// code fields
* Update Italian and Latvian language
* Update BouncyCastle and vdstools library
* Fix deeplink argument parsing
## 1.72.2
* Import/export preferences using QR codes
* Add predefined expert and simple profiles
* Update ZXingCpp library
* Fix beeping on scans
## 1.72.1
* Fix migrating DATA_BAR entries
* Italian language update
## 1.72.0
* Improve result screen
* Update vdstools
* Fix camera initialization
## 1.71.0
* Parse VDS and IDB barcodes
* Restrict scanning to horizontal crosshair if set
* Include barcode format in history search
* Add a dedicated barcode icon for PDF417
* Add barcode format descriptions
* Fix handling deep links
* Fix detecting web URLs
## 1.70.0
* Add barcode format icons in history
* Add beep audio stream preference
* Parse EPC-QR codes and show the data
* Improve layout of decode screen
* Fix parsing first field of subfile in IDLParser
* Use quiet zone from ZXingCpp
* Italian language update
## 1.69.1
* Update translations
## 1.69.0
* Add an option to show crosshairs
* Add preference for codes that should not be saved
+2 -2
View File
@@ -5,10 +5,10 @@ all: debug install start
debug:
./gradlew assembleDebug
release: lint
release: lint test
./gradlew assembleRelease
bundle: lint
bundle: lint test
./gradlew bundleRelease
lint:
+3 -1
View File
@@ -59,6 +59,7 @@ ZXing can read the following barcode formats:
* [ITF][itf]
* [MAXICODE][maxicode] (partial)
* [PDF417][pdf417]
* [Telepen][telepen]
* [QR CODE][qr_code]
* [Micro QR Code][micro_qr_code]
* [rMQR Code][rmqr_code]
@@ -186,7 +187,7 @@ In order to set up Bluetooth forwarding to a computer:
1. Pair your devices (phone and pc) if not paired already
2. Download and run a companion app:
- Windows: https://github.com/KamaleiZestri/BinaryReceptorWindows
- Windows: https://github.com/KamaleiZestri/BinaryReceptor
- Linux: https://github.com/sean666888/bin_eye_bt_receiver
3. In the settings of Binary Eye, enable "Forward scans with Bluetooth"
and select the appropriate target host device that is running the
@@ -207,6 +208,7 @@ In order to set up Bluetooth forwarding to a computer:
[itf]: https://en.wikipedia.org/wiki/Interleaved_2_of_5
[maxicode]: https://en.wikipedia.org/wiki/MaxiCode
[pdf417]: https://en.wikipedia.org/wiki/PDF417
[telepen]: https://en.wikipedia.org/wiki/Telepen
[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
+14 -10
View File
@@ -8,8 +8,8 @@ android {
minSdk 21
targetSdk sdk_version
versionCode 162
versionName '1.69.0'
versionCode 172
versionName '1.74.1'
}
signingConfigs {
@@ -67,21 +67,25 @@ android {
dependencies {
// Kotlin
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.10.2"
implementation "androidx.core:core-ktx:1.17.0"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.11.0"
implementation 'org.jetbrains.kotlinx:kotlinx-datetime:0.8.0'
implementation "androidx.core:core-ktx:1.17.0" // Keep for minSDK 21.
// Support
implementation "androidx.appcompat:appcompat:1.7.1"
implementation "com.google.android.material:material:1.13.0"
implementation "com.google.android.material:material:1.13.0" // Keep for minSDK 21.
implementation "androidx.preference:preference:1.2.1"
// External
implementation 'androidx.camera:camera-core:1.4.2'
implementation 'androidx.camera:camera-camera2:1.4.2'
implementation 'androidx.camera:camera-lifecycle:1.4.2'
implementation 'androidx.camera:camera-view:1.4.2'
var camerax_version = '1.4.2' // Keep for minSDK 21.
implementation "androidx.camera:camera-core:$camerax_version"
implementation "androidx.camera:camera-camera2:$camerax_version"
implementation "androidx.camera:camera-lifecycle:$camerax_version"
implementation "androidx.camera:camera-view:$camerax_version"
implementation 'com.github.markusfisch:ScalingImageView:1.4.3'
implementation 'com.github.markusfisch:zxing-cpp:v3.0.2.1'
implementation 'com.github.markusfisch:zxing-cpp:v3.0.2.7'
implementation 'de.tsenger:vdstools:0.17.0'
implementation 'org.bouncycastle:bcpkix-jdk15to18:1.84'
// Testing
testImplementation 'junit:junit:4.13.2'
+2
View File
@@ -3,3 +3,5 @@
-dontwarn android.support.v8.**
-keep class android.support.v7.widget.SearchView { *; }
-keep class de.tsenger.vdstools.** { *; }
Binary file not shown.
@@ -2,15 +2,15 @@ package de.markusfisch.android.binaryeye.actions
import android.content.Context
import android.content.Intent
import de.markusfisch.android.binaryeye.content.execShareIntent
import de.markusfisch.android.binaryeye.content.openUrl
import de.markusfisch.android.binaryeye.content.startIntentOrToast
import de.markusfisch.android.binaryeye.widget.toast
abstract class Action {
abstract val iconResId: Int
abstract val titleResId: Int
var fired: Boolean = false
var executed: Boolean = false
abstract fun canExecuteOn(data: ByteArray): Boolean
abstract suspend fun execute(context: Context, data: ByteArray)
@@ -24,7 +24,7 @@ abstract class IntentAction : Action() {
if (intent == null) {
context.toast(errorMsg)
} else {
fired = context.execShareIntent(intent)
executed = context.startIntentOrToast(intent)
}
}
@@ -52,6 +52,6 @@ abstract class SchemeAction : Action() {
}
final override suspend fun execute(context: Context, data: ByteArray) {
fired = context.openUrl(String(data))
executed = context.openUrl(String(data))
}
}
@@ -1,5 +1,6 @@
package de.markusfisch.android.binaryeye.actions
import de.markusfisch.android.binaryeye.actions.fido.FidoAction
import de.markusfisch.android.binaryeye.actions.mail.MailAction
import de.markusfisch.android.binaryeye.actions.mail.MatMsgAction
import de.markusfisch.android.binaryeye.actions.otpauth.OtpauthAction
@@ -17,6 +18,7 @@ object ActionRegistry {
private val REGISTRY: Set<Action> = setOf(
MailAction,
MatMsgAction,
FidoAction,
OtpauthAction,
SmsAction,
TelAction,
@@ -0,0 +1,23 @@
package de.markusfisch.android.binaryeye.actions.fido
import android.content.Context
import de.markusfisch.android.binaryeye.R
import de.markusfisch.android.binaryeye.actions.Action
import de.markusfisch.android.binaryeye.content.openUrl
object FidoAction : Action() {
private val fidoUriRegex = "^fido:/[0-9]+$".toRegex(
RegexOption.IGNORE_CASE
)
override val iconResId: Int = R.drawable.ic_action_open
override val titleResId: Int = R.string.open_url
override fun canExecuteOn(data: ByteArray): Boolean {
return String(data).trim().matches(fidoUriRegex)
}
override suspend fun execute(context: Context, data: ByteArray) {
executed = context.openUrl(String(data).trim())
}
}
@@ -15,7 +15,7 @@ object OpenOrSearchAction : Action() {
override fun canExecuteOn(data: ByteArray): Boolean = false
override suspend fun execute(context: Context, data: ByteArray) {
fired = openUrlOrSearch(context, String(data))
executed = openUrlOrSearch(context, String(data))
}
}
@@ -7,7 +7,7 @@ import de.markusfisch.android.binaryeye.content.openUrl
object WebAction : Action() {
private val colloquialRegex =
"^(URL:[ ]*)*(http[s]*://)*[A-Za-z0-9-]{3,}\\.[A-Za-z]{2,}[^ \t\r\n]*$".toRegex()
"^(URL:[ ]*)*(http[s]*://)*[A-Za-z0-9.-]{3,}\\.[A-Za-z]{2,}[^ \t\r\n]*$".toRegex()
override val iconResId: Int = R.drawable.ic_action_open
override val titleResId: Int = R.string.open_url
@@ -24,6 +24,6 @@ object WebAction : Action() {
if (!url.startsWith("http") && !url.startsWith("ftp")) {
url = "http://${url}"
}
fired = context.openUrl(url)
executed = context.openUrl(url)
}
}
@@ -23,12 +23,12 @@ object WifiAction : Action() {
override suspend fun execute(context: Context, data: ByteArray) {
withContext(Dispatchers.IO) {
fired = WifiConnector.parse(String(data))?.let {
executed = WifiConnector.parse(String(data))?.let {
WifiConnector.addNetwork(context, it)
} ?: false
withContext(Dispatchers.Main) {
context.toast(
if (fired) {
if (executed) {
R.string.wifi_added
} else {
R.string.wifi_config_failed
@@ -1,6 +1,5 @@
package de.markusfisch.android.binaryeye.activity
import android.app.AlertDialog
import android.app.Dialog
import android.content.Context
import android.os.Bundle
@@ -12,6 +11,8 @@ import android.widget.ListView
import android.widget.RadioGroup
import android.widget.Spinner
import android.widget.TextView
import androidx.appcompat.app.AlertDialog
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import de.markusfisch.android.binaryeye.R
import de.markusfisch.android.binaryeye.adapter.prettifyFormatName
import de.markusfisch.android.binaryeye.app.prefs
@@ -49,7 +50,7 @@ class AutomatedActionsActivity : AbstractBaseActivity() {
editAction(null)
}
findViewById<View>(R.id.inset_layout).setPaddingFromWindowInsets()
findViewById<View>(R.id.inset_layout).setPaddingFromWindowInsets(bottom = false)
listView.setPaddingFromWindowInsets()
}
@@ -125,7 +126,7 @@ class AutomatedActionsActivity : AbstractBaseActivity() {
typeGroup.check(initialTypeId)
updateTypeSections(initialTypeId)
val dialog = AlertDialog.Builder(this)
val dialog = MaterialAlertDialogBuilder(this)
.setTitle(
if (action == null) {
R.string.automated_action_add
@@ -229,7 +230,7 @@ class AutomatedActionsActivity : AbstractBaseActivity() {
}
private fun confirmRemoveAction(position: Int) {
AlertDialog.Builder(this)
MaterialAlertDialogBuilder(this)
.setMessage(R.string.automated_action_remove_confirm)
.setPositiveButton(android.R.string.ok) { _, _ ->
actions.removeAt(position)
@@ -1,7 +1,6 @@
package de.markusfisch.android.binaryeye.activity
import android.annotation.SuppressLint
import android.app.AlertDialog
import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
@@ -16,6 +15,7 @@ import android.widget.EditText
import android.widget.ImageView
import android.widget.SeekBar
import android.widget.TextView
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import de.markusfisch.android.binaryeye.R
import de.markusfisch.android.binaryeye.app.db
import de.markusfisch.android.binaryeye.app.hasWritePermission
@@ -120,8 +120,8 @@ class BarcodeActivity : AbstractBaseActivity() {
}
}
findViewById<View>(R.id.inset_layout).setPaddingFromWindowInsets()
imageView.doOnApplyWindowInsets { _, insets ->
findViewById<View>(R.id.inset_layout).setPaddingFromWindowInsets(bottom = false)
imageView.doOnApplyWindowInsets { _, insets, _ ->
imageView.insets.set(insets)
}
}
@@ -154,7 +154,7 @@ class BarcodeActivity : AbstractBaseActivity() {
content,
barcodeFormat,
getInt(EC_LEVEL),
getInt(MARGIN),
getBoolean(ADD_QUIET_ZONE),
colors
)
}
@@ -257,7 +257,7 @@ class BarcodeActivity : AbstractBaseActivity() {
action: (FileType) -> Unit
) {
val fileTypes = FileType.entries.toTypedArray()
AlertDialog.Builder(this)
MaterialAlertDialogBuilder(this)
.setTitle(title)
.setItems(fileTypes.map { it.name }.toTypedArray()) { _, which ->
action(fileTypes[which])
@@ -281,7 +281,7 @@ class BarcodeActivity : AbstractBaseActivity() {
editText.setText(
encodeFileName("${barcode.format}_${barcode.content}")
)
AlertDialog.Builder(this)
MaterialAlertDialogBuilder(this)
.setView(view)
.setPositiveButton(android.R.string.ok) { _, _ ->
val fileName = editText.text.toString()
@@ -327,7 +327,7 @@ class BarcodeActivity : AbstractBaseActivity() {
val sizeView = view.findViewById<TextView>(R.id.size_display)
val sizeBarView = view.findViewById<SeekBar>(R.id.size_bar)
sizeBarView.initSizeBar(sizeView)
AlertDialog.Builder(this)
MaterialAlertDialogBuilder(this)
.setView(view)
.setPositiveButton(android.R.string.ok) { _, _ ->
write(getSize(sizeBarView.progress))
@@ -450,7 +450,7 @@ class BarcodeActivity : AbstractBaseActivity() {
private const val CONTENT_RAW = "content_raw"
private const val FORMAT = "format"
private const val EC_LEVEL = "ec_level"
private const val MARGIN = "margin"
private const val ADD_QUIET_ZONE = "add_quiet_zone"
private const val COLORS = "colors"
private const val MIME_PNG = "image/png"
private const val MIME_JPG = "image/jpeg"
@@ -464,9 +464,9 @@ class BarcodeActivity : AbstractBaseActivity() {
): Intent {
val args = Bundle()
when (barcode) {
is ContentBarcode -> args.putEcLevelMargin(
is ContentBarcode -> args.putEcLevelQuietZone(
barcode.ecLevel,
barcode.margin
barcode.addQuietZone
)
is BitMatrixBarcode -> {
@@ -490,12 +490,12 @@ class BarcodeActivity : AbstractBaseActivity() {
content: T,
format: BarcodeFormat,
ecLevel: Int = -1,
margin: Int = 0,
addQuietZone: Boolean = false,
colors: Int = 0
): Intent {
val args = Bundle()
args.putContentFormatColors(content, format, colors)
args.putEcLevelMargin(ecLevel, margin)
args.putEcLevelQuietZone(ecLevel, addQuietZone)
return Intent(context, BarcodeActivity::class.java).apply {
putExtras(args)
}
@@ -525,12 +525,12 @@ class BarcodeActivity : AbstractBaseActivity() {
putInt(COLORS, colors)
}
private fun Bundle.putEcLevelMargin(
private fun Bundle.putEcLevelQuietZone(
ecLevel: Int,
margin: Int
addQuietZone: Boolean
) {
putInt(EC_LEVEL, ecLevel)
putInt(MARGIN, margin)
putBoolean(ADD_QUIET_ZONE, addQuietZone)
}
}
}
@@ -2,7 +2,6 @@ package de.markusfisch.android.binaryeye.activity
import android.annotation.SuppressLint
import android.app.Activity
import android.app.AlertDialog
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
@@ -35,6 +34,7 @@ import androidx.camera.view.PreviewView
import androidx.core.content.ContextCompat
import androidx.core.content.edit
import androidx.core.net.toUri
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.floatingactionbutton.FloatingActionButton
import de.markusfisch.android.binaryeye.R
import de.markusfisch.android.binaryeye.adapter.prettifyFormatName
@@ -48,17 +48,19 @@ import de.markusfisch.android.binaryeye.app.prefs
import de.markusfisch.android.binaryeye.automation.runAutomatedActions
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
import de.markusfisch.android.binaryeye.content.startIntentOrToast
import de.markusfisch.android.binaryeye.database.toScan
import de.markusfisch.android.binaryeye.graphics.FrameMetrics
import de.markusfisch.android.binaryeye.graphics.mapPosition
import de.markusfisch.android.binaryeye.graphics.mapViewYToFrame
import de.markusfisch.android.binaryeye.graphics.setCovered
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.isScanDeeplink
import de.markusfisch.android.binaryeye.net.sendAsync
import de.markusfisch.android.binaryeye.net.urlEncode
import de.markusfisch.android.binaryeye.os.showWhenLocked
import de.markusfisch.android.binaryeye.preference.Preferences
import de.markusfisch.android.binaryeye.view.errorFeedback
import de.markusfisch.android.binaryeye.view.initBars
@@ -83,10 +85,6 @@ import kotlin.math.min
import kotlin.math.roundToInt
class CameraActivity : AppCompatActivity() {
private val frameRoi = Rect()
private val previewRect = Rect()
private val viewBounds = Rect()
private val matrix = Matrix()
private val analyzerExecutor: ExecutorService =
Executors.newSingleThreadExecutor()
private val readerOptions = ReaderOptions(
@@ -105,10 +103,11 @@ class CameraActivity : AppCompatActivity() {
private var cameraProvider: ProcessCameraProvider? = null
private var camera: Camera? = null
private var preview: Preview? = null
private var imageAnalysis: ImageAnalysis? = null
private var currentProfile = prefs.profile
private var shouldStoreSettings = true
private var formatsToRead = setOf<BarcodeFormat>()
private var frameMetrics = FrameMetrics()
private var decoding = true
private var returnResult = false
private var returnUrlTemplate: String? = null
@@ -121,6 +120,29 @@ class CameraActivity : AppCompatActivity() {
private var requestCameraPermission = true
private var useLocalAverage = false
@Volatile
private var cameraViewSnapshot = CameraViewSnapshot()
private data class CameraViewSnapshot(
val viewWidth: Int = 0,
val viewHeight: Int = 0,
val roiLeft: Int = 0,
val roiTop: Int = 0,
val roiRight: Int = 0,
val roiBottom: Int = 0,
val horizontalCrosshairY: Float = 0f
) {
fun isValid() = viewWidth > 0 && viewHeight > 0
fun toViewRoi() = Rect(roiLeft, roiTop, roiRight, roiBottom)
}
private data class CameraFrameMapping(
val frameRoi: Rect,
val matrix: Matrix,
val horizontalCrosshairY: Float
)
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<String>,
@@ -169,6 +191,7 @@ class CameraActivity : AppCompatActivity() {
override fun onCreate(state: Bundle?) {
super.onCreate(state)
showWhenLocked()
setContentView(R.layout.activity_camera)
// Necessary to get the right translation after setting a
@@ -304,6 +327,10 @@ class CameraActivity : AppCompatActivity() {
private fun unbindCameraUseCases() {
decoding = false
imageAnalysis?.clearAnalyzer()
imageAnalysis = null
preview?.surfaceProvider = null
preview = null
cameraProvider?.unbindAll()
camera = null
}
@@ -345,7 +372,7 @@ class CameraActivity : AppCompatActivity() {
menu.findItem(R.id.bulk_mode).isChecked = bulkMode
menu.findItem(R.id.profile).title = getString(
R.string.current_profile,
prefs.profile ?: getString(R.string.profile_default)
prefs.profileLabel(this, prefs.profile)
)
return true
}
@@ -438,7 +465,7 @@ class CameraActivity : AppCompatActivity() {
names.add(0, getString(R.string.remove_restriction))
formats.add(0, null)
}
AlertDialog.Builder(this).apply {
MaterialAlertDialogBuilder(this).apply {
setTitle(R.string.restrict_format)
setItems(names.toTypedArray()) { _, which ->
restrictFormat = formats[which]
@@ -461,7 +488,7 @@ class CameraActivity : AppCompatActivity() {
formatView.setSelection(
values.indexOf(restrictFormat ?: "").coerceAtLeast(0)
)
AlertDialog.Builder(this)
MaterialAlertDialogBuilder(this)
.setView(view)
.setPositiveButton(android.R.string.ok) { _, _ ->
val term = editText.text.toString().trim()
@@ -487,35 +514,32 @@ class CameraActivity : AppCompatActivity() {
Intent.ACTION_VIEW,
getString(R.string.project_url).toUri()
)
execShareIntent(intent)
startIntentOrToast(intent)
}
private fun pickProfile() {
val profiles = arrayOf(
getString(R.string.profile_default),
) + prefs.profiles
AlertDialog.Builder(this)
val profiles = arrayOf<String?>(null) + prefs.profiles
val labels = profiles.map {
prefs.profileLabel(this, it)
}.toTypedArray()
MaterialAlertDialogBuilder(this)
.setTitle(R.string.profile)
.setItems(profiles) { _, which ->
val newProfile = when (which) {
0 -> {
null
}
else -> {
profiles[which]
}
}
.setItems(labels) { _, which ->
val newProfile = profiles[which]
if (currentProfile != newProfile) {
storeSettings()
prefs.load(this, newProfile)
loadPreferences()
refreshIfProfileChanged()
switchProfile(newProfile)
}
}
.show()
}
private fun switchProfile(name: String?) {
storeSettings()
prefs.load(this, name)
loadPreferences()
refreshIfProfileChanged()
}
private fun handleSendText(intent: Intent) {
val text = intent.getStringExtra(Intent.EXTRA_TEXT)
@@ -561,7 +585,7 @@ class CameraActivity : AppCompatActivity() {
private fun initPreviewView() {
cameraView.addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ ->
updateFrameRoiAndMappingMatrix()
updateCameraViewSnapshot()
}
@Suppress("ClickableViewAccessibility")
val touchSlop = ViewConfiguration.get(this).scaledTouchSlop
@@ -670,7 +694,7 @@ class CameraActivity : AppCompatActivity() {
}
detectorView.onRoiChanged = {
decoding = true
updateFrameRoiAndMappingMatrix()
updateCameraViewSnapshot()
}
detectorView.setPaddingFromWindowInsets()
detectorView.cropHandleName = "camera_crop_handle"
@@ -733,12 +757,14 @@ class CameraActivity : AppCompatActivity() {
imageAnalysis
)
preview.surfaceProvider = cameraView.surfaceProvider
this.preview = preview
this.imageAnalysis = imageAnalysis
ignoreNext = null
decoding = true
useLocalAverage = false
updateZoomState()
updateFlashFab(camera?.cameraInfo?.hasFlashUnit() ?: false)
updateFrameRoiAndMappingMatrix()
updateCameraViewSnapshot()
} catch (_: Exception) {
camera = null
toast(R.string.camera_error)
@@ -751,21 +777,18 @@ class CameraActivity : AppCompatActivity() {
if (!decoding) {
return
}
frameMetrics = FrameMetrics(
val frameMetrics = FrameMetrics(
image.width,
image.height,
image.imageInfo.rotationDegrees
)
updateFrameRoiAndMappingMatrix()
if (frameRoi.width() < 1 || frameRoi.height() < 1) {
return
}
val frameMapping = getFrameMapping(frameMetrics) ?: return
useLocalAverage = useLocalAverage xor true
val yPlane = image.planes[0]
ZxingCpp.readYBuffer(
yPlane.buffer,
yPlane.rowStride,
frameRoi,
frameMapping.frameRoi,
frameMetrics.orientation,
readerOptions.apply {
binarizer = if (useLocalAverage) {
@@ -778,14 +801,17 @@ class CameraActivity : AppCompatActivity() {
tryRotate = prefs.autoRotate
}
)?.firstOrNull()?.let { result ->
handleDecodeResult(result)
handleDecodeResult(result, frameMapping)
}
} finally {
image.close()
}
}
private fun handleDecodeResult(result: Result) {
private fun handleDecodeResult(
result: Result,
frameMapping: CameraFrameMapping
) {
val text = result.text
if (text == ignoreNext) {
return
@@ -802,8 +828,29 @@ class CameraActivity : AppCompatActivity() {
}
return
}
if (prefs.showCrosshairs &&
!resultTouchesHorizontalCrosshair(result, frameMapping)
) {
return
}
decoding = false
postResult(result)
postResult(result, frameMapping)
}
private fun resultTouchesHorizontalCrosshair(
result: Result,
frameMapping: CameraFrameMapping
): Boolean {
val crosshairY = frameMapping.matrix.mapViewYToFrame(
frameMapping.horizontalCrosshairY
)
val pos = result.position
val minY = minOf(pos.topLeft.y, pos.topRight.y, pos.bottomLeft.y, pos.bottomRight.y)
val maxY = maxOf(pos.topLeft.y, pos.topRight.y, pos.bottomLeft.y, pos.bottomRight.y)
val centerY = (minY + maxY) / 2f
val halfHeight = (maxY - minY) / 2f
val halfWidth = abs(pos.topRight.x - pos.topLeft.x) / 2f
return abs(centerY - crosshairY) <= maxOf(halfHeight, halfWidth / 3f)
}
private fun updateZoomState() {
@@ -816,29 +863,55 @@ class CameraActivity : AppCompatActivity() {
}
}
private fun updateFrameRoiAndMappingMatrix() {
viewBounds.set(0, 0, cameraView.width, cameraView.height)
if (viewBounds.width() < 1 ||
viewBounds.height() < 1 ||
!frameMetrics.isValid()
) {
return
}
previewRect.setCovered(
viewBounds.width(),
viewBounds.height(),
frameMetrics
)
private fun updateCameraViewSnapshot() {
val viewRoi = if (
detectorView.roi.width() < 1 ||
detectorView.roi.height() < 1
) {
viewBounds
Rect(0, 0, cameraView.width, cameraView.height)
} else {
detectorView.roi
}
frameRoi.setFrameRoi(frameMetrics, previewRect, viewRoi)
matrix.setFrameToView(frameMetrics, previewRect, viewRoi)
cameraViewSnapshot = CameraViewSnapshot(
cameraView.width,
cameraView.height,
viewRoi.left,
viewRoi.top,
viewRoi.right,
viewRoi.bottom,
detectorView.getHorizontalCrosshairY()
)
}
private fun getFrameMapping(
frameMetrics: FrameMetrics
): CameraFrameMapping? {
val snapshot = cameraViewSnapshot
if (!snapshot.isValid() || !frameMetrics.isValid()) {
return null
}
val previewRect = Rect().apply {
setCovered(
snapshot.viewWidth,
snapshot.viewHeight,
frameMetrics
)
}
val viewRoi = snapshot.toViewRoi()
val frameRoi = Rect().apply {
setFrameRoi(frameMetrics, previewRect, viewRoi)
}
if (frameRoi.width() < 1 || frameRoi.height() < 1) {
return null
}
val matrix = Matrix().apply {
setFrameToView(frameMetrics, previewRect, viewRoi)
}
return CameraFrameMapping(
frameRoi,
matrix,
snapshot.horizontalCrosshairY
)
}
private fun updateFlashFab(available: Boolean) {
@@ -858,53 +931,13 @@ class CameraActivity : AppCompatActivity() {
)
}
private fun Rect.setCovered(
viewWidth: Int,
viewHeight: Int,
frameMetrics: FrameMetrics
private fun postResult(
result: Result,
frameMapping: CameraFrameMapping
) {
val frameWidth: Int
val frameHeight: Int
when (frameMetrics.orientation) {
90, 270 -> {
frameWidth = frameMetrics.height
frameHeight = frameMetrics.width
}
else -> {
frameWidth = frameMetrics.width
frameHeight = frameMetrics.height
}
}
if (frameWidth < 1 || frameHeight < 1) {
set(0, 0, 0, 0)
return
}
var coveredWidth = frameWidth
var coveredHeight = frameHeight
if (viewWidth.toLong() * coveredWidth <
viewHeight.toLong() * coveredHeight
) {
coveredWidth = coveredWidth * viewHeight / coveredHeight
coveredHeight = viewHeight
} else {
coveredHeight = coveredHeight * viewWidth / coveredWidth
coveredWidth = viewWidth
}
val left = (viewWidth - coveredWidth) / 2
val top = (viewHeight - coveredHeight) / 2
set(
left,
top,
left + coveredWidth,
top + coveredHeight
)
}
private fun postResult(result: Result) {
cameraView.post {
detectorView.update(
matrix.mapPosition(
frameMapping.matrix.mapPosition(
result.position,
detectorView.coordinates
)
@@ -927,12 +960,15 @@ class CameraActivity : AppCompatActivity() {
}
returnUri != null -> {
execShareIntent(
startIntentOrToast(
Intent(Intent.ACTION_VIEW, returnUri)
)
}
else -> {
if (importProfile(result.text)) {
return@post
}
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
@@ -962,6 +998,21 @@ class CameraActivity : AppCompatActivity() {
}
}
private fun importProfile(text: String): Boolean {
val name = prefs.importProfile(this, text) ?: return false
MaterialAlertDialogBuilder(this)
.setMessage(getString(R.string.profile_import_set_default, name))
.setPositiveButton(android.R.string.ok) { _, _ ->
switchProfile(name)
}
.setNegativeButton(android.R.string.cancel, null)
.setOnDismissListener {
decoding = true
}
.show()
return true
}
companion object {
private const val PICK_FILE_RESULT_CODE = 1
private const val INITIAL_ZOOM = 0
@@ -991,16 +1042,7 @@ fun Activity.showResult(
scan.id = db.insertScan(scan)
}
if (prefs.sendScanActive && prefs.sendScanUrl.isNotEmpty()) {
if (prefs.sendScanType == "4") {
openUrl(
prefs.sendScanUrl + scan.text.urlEncode()
)
return
}
scan.sendAsync(
prefs.sendScanUrl,
prefs.sendScanType
) { code, body ->
sendAsync(scan) { code, body ->
if (code == null || code < 200 || code > 299) {
errorFeedback()
}
@@ -1041,7 +1083,7 @@ fun Activity.showResult(
return
}
if (!bulkMode) {
startActivity(DecodeActivity.newIntent(this, scan))
startActivity(DecodeActivity.newIntent(this, scan, showWhenLocked = true))
}
}
@@ -1,20 +1,18 @@
package de.markusfisch.android.binaryeye.activity
import android.app.Activity
import android.app.AlertDialog
import android.content.Context
import android.content.Intent
import android.content.res.ColorStateList
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.Environment
import android.provider.DocumentsContract
import android.text.Editable
import android.text.Html
import android.text.Spannable
import android.text.SpannableString
import android.text.TextWatcher
import android.text.method.LinkMovementMethod
import android.text.style.TypefaceSpan
import android.view.Menu
import android.view.MenuInflater
@@ -22,30 +20,27 @@ import android.view.MenuItem
import android.view.View
import android.widget.EditText
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TableLayout
import android.widget.TableRow
import android.widget.TextView
import androidx.core.net.toUri
import androidx.core.widget.TextViewCompat
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.floatingactionbutton.FloatingActionButton
import de.markusfisch.android.binaryeye.R
import de.markusfisch.android.binaryeye.actions.ActionRegistry
import de.markusfisch.android.binaryeye.actions.mail.MatMsg
import de.markusfisch.android.binaryeye.actions.mail.MatMsgAction
import de.markusfisch.android.binaryeye.actions.vtype.VTypeParser
import de.markusfisch.android.binaryeye.actions.vtype.vcard.VCardAction
import de.markusfisch.android.binaryeye.actions.vtype.vevent.VEventAction
import de.markusfisch.android.binaryeye.actions.web.WebAction
import de.markusfisch.android.binaryeye.actions.wifi.WifiAction
import de.markusfisch.android.binaryeye.actions.wifi.WifiConnector
import de.markusfisch.android.binaryeye.adapter.prettifyFormatName
import de.markusfisch.android.binaryeye.adapter.toFormatDescriptionResId
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.ContentBarcode
import de.markusfisch.android.binaryeye.content.IdlParser
import de.markusfisch.android.binaryeye.content.ParsedContentType
import de.markusfisch.android.binaryeye.content.copyToClipboard
import de.markusfisch.android.binaryeye.content.idlToRes
import de.markusfisch.android.binaryeye.content.openUrl
import de.markusfisch.android.binaryeye.content.parseData
import de.markusfisch.android.binaryeye.content.shareAsFile
import de.markusfisch.android.binaryeye.content.shareText
import de.markusfisch.android.binaryeye.content.toBarcode
@@ -58,6 +53,7 @@ import de.markusfisch.android.binaryeye.io.askForFileName
import de.markusfisch.android.binaryeye.io.toSaveResult
import de.markusfisch.android.binaryeye.io.writeExternalFile
import de.markusfisch.android.binaryeye.net.createEncodeDeeplink
import de.markusfisch.android.binaryeye.os.showWhenLocked
import de.markusfisch.android.binaryeye.view.hideSoftKeyboard
import de.markusfisch.android.binaryeye.view.setPaddingFromWindowInsets
import de.markusfisch.android.binaryeye.widget.toast
@@ -70,14 +66,21 @@ import kotlinx.coroutines.withContext
import java.io.File
import java.security.MessageDigest
import kotlin.math.roundToInt
import de.markusfisch.android.binaryeye.content.ParsedField as Field
class DecodeActivity : AbstractBaseActivity() {
private lateinit var labelHeadlineView: TextView
private lateinit var contentView: EditText
private lateinit var formatView: TextView
private lateinit var dataView: TableLayout
private lateinit var contentHeadlineView: TextView
private lateinit var characterCountView: TextView
private lateinit var dataHeadlineView: TextView
private lateinit var formatHeadlineView: TextView
private lateinit var dataView: LinearLayout
private lateinit var metaHeadlineView: TextView
private lateinit var metaView: TableLayout
private lateinit var hexHeadlineView: TextView
private lateinit var hexView: TextView
private lateinit var stampView: TextView
private lateinit var formatDescriptionView: TextView
private lateinit var recreationView: ImageView
private lateinit var labelView: EditText
private lateinit var fab: FloatingActionButton
@@ -97,6 +100,8 @@ class DecodeActivity : AbstractBaseActivity() {
private var action = ActionRegistry.DEFAULT_ACTION
private var isBinary = false
private var originalBytes: ByteArray = ByteArray(0)
private var lastParsedDataItems: List<Field> = emptyList()
private var showingDimmed = false
private var label: String? = null
private var recreationSize = 0
@@ -118,6 +123,9 @@ class DecodeActivity : AbstractBaseActivity() {
override fun onCreate(state: Bundle?) {
super.onCreate(state)
if (intent.getBooleanExtra(SHOW_WHEN_LOCKED, false)) {
showWhenLocked()
}
setScreenContentView(R.layout.activity_decode)
setTitle(R.string.content)
@@ -132,32 +140,62 @@ class DecodeActivity : AbstractBaseActivity() {
format = scan.format.name
recreationSize = (200f * dp).roundToInt()
labelHeadlineView = findViewById(R.id.label_headline)
contentView = findViewById(R.id.content)
formatView = findViewById(R.id.format)
contentHeadlineView = findViewById(R.id.content_headline)
characterCountView = findViewById(R.id.character_count)
dataHeadlineView = findViewById(R.id.data_headline)
formatHeadlineView = findViewById(R.id.format_headline)
dataView = findViewById(R.id.data)
metaHeadlineView = findViewById(R.id.meta_headline)
metaView = findViewById(R.id.meta)
hexHeadlineView = findViewById(R.id.hex_headline)
hexView = findViewById(R.id.hex)
stampView = findViewById(R.id.stamp)
formatDescriptionView = findViewById(R.id.format_description)
recreationView = findViewById(R.id.recreation)
labelView = findViewById(R.id.label)
fab = findViewById(R.id.open)
contentHeadlineView.setText(
if (isBinary) {
R.string.section_raw_barcode_content
} else {
R.string.section_raw_barcode_content_editable
}
)
if (prefs.showMetaData) {
val descResId = format.toFormatDescriptionResId()
formatHeadlineView.text = getString(
R.string.section_format_details,
format.prettifyFormatName()
)
formatHeadlineView.visibility = View.VISIBLE
if (descResId != 0) {
formatDescriptionView.setText(descResId)
} else {
formatDescriptionView.visibility = View.GONE
}
metaView.fillMetaView(scan)
} else {
formatHeadlineView.visibility = View.GONE
formatDescriptionView.visibility = View.GONE
metaHeadlineView.visibility = View.GONE
metaView.visibility = View.GONE
}
if (scan.id > 0) {
labelHeadlineView.visibility = View.VISIBLE
scan.label?.let {
labelView.setText(it)
label = it
}
} else {
labelHeadlineView.visibility = View.GONE
labelView.visibility = View.GONE
}
findViewById<View>(R.id.inset_layout).setPaddingFromWindowInsets()
findViewById<View>(R.id.inset_layout).setPaddingFromWindowInsets(bottom = false)
findViewById<View>(R.id.scroll_view).setPaddingFromWindowInsets()
initContentAndFab(justScanned)
@@ -174,7 +212,7 @@ class DecodeActivity : AbstractBaseActivity() {
label = newLabel
}
}
if (action.fired) {
if (action.executed) {
maybeBackOrFinish()
}
}
@@ -252,15 +290,12 @@ class DecodeActivity : AbstractBaseActivity() {
}
private fun updateViews(text: String, bytes: ByteArray) {
formatView.text = resources.getQuantityString(
R.plurals.barcode_info,
dataView.fillDataView(text, bytes, text != scan.text)
characterCountView.text = resources.getQuantityString(
R.plurals.character_count,
bytes.size,
format.prettifyFormatName(),
bytes.size
)
hexView.showIf(prefs.showHexDump) { v ->
v.text = hexDump(bytes)
}
recreationView.showIf(prefs.showRecreation) { v ->
val unmodified: Boolean
val content = if (isBinary) {
@@ -285,7 +320,7 @@ class DecodeActivity : AbstractBaseActivity() {
content,
scan.format,
scan.errorCorrectionLevel.toErrorCorrectionInt(),
margin = 1
addQuietZone = true
)
}
try {
@@ -302,8 +337,16 @@ class DecodeActivity : AbstractBaseActivity() {
clearRecreation()
}
}
dataView.fillDataView(text, bytes)
stampView.setTrackingLink(bytes, format)
hexView.showIf(prefs.showHexDump) { v ->
hexHeadlineView.visibility = View.VISIBLE
v.text = hexDump(bytes)
v.setOnClickListener {
copyToClipboard(bytes.toHexString())
}
}
if (!prefs.showHexDump) {
hexHeadlineView.visibility = View.GONE
}
}
private fun clearRecreation() {
@@ -313,90 +356,51 @@ class DecodeActivity : AbstractBaseActivity() {
}
}
private fun TableLayout.fillDataView(text: String, bytes: ByteArray) {
val items = LinkedHashMap<Any, CharSequence?>()
private fun LinearLayout.fillDataView(
text: String,
bytes: ByteArray,
isEditing: Boolean
) {
val items = mutableListOf<Field>()
val parsedData = parseData(this@DecodeActivity, text, bytes, format, action)
items.addAll(parsedData.fields)
when (prefs.showChecksum) {
"CRC4" -> items[R.string.crc4] = String.format("%X", crc4(bytes))
"MD5" -> items[R.string.md5] = bytes.md5().toHexString().fold()
"SHA1" -> items[R.string.sha1] = bytes.sha1().toHexString().fold()
"SHA256" -> items[R.string.sha256] =
bytes.sha256().toHexString().fold()
else -> Unit
"CRC4" -> items.addField(R.string.crc4, String.format("%X", crc4(bytes)))
"MD5" -> items.addField(R.string.md5, bytes.md5().toHexString().fold())
"SHA1" -> items.addField(R.string.sha1, bytes.sha1().toHexString().fold())
"SHA256" -> items.addField(R.string.sha256, bytes.sha256().toHexString().fold())
}
IdlParser.parse(String(bytes))?.let {
items.putAll(mapOf("IIN" to it.iin))
items.putAll(
it.elements.entries.associate { (id, value) ->
context.idlToRes(id) to value
}
if (parsedData.type != ParsedContentType.UNKNOWN) {
items.add(
0,
Field(
R.string.parsed_type,
getString(parsedData.type.resId)
)
)
lastParsedDataItems = items
fillDataItems(items, false)
return
}
when (action) {
is MatMsgAction -> items.putAll(
MatMsg(text).run {
mapOf(
R.string.email_to to to,
R.string.email_subject to sub,
R.string.email_body to body
)
}
)
is VCardAction,
is VEventAction -> VTypeParser.parseMap(text).let { vData ->
items.putAll(
vData.map { item ->
item.key to item.value.joinToString("\n") {
it.value
}
}.toMap()
)
}
is WebAction -> try {
items.putAll(
text.toUri().run {
mapOf(
R.string.scheme to scheme,
R.string.host to host,
R.string.query to query
)
}
)
} catch (_: Exception) {
// Ignore
}
is WifiAction -> WifiConnector.parseMap(text)?.let { wifiData ->
items.putAll(
linkedMapOf(
R.string.entry_type to getString(R.string.wifi_network),
R.string.wifi_ssid to wifiData["S"],
R.string.wifi_password to wifiData["P"],
R.string.wifi_type to wifiData["T"],
R.string.wifi_hidden to wifiData["H"],
R.string.wifi_eap to wifiData["E"],
R.string.wifi_identity to wifiData["I"],
R.string.wifi_anonymous_identity to wifiData["A"],
R.string.wifi_phase2 to wifiData["PH2"]
)
)
if (isEditing && lastParsedDataItems.isNotEmpty()) {
if (!showingDimmed) {
fillDataItems(lastParsedDataItems, true)
}
return
}
fill(items)
fillDataItems(items, false)
}
private fun TableLayout.fillMetaView(scan: Scan) {
val items = linkedMapOf<Any, CharSequence?>(
R.string.error_correction_level to scan.errorCorrectionLevel,
R.string.sequence_size to scan.sequenceSize.positiveToString(),
R.string.sequence_index to scan.sequenceIndex.positiveToString(),
R.string.sequence_id to scan.sequenceId,
R.string.gtin_country to scan.country,
R.string.gtin_add_on to scan.addOn,
R.string.gtin_price to scan.price,
R.string.gtin_issue_number to scan.issueNumber,
val items = mutableListOf(
Field(R.string.error_correction_level, scan.errorCorrectionLevel),
Field(R.string.sequence_size, scan.sequenceSize.positiveToString()),
Field(R.string.sequence_index, scan.sequenceIndex.positiveToString()),
Field(R.string.sequence_id, scan.sequenceId),
Field(R.string.gtin_country, scan.country),
Field(R.string.gtin_add_on, scan.addOn),
Field(R.string.gtin_price, scan.price),
Field(R.string.gtin_issue_number, scan.issueNumber),
)
if (!scan.version.isNullOrBlank()) {
val versionString = if (scan.format == ZxingCpp.BarcodeFormat.QRCode) {
@@ -408,57 +412,140 @@ class DecodeActivity : AbstractBaseActivity() {
} else {
scan.version
}
items.putAll(
linkedMapOf(
R.string.barcode_version_number to versionString
)
)
items.addField(R.string.barcode_version_number, versionString)
}
if (scan.format == ZxingCpp.BarcodeFormat.QRCode &&
scan.dataMask > -1
) {
items.putAll(
linkedMapOf(
R.string.qr_data_mask to scan.dataMask.toString()
)
)
items.addField(R.string.qr_data_mask, scan.dataMask.toString())
}
val itemsWithValues = items.filter { !it.value.isNullOrBlank() }
if (!itemsWithValues.isEmpty()) {
metaHeadlineView.visibility = View.VISIBLE
fillTable(itemsWithValues)
}
fill(items)
}
private fun TableLayout.fill(
items: LinkedHashMap<Any, CharSequence?>
) {
private fun TableLayout.fillTable(items: List<Field>) {
removeAllViews()
visibility = if (items.isEmpty()) View.GONE else {
val ctx = context
val spaceBetween = (16f * dp).roundToInt()
items.forEach { item ->
val text = item.value
if (!text.isNullOrBlank()) {
val tr = TableRow(ctx)
val keyView = TextView(ctx)
val tr = TableRow(ctx)
val keyView = TextView(ctx).apply {
when (val key = item.key) {
is Int -> keyView.setText(key)
is String -> keyView.text = key
else -> keyView.text = key.toString()
is Int -> setText(key)
is String -> this.text = key
else -> this.text = key.toString()
}
val valueView = TextView(ctx).apply {
setPadding(spaceBetween, 0, 0, 0)
this.text = text
setOnClickListener {
copyToClipboard(text.toString())
}
}
tr.addView(keyView)
tr.addView(valueView)
addView(tr)
}
val valueView = TextView(ctx).apply {
setPadding(spaceBetween, 0, 0, 0)
this.text = text
setOnClickListener {
copyToClipboard(text.toString())
}
}
tr.addView(keyView)
tr.addView(valueView)
addView(tr)
}
View.VISIBLE
}
}
private fun LinearLayout.fillDataItems(
items: List<Field>,
dimmed: Boolean
) {
showingDimmed = dimmed
removeAllViews()
if (items.isEmpty()) {
dataHeadlineView.visibility = View.GONE
visibility = View.GONE
return
}
dataHeadlineView.visibility = View.VISIBLE
val ctx = context
val spaceBetween = (16f * dp).roundToInt()
for (item in items) {
val text = item.value
if (text.isNullOrBlank()) {
continue
}
val rowView = LinearLayout(ctx).apply {
orientation = LinearLayout.VERTICAL
layoutParams = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT
).apply {
bottomMargin = spaceBetween
}
setBackgroundResource(R.drawable.list_selector)
setOnClickListener {
item.link?.let {
openUrl(it)
}
copyToClipboard(text.toString())
}
}
val keyView = TextView(ctx).apply {
TextViewCompat.setTextAppearance(
this,
R.style.SecondaryText
)
when (val key = item.key) {
is Int -> setText(key)
is String -> this.text = key
else -> this.text = key.toString()
}
}
val valueView = TextView(ctx).apply {
TextViewCompat.setTextAppearance(
this,
android.R.style.TextAppearance_Medium
)
setTextColor(
ColorStateList.valueOf(
ctx.obtainStyledAttributes(
intArrayOf(android.R.attr.textColorPrimary)
).run {
try {
getColor(0, currentTextColor)
} finally {
recycle()
}
}
)
)
this.text = text
if (item.link != null) {
setCompoundDrawablesWithIntrinsicBounds(
0,
0,
R.drawable.ic_action_open,
0
)
compoundDrawablePadding = spaceBetween / 2
}
}
val alpha = if (dimmed) 0.5f else 1f
keyView.alpha = alpha
valueView.alpha = alpha
rowView.addView(keyView)
rowView.addView(valueView)
addView(rowView)
}
visibility = View.VISIBLE
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.fragment_decode, menu)
if (scan.id > 0L) {
@@ -527,7 +614,7 @@ class DecodeActivity : AbstractBaseActivity() {
}
private fun askToRemoveScan(id: Long) {
AlertDialog.Builder(this)
MaterialAlertDialogBuilder(this)
.setMessage(R.string.really_remove_scan)
.setPositiveButton(android.R.string.ok) { _, _ ->
db.removeScan(id)
@@ -651,18 +738,32 @@ class DecodeActivity : AbstractBaseActivity() {
private const val SCAN_BUNDLE = "scan_bundle"
private const val OPEN_DOCUMENT = 1
private const val SCHEME_FILE = "file://"
private const val SHOW_WHEN_LOCKED = "show_when_locked"
fun newIntent(context: Context, scan: Scan): Intent {
fun newIntent(
context: Context,
scan: Scan,
showWhenLocked: Boolean = false
): Intent {
val args = Bundle()
args.putBundle(SCAN_BUNDLE, scan.toBundle())
return Intent(context, DecodeActivity::class.java).apply {
putExtra(DECODED_SCAN, true)
putExtra(SHOW_WHEN_LOCKED, showWhenLocked)
putExtras(args)
}
}
}
}
private fun MutableList<Field>.addField(
key: Any,
value: CharSequence?,
link: String? = null
) {
add(Field(key, value, link))
}
private inline fun <T : View> T.showIf(
visible: Boolean,
block: (T) -> Unit
@@ -721,60 +822,6 @@ private fun hexDump(bytes: ByteArray, charsPerLine: Int = 33): String {
private fun Int.positiveToString() = if (this > -1) this.toString() else ""
private fun TextView.setTrackingLink(bytes: ByteArray, format: String) {
val trackingLink = generateDpTrackingLink(bytes, format)
if (trackingLink != null) {
text = trackingLink.fromHtml()
isClickable = true
movementMethod = LinkMovementMethod.getInstance()
} else {
visibility = View.GONE
}
}
private fun generateDpTrackingLink(bytes: ByteArray, format: String): String? {
// Check for Deutsche Post Matrixcode stamp.
var isStamp = false
var rawData = bytes
if (format == ZxingCpp.BarcodeFormat.DataMatrix.name &&
bytes.toString(Charsets.ISO_8859_1).startsWith("DEA5")
) {
if (bytes.size == 47) {
isStamp = true
} else if (bytes.size > 47) {
// Transform back to original data.
rawData = bytes.toString(Charsets.UTF_8).toByteArray(
Charsets.ISO_8859_1
)
if (rawData.size == 47) {
isStamp = true
}
}
}
if (!isStamp) {
return null
}
val hex = StringBuilder()
hex.append(String.format("%02X", rawData[9]))
hex.append(String.format("%02X", rawData[10]))
hex.append(String.format("%02X", rawData[11]))
hex.append(String.format("%02X", rawData[12]))
hex.append(String.format("%02X", rawData[13]))
hex.append(String.format("%X", (rawData[4].toInt() and 0x0f).toByte()))
hex.append(String.format("%02X", rawData[5]))
hex.append(String.format("%02X", rawData[6]))
hex.append(String.format("%02X", rawData[7]))
hex.append(String.format("%02X", rawData[8]))
val hexString = hex.toString()
val trackingNumber = hexString + String.format(
"%X",
crc4(hexString.toByteArray(Charsets.ISO_8859_1))
)
return "<a href=\"https://www.deutschepost.de/de/s/sendungsverfolgung.html?piececode=$trackingNumber\">Deutsche Post: $trackingNumber</a>"
}
// CRC-4 with polynomial x^4 + x + 1.
private fun crc4(input: ByteArray): Int {
var crc = 0
@@ -799,15 +846,6 @@ private fun crc4(input: ByteArray): Int {
return crc
}
private fun String.fromHtml() = if (
Build.VERSION.SDK_INT >= Build.VERSION_CODES.N
) {
Html.fromHtml(this, Html.FROM_HTML_MODE_LEGACY)
} else {
@Suppress("DEPRECATION")
Html.fromHtml(this)
}
private fun ByteArray.md5(): ByteArray = MessageDigest.getInstance(
"MD5"
).run {
@@ -18,6 +18,7 @@ import androidx.appcompat.widget.SwitchCompat
import androidx.core.net.toUri
import de.markusfisch.android.binaryeye.R
import de.markusfisch.android.binaryeye.adapter.prettifyFormatName
import de.markusfisch.android.binaryeye.adapter.toFormatDescriptionResId
import de.markusfisch.android.binaryeye.app.prefs
import de.markusfisch.android.binaryeye.net.isEncodeDeeplink
import de.markusfisch.android.binaryeye.text.unescape
@@ -28,9 +29,9 @@ import de.markusfisch.android.zxingcpp.ZxingCpp.BarcodeFormat
import java.io.InputStream
import kotlin.math.min
class EncodeActivity : AbstractBaseActivity() {
private lateinit var formatView: Spinner
private lateinit var formatDescriptionView: TextView
private lateinit var ecLabel: TextView
private lateinit var ecSpinner: Spinner
private lateinit var colorsLabel: TextView
@@ -42,7 +43,7 @@ class EncodeActivity : AbstractBaseActivity() {
private val writeableFormats = arrayListOf(
BarcodeFormat.Aztec,
BarcodeFormat.Codabar,
BarcodeFormat.Code39,
BarcodeFormat.Code39Std,
BarcodeFormat.Code39Ext,
BarcodeFormat.Code32,
BarcodeFormat.PZN,
@@ -99,6 +100,7 @@ class EncodeActivity : AbstractBaseActivity() {
setTitle(R.string.compose_barcode)
formatView = findViewById(R.id.format)
formatDescriptionView = findViewById(R.id.format_description)
val formatAdapter = ArrayAdapter(
this,
android.R.layout.simple_spinner_item,
@@ -116,6 +118,13 @@ class EncodeActivity : AbstractBaseActivity() {
id: Long
) {
val format = writeableFormats[position]
val descResId = format.name.toFormatDescriptionResId()
if (descResId != 0) {
formatDescriptionView.setText(descResId)
formatDescriptionView.visibility = View.VISIBLE
} else {
formatDescriptionView.visibility = View.GONE
}
val arrayId = when (format) {
BarcodeFormat.Aztec -> R.array.aztec_error_correction_levels
BarcodeFormat.QRCode -> R.array.qr_error_correction_levels
@@ -172,45 +181,20 @@ class EncodeActivity : AbstractBaseActivity() {
unescapeCheckBox = findViewById(R.id.unescape)
unescapeCheckBox.isChecked = prefs.expandEscapeSequences
var complete = false
val args = intent?.extras ?: getExtrasFromDeeplink()
args?.getString(CONTENT_TEXT)?.let {
contentView.setText(it)
complete = it.isNotEmpty()
}
args?.getByteArray(CONTENT_RAW)?.let {
bytes = it
complete = it.isNotEmpty()
setEncodeByteArray()
}
val barcodeFormat = args?.getString(FORMAT)
if (barcodeFormat != null) {
formatView.setSelection(
writeableFormats.indexOf(barcodeFormat.toFormat())
)
} else if (state == null) {
formatView.post {
formatView.setSelection(prefs.indexOfLastSelectedFormat)
}
}
applyIntent(state == null)
findViewById<View>(R.id.encode).setOnClickListener {
it.context.encode()
}
findViewById<View>(R.id.inset_layout).setPaddingFromWindowInsets()
findViewById<View>(R.id.inset_layout).setPaddingFromWindowInsets(bottom = false)
findViewById<View>(R.id.scroll_view).setPaddingFromWindowInsets()
}
if (complete &&
args?.getBoolean(EXECUTE, false) == true
) {
contentView.post {
val content = contentView.context.getContent() ?: return@post
startActivity(newBarcodeIntent(content))
finish()
}
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
setIntent(intent)
applyIntent(true)
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
@@ -243,6 +227,59 @@ class EncodeActivity : AbstractBaseActivity() {
prefs.addQuietZone = addQuietZoneSwitch.isChecked
}
private fun applyIntent(applyDefaultFormat: Boolean) {
val args = Bundle().apply {
intent?.extras?.let { putAll(it) }
getExtrasFromDeeplink()?.let { putAll(it) }
}.takeIf { !it.isEmpty }
val hasTextContent = args?.containsKey(CONTENT_TEXT) == true
val hasRawContent = args?.containsKey(CONTENT_RAW) == true
var complete = bytes?.isNotEmpty() == true ||
contentView.text?.isNotEmpty() == true
if (hasTextContent || hasRawContent) {
bytes = null
contentView.isEnabled = true
contentView.hint = null
unescapeCheckBox.isEnabled = true
}
if (hasTextContent) {
args.getString(CONTENT_TEXT)?.let {
contentView.setText(it)
complete = it.isNotEmpty()
}
}
if (hasRawContent) {
args.getByteArray(CONTENT_RAW)?.let {
bytes = it
complete = it.isNotEmpty()
setEncodeByteArray()
}
}
val barcodeFormat = args?.getString(FORMAT)
if (barcodeFormat != null) {
formatView.setSelection(
writeableFormats.indexOf(barcodeFormat.toFormat())
)
} else if (applyDefaultFormat) {
formatView.post {
formatView.setSelection(prefs.indexOfLastSelectedFormat)
}
}
if (complete &&
args?.getBoolean(EXECUTE, false) == true
) {
contentView.post {
val content = contentView.context.getContent() ?: return@post
startActivity(newBarcodeIntent(content))
finish()
}
}
}
private fun Context.encode() {
hideSoftKeyboard(contentView)
val content = getContent() ?: return
@@ -258,11 +295,7 @@ class EncodeActivity : AbstractBaseActivity() {
content,
format,
format.getErrorCorrectionLevel(ecSpinner.selectedItemPosition),
if (addQuietZoneSwitch.isChecked) {
1
} else {
0
},
addQuietZoneSwitch.isChecked,
if (format.canBeInverted()) {
colorsSpinner.selectedItemPosition
} else 0
@@ -1,10 +1,10 @@
package de.markusfisch.android.binaryeye.activity
import android.annotation.SuppressLint
import android.app.AlertDialog
import android.app.SearchManager
import android.content.Context
import android.database.Cursor
import android.graphics.Color
import android.os.Build
import android.os.Bundle
import android.os.Parcelable
@@ -18,7 +18,8 @@ import android.widget.ListView
import androidx.appcompat.view.ActionMode
import androidx.appcompat.widget.SearchView
import androidx.appcompat.widget.SwitchCompat
import androidx.core.content.ContextCompat
import com.google.android.material.color.MaterialColors
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import de.markusfisch.android.binaryeye.R
import de.markusfisch.android.binaryeye.adapter.ScansAdapter
import de.markusfisch.android.binaryeye.app.alertDialog
@@ -69,9 +70,10 @@ class HistoryActivity : AbstractBaseActivity() {
lockStatusBarColor()
val ac = this@HistoryActivity
@Suppress("DEPRECATION")
ac.window.statusBarColor = ContextCompat.getColor(
ac.window.statusBarColor = MaterialColors.getColor(
ac,
R.color.accent_dark
com.google.android.material.R.attr.colorSecondaryContainer,
Color.TRANSPARENT
)
}
return true
@@ -174,7 +176,7 @@ class HistoryActivity : AbstractBaseActivity() {
progressView = findViewById(R.id.progress_view)
findViewById<View>(R.id.inset_layout).setPaddingFromWindowInsets()
findViewById<View>(R.id.inset_layout).setPaddingFromWindowInsets(bottom = false)
listView.setPaddingFromWindowInsets()
}
@@ -342,7 +344,7 @@ class HistoryActivity : AbstractBaseActivity() {
)
val nameView = view.findViewById<EditText>(R.id.name)
nameView.setText(text)
AlertDialog.Builder(this)
MaterialAlertDialogBuilder(this)
.setTitle(
if (content.isNullOrEmpty()) {
getString(R.string.binary_data)
@@ -361,7 +363,7 @@ class HistoryActivity : AbstractBaseActivity() {
}
private fun Context.askToRemoveScans(ids: List<Long>) {
AlertDialog.Builder(this)
MaterialAlertDialogBuilder(this)
.setMessage(
if (ids.size > 1) {
R.string.really_remove_selected_scans
@@ -383,7 +385,7 @@ class HistoryActivity : AbstractBaseActivity() {
}
private fun Context.askToRemoveScans() {
AlertDialog.Builder(this)
MaterialAlertDialogBuilder(this)
.setMessage(
if (filter == null) {
R.string.really_remove_all_scans
@@ -448,7 +450,7 @@ class HistoryActivity : AbstractBaseActivity() {
val separators = resources.getStringArray(
R.array.list_separators_values
)
AlertDialog.Builder(this)
MaterialAlertDialogBuilder(this)
.setTitle(R.string.pick_list_separator)
.setItems(R.array.list_separators_names) { _, which ->
shareScans(separators[which], asFile)
@@ -1,6 +1,5 @@
package de.markusfisch.android.binaryeye.activity
import android.app.AlertDialog
import android.app.Dialog
import android.os.Bundle
import android.view.View
@@ -10,6 +9,8 @@ import android.widget.EditText
import android.widget.ListView
import android.widget.Spinner
import android.widget.TextView
import androidx.appcompat.app.AlertDialog
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import de.markusfisch.android.binaryeye.R
import de.markusfisch.android.binaryeye.adapter.prettifyFormatName
import de.markusfisch.android.binaryeye.adapter.setupFormatSpinner
@@ -54,7 +55,7 @@ class IgnoreCodesActivity : AbstractBaseActivity() {
}
}
findViewById<View>(R.id.inset_layout).setPaddingFromWindowInsets()
findViewById<View>(R.id.inset_layout).setPaddingFromWindowInsets(bottom = false)
listView.setPaddingFromWindowInsets()
}
@@ -74,7 +75,7 @@ class IgnoreCodesActivity : AbstractBaseActivity() {
formatView.setSelection(0)
}
val dialog = AlertDialog.Builder(this)
val dialog = MaterialAlertDialogBuilder(this)
.setTitle(
if (pattern == null) {
R.string.ignore_code_add
@@ -133,7 +134,7 @@ class IgnoreCodesActivity : AbstractBaseActivity() {
}
private fun confirmRemovePattern(position: Int) {
AlertDialog.Builder(this)
MaterialAlertDialogBuilder(this)
.setMessage(R.string.ignore_code_remove_confirm)
.setPositiveButton(android.R.string.ok) { _, _ ->
patterns.removeAt(position)
@@ -40,7 +40,7 @@ class NetworkSuggestionsActivity : AbstractBaseActivity() {
listView.emptyView = findViewById(R.id.no_suggestions)
listView.adapter = suggestionArrayAdapter
listView.setOnScrollListener(systemBarListViewScrollListener)
findViewById<View>(R.id.inset_layout).setPaddingFromWindowInsets()
findViewById<View>(R.id.inset_layout).setPaddingFromWindowInsets(bottom = false)
listView.setPaddingFromWindowInsets()
findViewById<View>(R.id.remove).setOnClickListener {
@@ -1,6 +1,5 @@
package de.markusfisch.android.binaryeye.activity
import android.app.AlertDialog
import android.content.Context
import android.os.Bundle
import android.view.View
@@ -9,11 +8,13 @@ import android.widget.ArrayAdapter
import android.widget.EditText
import android.widget.ListView
import android.widget.TextView
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import de.markusfisch.android.binaryeye.R
import de.markusfisch.android.binaryeye.app.prefs
import de.markusfisch.android.binaryeye.view.setPaddingFromWindowInsets
import de.markusfisch.android.binaryeye.view.systemBarListViewScrollListener
import de.markusfisch.android.binaryeye.widget.toast
import de.markusfisch.android.zxingcpp.ZxingCpp.BarcodeFormat
class ProfilesActivity : AbstractBaseActivity() {
private val profiles = ArrayList<ProfileItem>()
@@ -44,7 +45,7 @@ class ProfilesActivity : AbstractBaseActivity() {
addProfile()
}
findViewById<View>(R.id.inset_layout).setPaddingFromWindowInsets()
findViewById<View>(R.id.inset_layout).setPaddingFromWindowInsets(bottom = false)
listView.setPaddingFromWindowInsets()
}
@@ -53,11 +54,11 @@ class ProfilesActivity : AbstractBaseActivity() {
profiles.add(
ProfileItem(
null,
getString(R.string.profile_default)
prefs.profileLabel(this, null)
)
)
prefs.profiles.forEach { name ->
profiles.add(ProfileItem(name, name))
profiles.add(ProfileItem(name, prefs.profileLabel(this, name)))
}
if (this::adapter.isInitialized) {
adapter.notifyDataSetChanged()
@@ -80,7 +81,7 @@ class ProfilesActivity : AbstractBaseActivity() {
R.layout.dialog_profile_name, null
)
val editText = view.findViewById<EditText>(R.id.name)
AlertDialog.Builder(this)
MaterialAlertDialogBuilder(this)
.setView(view)
.setPositiveButton(android.R.string.ok) { _, _ ->
val name = editText.text.toString().trim()
@@ -97,7 +98,7 @@ class ProfilesActivity : AbstractBaseActivity() {
private fun confirmRemoveProfile(position: Int) {
val profile = profiles[position]
val profileName = profile.name ?: return
AlertDialog.Builder(this)
MaterialAlertDialogBuilder(this)
.setMessage(R.string.profile_remove_confirm)
.setPositiveButton(android.R.string.ok) { _, _ ->
val isCurrent = prefs.profile == profileName
@@ -116,8 +117,8 @@ class ProfilesActivity : AbstractBaseActivity() {
private val items: List<ProfileItem>
) : ArrayAdapter<ProfileItem>(
context,
android.R.layout.simple_list_item_2,
android.R.id.text1,
R.layout.row_profile,
R.id.title,
items
) {
override fun getView(
@@ -129,6 +130,7 @@ class ProfilesActivity : AbstractBaseActivity() {
val item = items[position]
val isCurrent = item.name == prefs.profile
getViewHolder(view).apply {
current = item
title.text = item.label
if (isCurrent) {
subtitle.text = context.getString(R.string.profile_current)
@@ -144,16 +146,31 @@ class ProfilesActivity : AbstractBaseActivity() {
private fun getViewHolder(
view: View
): ViewHolder = view.tag as ViewHolder? ?: ViewHolder(
view.findViewById(android.R.id.text1),
view.findViewById(android.R.id.text2),
).also {
view.tag = it
view.findViewById(R.id.title),
view.findViewById(R.id.subtitle),
view.findViewById(R.id.share_qr),
).also { holder ->
holder.shareQr.setOnClickListener {
val item = holder.current ?: return@setOnClickListener
context.startActivity(
BarcodeActivity.newIntent(
context,
prefs.toJson(context, item.name),
BarcodeFormat.QRCode,
addQuietZone = true
)
)
}
view.tag = holder
}
private data class ViewHolder(
private class ViewHolder(
val title: TextView,
val subtitle: TextView,
)
val shareQr: View,
) {
var current: ProfileItem? = null
}
}
}
@@ -1,10 +1,10 @@
package de.markusfisch.android.binaryeye.activity
import android.app.Activity
import android.content.Intent
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
class SplashActivity : AppCompatActivity() {
class SplashActivity : Activity() {
override fun onCreate(state: Bundle?) {
super.onCreate(state)
@@ -11,22 +11,11 @@ class WelcomeActivity : AppCompatActivity() {
super.onCreate(state)
setContentView(R.layout.activity_welcome)
findViewById<View>(R.id.expert).setOnClickListener {
prefs.loadExpertProfile(this)
finish()
}
findViewById<View>(R.id.simple).setOnClickListener {
prefs.apply {
showCropHandle = false
zoomBySwiping = false
beep = true
copyImmediately = true
openImmediately = true
showMetaData = false
showHexDump = false
defaultSearchUrl = "https://www.google.com/search?q="
openWithUrl = defaultSearchUrl
expandEscapeSequences = false
brightenScreen = true
}
prefs.loadSimpleProfile(this)
finish()
}
}
@@ -8,10 +8,13 @@ import android.view.ViewGroup
import android.widget.ArrayAdapter
import android.widget.CursorAdapter
import android.widget.ImageButton
import android.widget.ImageView
import android.widget.Spinner
import android.widget.TextView
import androidx.core.content.ContextCompat
import de.markusfisch.android.binaryeye.R
import de.markusfisch.android.binaryeye.content.ParsedContentType
import de.markusfisch.android.binaryeye.content.getContentPreview
import de.markusfisch.android.binaryeye.content.parsedContentTypeFromName
import de.markusfisch.android.binaryeye.database.Database
import java.text.DateFormat
import java.text.ParseException
@@ -29,12 +32,10 @@ class ScansAdapter(
private val timeIndex = cursor.getColumnIndex(Database.SCANS_DATETIME)
private val nameIndex = cursor.getColumnIndex(Database.SCANS_NAME)
private val textIndex = cursor.getColumnIndex(Database.SCANS_TEXT)
private val rawIndex = cursor.getColumnIndex(Database.SCANS_RAW)
private val contentTypeIndex = cursor.getColumnIndex(Database.SCANS_CONTENT_TYPE)
private val formatIndex = cursor.getColumnIndex(Database.SCANS_FORMAT)
private val selections = mutableMapOf<Long, Int>()
private val selectedColor = ContextCompat.getColor(
context, R.color.selected_row
)
fun select(view: View, id: Long, position: Int) {
view.select(
if (selections[id] == null) {
@@ -99,14 +100,20 @@ class ScansAdapter(
) {
val id = cursor.getLong(idIndex)
val pinned = cursor.getInt(pinnedIndex) != 0
val format = cursor.getString(formatIndex)
val contentType = parsedContentTypeFromName(
cursor.getString(contentTypeIndex)
)
getViewHolder(view).apply {
metaView.text = context.getString(
R.string.scan_meta_data,
formatDateTime(cursor.getString(timeIndex)),
cursor.getString(formatIndex).prettifyFormatName()
metaView.text = formatDateTime(cursor.getString(timeIndex))
formatView.text = format.prettifyFormatName().withContentType(
context,
contentType
)
formatIconView.setImageResource(format.toBarcodeIcon())
val name = cursor.getString(nameIndex)
val text = cursor.getString(textIndex)
val raw = cursor.getBlob(rawIndex)
var icon = 0
contentView.text = when {
name?.isNotEmpty() == true -> {
@@ -114,8 +121,11 @@ class ScansAdapter(
name
}
text.isNullOrEmpty() -> context.getString(
R.string.binary_data
text.isNullOrEmpty() -> getContentPreview(
context,
text ?: "",
raw ?: ByteArray(0),
format
)
else -> text
@@ -151,13 +161,16 @@ class ScansAdapter(
}
private fun View.select(selected: Boolean) {
setBackgroundColor(if (selected) selectedColor else 0)
isActivated = selected
refreshDrawableState()
}
private fun getViewHolder(
view: View
): ViewHolder = view.tag as ViewHolder? ?: ViewHolder(
view.findViewById(R.id.format_icon),
view.findViewById(R.id.meta),
view.findViewById(R.id.format),
view.findViewById(R.id.content),
view.findViewById(R.id.pin)
).also {
@@ -165,7 +178,9 @@ class ScansAdapter(
}
private data class ViewHolder(
val formatIconView: ImageView,
val metaView: TextView,
val formatView: TextView,
val contentView: TextView,
val pinView: ImageButton
)
@@ -200,6 +215,69 @@ fun Context.setupFormatSpinner(spinner: Spinner): List<String> {
fun String.prettifyFormatName() = replace("_", " ")
.replace(formatWordBoundary, " ")
private fun String.withContentType(
context: Context,
type: ParsedContentType
) = if (type == ParsedContentType.UNKNOWN) {
this
} else {
context.getString(
R.string.scan_meta_data,
this,
context.getString(type.resId)
)
}
fun String.toFormatDescriptionResId(): Int = when (this) {
"Aztec" -> R.string.format_description_aztec
"Codabar" -> R.string.format_description_codabar
"Code39",
"Code39Std" -> R.string.format_description_code39
"Code39Ext" -> R.string.format_description_code39ext
"Code32" -> R.string.format_description_code32
"PZN" -> R.string.format_description_pzn
"Code93" -> R.string.format_description_code93
"Code128" -> R.string.format_description_code128
"DataBar" -> R.string.format_description_databar
"DataBarOmni" -> R.string.format_description_databar_omni
"DataBarStk" -> R.string.format_description_databar_stk
"DataBarLtd" -> R.string.format_description_databar_ltd
"DataBarExp" -> R.string.format_description_databar_exp
"DataBarExpStk" -> R.string.format_description_databar_exp_stk
"DataMatrix" -> R.string.format_description_data_matrix
"DXFilmEdge" -> R.string.format_description_dx_film_edge
"EAN8" -> R.string.format_description_ean8
"EAN13" -> R.string.format_description_ean13
"ITF" -> R.string.format_description_itf
"MaxiCode" -> R.string.format_description_maxicode
"PDF417" -> R.string.format_description_pdf417
"Telepen" -> R.string.format_description_telepen
"TelepenAlpha" -> R.string.format_description_telepen_alpha
"TelepenNumeric" -> R.string.format_description_telepen_numeric
"QRCode" -> R.string.format_description_qr_code
"MicroQRCode" -> R.string.format_description_micro_qr_code
"RMQRCode" -> R.string.format_description_rmqr_code
"UPCA" -> R.string.format_description_upca
"UPCE" -> R.string.format_description_upce
else -> 0
}
private fun String?.toBarcodeIcon(): Int = when (this) {
"QRCode",
"RMQRCode" -> R.drawable.ic_barcode_qr_code
"MicroQRCode" -> R.drawable.ic_barcode_micro_qr
"Aztec" -> R.drawable.ic_barcode_aztec
"DataMatrix",
"MaxiCode" -> R.drawable.ic_barcode_data_matrix
"PDF417" -> R.drawable.ic_barcode_pdf417
else -> R.drawable.ic_barcode_linear
}
private fun formatDateTime(rfc: String): String {
try {
SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.US).parse(rfc)?.let {
@@ -1,7 +1,7 @@
package de.markusfisch.android.binaryeye.app
import android.app.AlertDialog
import android.content.Context
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlin.coroutines.resume
@@ -9,10 +9,10 @@ import kotlin.coroutines.suspendCoroutine
suspend inline fun <T : Any> alertDialog(
context: Context,
crossinline build: AlertDialog.Builder.(resume: (T?) -> Unit) -> Unit
crossinline build: MaterialAlertDialogBuilder.(resume: (T?) -> Unit) -> Unit
): T? = withContext(Dispatchers.Main) {
suspendCoroutine { continuation ->
AlertDialog.Builder(context).apply {
MaterialAlertDialogBuilder(context).apply {
setOnCancelListener {
continuation.resume(null)
}
@@ -1,6 +1,7 @@
package de.markusfisch.android.binaryeye.app
import android.app.Application
import com.google.android.material.color.DynamicColors
import de.markusfisch.android.binaryeye.database.Database
import de.markusfisch.android.binaryeye.preference.Preferences
@@ -10,7 +11,11 @@ val prefs = Preferences()
class BinaryEyeApp : Application() {
override fun onCreate() {
super.onCreate()
db.open(this)
prefs.init(this)
DynamicColors.applyToActivitiesIfAvailable(
this,
DynamicColors.Precondition { _, _ -> prefs.dynamicColors }
)
db.open(this)
}
}
@@ -4,8 +4,8 @@ import android.content.Context
import android.content.Intent
import de.markusfisch.android.binaryeye.R
import de.markusfisch.android.binaryeye.app.prefs
import de.markusfisch.android.binaryeye.content.execShareIntent
import de.markusfisch.android.binaryeye.content.openUrl
import de.markusfisch.android.binaryeye.content.startIntentOrToast
import de.markusfisch.android.binaryeye.database.Scan
import de.markusfisch.android.binaryeye.widget.toast
@@ -55,5 +55,5 @@ private fun Context.execCustomIntent(
toast(R.string.automated_action_intent_uri_invalid)
return false
}
return execShareIntent(intent)
return startIntentOrToast(intent)
}
@@ -1,6 +1,7 @@
package de.markusfisch.android.binaryeye.content
import android.graphics.Bitmap
import android.util.Size
import de.markusfisch.android.binaryeye.database.Scan
import de.markusfisch.android.binaryeye.graphics.COLOR_BLACK
import de.markusfisch.android.binaryeye.graphics.COLOR_WHITE
@@ -49,7 +50,7 @@ fun Scan.toBarcode(): Barcode<*> {
content,
format,
errorCorrectionLevel.toErrorCorrectionInt(),
margin = 1
addQuietZone = true
)
}
}
@@ -109,21 +110,24 @@ class ContentBarcode<T>(
content: T,
format: BarcodeFormat,
val ecLevel: Int = -1,
val margin: Int = 0,
val addQuietZone: Boolean = false,
colors: BarcodeColors = BarcodeColors.BLACK_ON_WHITE
) : Barcode<T>(content, format, colors) {
override fun toBitmap(size: Int) = ZxingCpp.encodeAsBitmap(
content, format, size, format.height(size), margin, ecLevel,
setColor = colors.foregroundColor(),
unsetColor = colors.backgroundColor()
)
override fun toBitmap(size: Int): Bitmap {
val s = format.resolveSize(size)
return ZxingCpp.encodeAsBitmap(
content, format, s.width, s.height, addQuietZone, ecLevel,
setColor = colors.foregroundColor(),
unsetColor = colors.backgroundColor()
)
}
override fun toSvg() = ZxingCpp.encodeAsSvg(
content, format, margin, ecLevel
content, format, addQuietZone, ecLevel
)
override fun toText() = ZxingCpp.encodeAsText(
content, format, margin, ecLevel,
content, format, addQuietZone, ecLevel,
inverted = colors == BarcodeColors.BLACK_ON_WHITE
)
}
@@ -185,10 +189,11 @@ private fun BitMatrix.inflate(
return scaled
}
private fun BarcodeFormat.height(size: Int) = when (this) {
private fun BarcodeFormat.resolveSize(size: Int): Size = when (this) {
// 1D barcodes don't need as much vertical space.
BarcodeFormat.Codabar,
BarcodeFormat.Code39,
BarcodeFormat.Code39Std,
BarcodeFormat.Code39Ext,
BarcodeFormat.Code32,
BarcodeFormat.PZN,
@@ -203,8 +208,14 @@ private fun BarcodeFormat.height(size: Int) = when (this) {
BarcodeFormat.EAN8,
BarcodeFormat.EAN13,
BarcodeFormat.ITF,
BarcodeFormat.Telepen,
BarcodeFormat.TelepenAlpha,
BarcodeFormat.TelepenNumeric,
BarcodeFormat.UPCA,
BarcodeFormat.UPCE -> size / 3
BarcodeFormat.UPCE -> Size(size, size / 3)
else -> size
// PDF417 requires more horizontal space.
BarcodeFormat.PDF417 -> Size(size * 3, size / 2)
else -> Size(size, size)
}
@@ -0,0 +1,239 @@
package de.markusfisch.android.binaryeye.content
import android.content.Context
import android.net.Uri
import androidx.core.net.toUri
import de.markusfisch.android.binaryeye.R
import de.markusfisch.android.binaryeye.actions.Action
import de.markusfisch.android.binaryeye.actions.ActionRegistry
import de.markusfisch.android.binaryeye.actions.mail.MatMsg
import de.markusfisch.android.binaryeye.actions.mail.MatMsgAction
import de.markusfisch.android.binaryeye.actions.otpauth.OtpauthAction
import de.markusfisch.android.binaryeye.actions.vtype.VTypeParser
import de.markusfisch.android.binaryeye.actions.vtype.vcard.VCardAction
import de.markusfisch.android.binaryeye.actions.vtype.vevent.VEventAction
import de.markusfisch.android.binaryeye.actions.web.WebAction
import de.markusfisch.android.binaryeye.actions.wifi.WifiAction
import de.markusfisch.android.binaryeye.actions.wifi.WifiConnector
import org.json.JSONException
import org.json.JSONObject
enum class ParsedContentType(
val resId: Int
) {
UNKNOWN(0),
DEUTSCHE_POST(R.string.parsed_type_deutsche_post),
JSON(R.string.parsed_type_json),
INTERNATIONAL_DRIVER_LICENSE(
R.string.parsed_type_international_driver_license
),
DIGITAL_SEAL(R.string.parsed_type_digital_seal),
SEPA_EPC_QR(R.string.parsed_type_sepa_epc_qr),
EMAIL(R.string.parsed_type_email),
URL(R.string.parsed_type_url),
OTP(R.string.parsed_type_otp),
CONTACT_CARD(R.string.parsed_type_contact_card),
CALENDAR_EVENT(R.string.parsed_type_calendar_event),
WIFI_NETWORK(R.string.parsed_type_wifi_network);
}
data class ParsedData(
val type: ParsedContentType,
val fields: List<ParsedField>
)
data class ParsedField(
val key: Any,
val value: CharSequence?,
val link: String? = null
)
fun parseData(
context: Context,
text: String,
bytes: ByteArray,
format: String,
action: Action = ActionRegistry.getAction(bytes)
): ParsedData {
val fields = mutableListOf<ParsedField>()
DeutschePostParser.parse(bytes, format)?.let {
fields.addField(
context.getString(R.string.tracking_number),
it.label,
it.link,
)
return ParsedData(ParsedContentType.DEUTSCHE_POST, fields)
}
try {
fields.addField(R.string.formatted_json, JSONObject(text).toString(2))
return ParsedData(ParsedContentType.JSON, fields)
} catch (_: JSONException) {
// Ignore
}
IdlParser.parse(String(bytes))?.let {
fields.addField("IIN", it.iin)
it.elements.forEach { (id, value) ->
fields.addField(context.idlToRes(id), value)
}
return ParsedData(ParsedContentType.INTERNATIONAL_DRIVER_LICENSE, fields)
}
SealParser.parse(context, bytes)?.let { sealFields ->
sealFields.forEach { vf ->
fields.addField(vf.name, vf.value)
}
return ParsedData(ParsedContentType.DIGITAL_SEAL, fields)
}
EpcQrParser.parse(text)?.let {
fields.addAll(it.map { (id, value) ->
ParsedField(context.epcQrToRes(id), value)
})
return ParsedData(ParsedContentType.SEPA_EPC_QR, fields)
}
when (action) {
is MatMsgAction -> MatMsg(text).run {
fields.addField(R.string.email_to, to)
fields.addField(R.string.email_subject, sub)
fields.addField(R.string.email_body, body)
return ParsedData(ParsedContentType.EMAIL, fields)
}
is VCardAction,
is VEventAction -> VTypeParser.parseMap(text).let { parsedMap ->
parsedMap.forEach { item ->
fields.addField(item.key, item.value.joinToString("\n") { it.value })
}
return ParsedData(
if (action is VCardAction) {
ParsedContentType.CONTACT_CARD
} else {
ParsedContentType.CALENDAR_EVENT
},
fields
)
}
is WebAction -> try {
text.toUri().run {
fields.addField(R.string.scheme, scheme)
fields.addField(R.string.host, host)
fields.addField(R.string.query, query)
}
return ParsedData(ParsedContentType.URL, fields)
} catch (_: Exception) {
// Ignore
}
is OtpauthAction -> try {
text.toUri().run {
val label = pathSegments.firstOrNull()?.let {
Uri.decode(it)
} ?: ""
val colonIdx = label.indexOf(':')
val issuerFromLabel = if (colonIdx >= 0) {
label.substring(0, colonIdx)
} else {
null
}
val account = if (colonIdx >= 0) {
label.substring(colonIdx + 1).trim()
} else {
label
}
val issuer = getQueryParameter("issuer") ?: issuerFromLabel
fields.addField(R.string.entry_type, host?.uppercase())
fields.addField(R.string.otp_account, account)
fields.addField(R.string.otp_issuer, issuer)
fields.addField(R.string.otp_algorithm, getQueryParameter("algorithm"))
fields.addField(R.string.otp_digits, getQueryParameter("digits"))
if (host == "totp") {
fields.addField(R.string.otp_period, getQueryParameter("period"))
} else {
fields.addField(R.string.otp_counter, getQueryParameter("counter"))
}
}
return ParsedData(ParsedContentType.OTP, fields)
} catch (_: Exception) {
// Ignore
}
is WifiAction -> WifiConnector.parseMap(text)?.let { wifiData ->
fields.addField(R.string.wifi_ssid, wifiData["S"])
fields.addField(R.string.wifi_password, wifiData["P"])
fields.addField(R.string.wifi_type, wifiData["T"])
fields.addField(R.string.wifi_hidden, wifiData["H"])
fields.addField(R.string.wifi_eap, wifiData["E"])
fields.addField(R.string.wifi_identity, wifiData["I"])
fields.addField(R.string.wifi_anonymous_identity, wifiData["A"])
fields.addField(R.string.wifi_phase2, wifiData["PH2"])
return ParsedData(ParsedContentType.WIFI_NETWORK, fields)
}
}
return ParsedData(ParsedContentType.UNKNOWN, emptyList())
}
fun parsedContentTypeFromName(
name: String?
): ParsedContentType = try {
ParsedContentType.valueOf(name ?: "")
} catch (_: IllegalArgumentException) {
ParsedContentType.UNKNOWN
}
fun getContentPreview(
context: Context,
text: String,
bytes: ByteArray,
format: String,
action: Action = ActionRegistry.getAction(bytes)
): CharSequence {
val parsedData = parseData(context, text, bytes, format, action)
return when (parsedData.type) {
ParsedContentType.UNKNOWN -> text.ifEmpty {
bytes.toHexString()
}
ParsedContentType.URL -> parsedData.valueFor(R.string.host)
?: text
ParsedContentType.WIFI_NETWORK -> parsedData.valueFor(R.string.wifi_ssid)
?: parsedData.firstValue()
?: text
ParsedContentType.EMAIL -> parsedData.valueFor(R.string.email_to)
?: parsedData.valueFor(R.string.email_subject)
?: parsedData.firstValue()
?: text
ParsedContentType.OTP -> parsedData.valueFor(R.string.otp_account)
?: parsedData.firstValue()
?: text
else -> parsedData.firstValue() ?: text
}
}
private fun ParsedData.valueFor(key: Int): CharSequence? =
fields.firstValue { it.key == key }
private fun ParsedData.firstValue(): CharSequence? =
fields.firstValue()
private fun List<ParsedField>.firstValue(
predicate: (ParsedField) -> Boolean = { true }
): CharSequence? = firstOrNull {
predicate(it) && !it.value.isNullOrBlank()
}?.value
private fun MutableList<ParsedField>.addField(
key: Any,
value: CharSequence?,
link: String? = null
) {
add(ParsedField(key, value, link))
}
@@ -0,0 +1,82 @@
package de.markusfisch.android.binaryeye.content
import de.markusfisch.android.zxingcpp.ZxingCpp
data class DeutschePostTrackingLink(
val label: String,
val link: String
)
object DeutschePostParser {
fun parse(
bytes: ByteArray,
format: String
): DeutschePostTrackingLink? {
var isStamp = false
var rawData = bytes
if (format == ZxingCpp.BarcodeFormat.DataMatrix.name &&
bytes.toString(Charsets.ISO_8859_1).startsWith("DEA5")
) {
if (bytes.size == 47) {
isStamp = true
} else if (bytes.size > 47) {
// Transform back to original data.
rawData = bytes.toString(Charsets.UTF_8).toByteArray(
Charsets.ISO_8859_1
)
if (rawData.size == 47) {
isStamp = true
}
}
}
if (!isStamp) {
return null
}
val hex = StringBuilder()
hex.append(String.format("%02X", rawData[9]))
hex.append(String.format("%02X", rawData[10]))
hex.append(String.format("%02X", rawData[11]))
hex.append(String.format("%02X", rawData[12]))
hex.append(String.format("%02X", rawData[13]))
hex.append(String.format("%X", (rawData[4].toInt() and 0x0f).toByte()))
hex.append(String.format("%02X", rawData[5]))
hex.append(String.format("%02X", rawData[6]))
hex.append(String.format("%02X", rawData[7]))
hex.append(String.format("%02X", rawData[8]))
val hexString = hex.toString()
val trackingNumber = hexString + String.format(
"%X",
crc4(hexString.toByteArray(Charsets.ISO_8859_1))
)
return DeutschePostTrackingLink(
trackingNumber,
"https://www.deutschepost.de/de/s/sendungsverfolgung.html?piececode=$trackingNumber",
)
}
// CRC-4 with polynomial x^4 + x + 1.
private fun crc4(input: ByteArray): Int {
var crc = 0
var i = 0
while (i < input.size) {
val c = input[i].toInt()
var j = 0x80
while (j != 0) {
var bit = crc and 0x8
crc = crc shl 1
if (c and j != 0) {
bit = bit xor 0x8
}
if (bit != 0) {
crc = crc xor 0x3
}
j = j ushr 1
}
++i
}
crc = crc and 0xF
return crc
}
}
@@ -0,0 +1,23 @@
package de.markusfisch.android.binaryeye.content
import android.content.Context
import de.markusfisch.android.binaryeye.R
private val elementToLabel = mapOf(
EpcQrElement.SERVICE_TAG to R.string.epc_service_tag,
EpcQrElement.VERSION to R.string.epc_version,
EpcQrElement.CHARSET to R.string.epc_charset,
EpcQrElement.IDENTIFICATION to R.string.epc_identification,
EpcQrElement.BIC to R.string.epc_bic,
EpcQrElement.NAME to R.string.epc_name,
EpcQrElement.IBAN to R.string.epc_iban,
EpcQrElement.AMOUNT to R.string.epc_amount,
EpcQrElement.PURPOSE to R.string.epc_purpose,
EpcQrElement.REFERENCE to R.string.epc_reference,
EpcQrElement.TEXT to R.string.epc_text,
EpcQrElement.INFORMATION to R.string.epc_information
)
fun Context.epcQrToRes(element: EpcQrElement): String {
return elementToLabel[element]?.let { getString(it) } ?: element.name
}
@@ -0,0 +1,141 @@
package de.markusfisch.android.binaryeye.content
enum class EpcQrElement {
SERVICE_TAG, VERSION, CHARSET, IDENTIFICATION,
BIC, NAME, IBAN, AMOUNT, PURPOSE, REFERENCE, TEXT, INFORMATION
}
object EpcQrParser {
private val BIC_REGEX = Regex("[A-Z0-9]{8}([A-Z0-9]{3})?")
private val IBAN_REGEX = Regex("[A-Z]{2}[0-9A-Z]{13,32}")
private val AMOUNT_REGEX = Regex("EUR\\d{1,10}(\\.\\d{2})?")
private val PURPOSE_REGEX = Regex("[A-Z]{4}")
private val charsetMap = mapOf(
"1" to "UTF-8",
"2" to "ISO 8859-1",
"3" to "ISO 8859-2",
"4" to "ISO 8859-4",
"5" to "ISO 8859-5",
"6" to "ISO 8859-7",
"7" to "ISO 8859-10",
"8" to "ISO 8859-15"
)
private fun splitLines(s: String): List<String>? {
val normalized = s
.replace("\r\n", "\n")
.replace('\r', '\n')
.removeSuffix("\n")
if (normalized.isEmpty()) {
return null
}
return normalized.split('\n')
}
private fun isValidVersion(value: String): Boolean {
return value == "001" || value == "002"
}
private fun isValidCharset(value: String): Boolean {
return charsetMap.containsKey(value)
}
private fun isValidIdentification(value: String): Boolean {
return value == "SCT"
}
private fun isValidBic(value: String): Boolean {
return value.matches(BIC_REGEX)
}
private fun isValidIban(value: String): Boolean {
return value.matches(IBAN_REGEX)
}
private fun isValidAmount(value: String): Boolean {
return value.matches(AMOUNT_REGEX)
}
fun parse(s: String?): Map<EpcQrElement, String>? {
if (s.isNullOrEmpty()) {
return null
}
val lines = splitLines(s) ?: return null
if (lines.size !in 7..12) {
return null
}
if (lines[0] != "BCD") {
return null
}
if (!isValidVersion(lines[1])) {
return null
}
if (!isValidCharset(lines[2])) {
return null
}
if (!isValidIdentification(lines[3])) {
return null
}
val bic = lines.getOrElse(4) { "" }
if ((lines[1] == "001" || bic.isNotEmpty()) && !isValidBic(bic)) {
return null
}
val recipient = lines.getOrElse(5) { "" }
if (recipient.isBlank() || recipient.length > 70) {
return null
}
val iban = lines.getOrElse(6) { "" }
if (!isValidIban(iban)) {
return null
}
val amount = lines.getOrElse(7) { "" }
if (amount.isNotEmpty() && !isValidAmount(amount)) {
return null
}
val purpose = lines.getOrElse(8) { "" }
if (purpose.isNotEmpty() && !purpose.matches(PURPOSE_REGEX)) {
return null
}
val remittanceReference = lines.getOrElse(9) { "" }
if (remittanceReference.length > 25) {
return null
}
val remittanceText = lines.getOrElse(10) { "" }
if (remittanceText.length > 140) {
return null
}
if (remittanceReference.isNotEmpty() && remittanceText.isNotEmpty()) {
return null
}
val information = lines.getOrElse(11) { "" }
if (information.length > 70) {
return null
}
return linkedMapOf(
EpcQrElement.SERVICE_TAG to lines[0],
EpcQrElement.VERSION to lines[1],
EpcQrElement.CHARSET to (charsetMap[lines[2]] ?: lines[2]),
EpcQrElement.IDENTIFICATION to lines[3],
EpcQrElement.BIC to bic,
EpcQrElement.NAME to recipient,
EpcQrElement.IBAN to iban,
EpcQrElement.AMOUNT to amount.removePrefix("EUR"),
EpcQrElement.PURPOSE to purpose,
EpcQrElement.REFERENCE to remittanceReference,
EpcQrElement.TEXT to remittanceText,
EpcQrElement.INFORMATION to information
)
}
}
@@ -0,0 +1,191 @@
package de.markusfisch.android.binaryeye.content
import android.content.Context
import de.markusfisch.android.binaryeye.R
import de.tsenger.vdstools.DataEncoder
import de.tsenger.vdstools.Verifier
import de.tsenger.vdstools.idb.IdbSeal
import org.bouncycastle.asn1.ASN1ObjectIdentifier
import org.bouncycastle.asn1.icao.CscaMasterList
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo
import org.bouncycastle.asn1.x9.ECNamedCurveTable
import org.bouncycastle.asn1.x9.X962Parameters
import org.bouncycastle.cert.X509CertificateHolder
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter
import org.bouncycastle.cms.CMSSignedData
import org.bouncycastle.jce.provider.BouncyCastleProvider
import java.security.Provider
import java.security.Security
import java.security.cert.X509Certificate
import java.util.Date
object IdbVerifier {
private const val MASTER_LIST_ASSET = "DE_ML_2026-01-08-12-20-54.ml"
private val provider: Provider by lazy {
BouncyCastleProvider().also { bcProvider ->
if (Security.getProvider(bcProvider.name) == null) {
Security.addProvider(bcProvider)
}
}
}
@Volatile
private var cachedMasterListCertificates: List<X509Certificate>? = null
fun verify(context: Context, seal: IdbSeal): List<SealField> {
val signatureInfo = seal.signatureInfo
?: return listOf(
SealField(
R.string.idb_verification,
context.getString(R.string.idb_verification_unsigned)
)
)
val headerCertificateReference = seal.signerCertReference
?.hexToByteArrayOrNull()
val signerCertificate = context.findCertificateByReference(
headerCertificateReference
)
val curveName = signerCertificate?.let {
curveNameFrom(it)
}
val signedBytes = seal.signedBytes
val signatureResult = if (
signerCertificate == null ||
curveName.isNullOrBlank() ||
signedBytes == null
) {
Verifier.Result.VerifyError
} else {
Verifier(
signedBytes,
signatureInfo.plainSignatureBytes,
signerCertificate.publicKey.encoded,
curveName
).verify()
}
val referenceMatches = signerCertificate != null &&
headerCertificateReference != null &&
DataEncoder.buildCertificateReference(signerCertificate.encoded)
.contentEquals(headerCertificateReference)
val certificateValidNow = signerCertificate != null && runCatching {
signerCertificate.checkValidity(Date())
true
}.getOrElse {
false
}
val verification = context.getString(
when {
signerCertificate == null ->
R.string.idb_verification_signer_certificate_issuer_not_found_in_master_list
!referenceMatches ->
R.string.idb_verification_signature_invalid
!certificateValidNow ->
R.string.idb_verification_signature_invalid
signatureResult == Verifier.Result.SignatureValid ->
R.string.idb_verification_verified
signatureResult == Verifier.Result.SignatureInvalid ->
R.string.idb_verification_signature_invalid
else -> R.string.idb_verification_error
}
)
val fields = mutableListOf(
SealField(R.string.idb_verification, verification),
SealField(
R.string.idb_certificate_reference,
signatureInfo.signerCertificateReference
),
SealField(
R.string.vds_signing_date,
signatureInfo.signingDate.toString()
),
SealField(
R.string.idb_signature_verification,
signatureResult.name
),
)
if (signerCertificate != null) {
fields.add(
SealField(
R.string.idb_signer_certificate_subject,
signerCertificate.subjectX500Principal.name
)
)
fields.add(
SealField(
R.string.idb_signer_certificate_issuer,
signerCertificate.issuerX500Principal.name
)
)
}
return fields
}
private fun String.hexToByteArrayOrNull(): ByteArray? {
if (length % 2 != 0) {
return null
}
return runCatching {
ByteArray(length / 2) { index ->
substring(index * 2, index * 2 + 2).toInt(16).toByte()
}
}.getOrNull()
}
private fun Context.loadMasterListCertificates(): List<X509Certificate> {
cachedMasterListCertificates?.let {
return it
}
synchronized(this) {
cachedMasterListCertificates?.let {
return it
}
val cmsSignedData = assets.open(MASTER_LIST_ASSET).use { input ->
CMSSignedData(input.readBytes())
}
val signedContent = cmsSignedData.signedContent?.content as? ByteArray
?: throw IllegalArgumentException("Master list does not contain signed content")
val masterList = CscaMasterList.getInstance(signedContent)
val certificates = masterList.certStructs.map { cert ->
JcaX509CertificateConverter()
.setProvider(provider)
.getCertificate(X509CertificateHolder(cert))
}
cachedMasterListCertificates = certificates
return certificates
}
}
private fun Context.findCertificateByReference(
certificateReference: ByteArray?
): X509Certificate? {
if (certificateReference == null || certificateReference.size != 5) {
return null
}
return runCatching {
loadMasterListCertificates().firstOrNull { certificate ->
DataEncoder.buildCertificateReference(certificate.encoded)
.contentEquals(certificateReference)
}
}.getOrNull()
}
private fun curveNameFrom(certificate: X509Certificate): String? {
return runCatching {
val subjectPublicKeyInfo = SubjectPublicKeyInfo.getInstance(
certificate.publicKey.encoded
)
val params = X962Parameters.getInstance(subjectPublicKeyInfo.algorithm.parameters)
val oid = params.parameters as? ASN1ObjectIdentifier
oid?.let {
ECNamedCurveTable.getName(it)
}
}.getOrNull()
}
}
@@ -96,15 +96,30 @@ object IdlParser {
val data = findSubType(s, idlInfo)
val len = data.length
var start = 0
var skipSubtype = false
for (p in 0..len) {
if (p < len && data[p] > '\u001f') {
continue
}
if (p - start > 3) {
val vp = start + 3
val key = data.substring(start, vp)
// Per spec, CR (0x0d) is the subfile boundary marker. The segment
// immediately following a CR may begin with a 2-char subfile type
// identifier (e.g. "ZV", "ZI") prepended to the first field.
// Detect this by checking that the marker matches the key prefix
// (jurisdiction fields always share their 2-char subfile prefix).
val seg = if (skipSubtype && p - start >= 4 &&
data[start] == data[start + 2] &&
data[start + 1] == data[start + 3]
) {
start + 2
} else {
start
}
if (p - seg > 3) {
val vp = seg + 3
val key = data.substring(seg, vp)
if (key.matches(Regex("[DZ][A-Z]{2}"))) {
var value = data.substring(vp, p)
@@ -114,6 +129,7 @@ object IdlParser {
idlInfo.elements[key] = value.trim()
}
}
skipSubtype = p < len && data[p] == '\r'
start = p + 1
}
@@ -0,0 +1,142 @@
package de.markusfisch.android.binaryeye.content
import android.content.Context
import de.markusfisch.android.binaryeye.R
import de.tsenger.vdstools.asn1.DerTlv
import de.tsenger.vdstools.generic.Message
import de.tsenger.vdstools.generic.MessageCoding
import de.tsenger.vdstools.generic.MessageValue
import de.tsenger.vdstools.generic.Seal
import de.tsenger.vdstools.idb.IdbSeal
import org.json.JSONArray
import org.json.JSONObject
import de.tsenger.vdstools.generic.SealParser as VdsSealParser
data class SealField(val name: Any, val value: String)
private data class IdbChildMessageDefinition(
val name: String,
val coding: MessageCoding
)
object SealParser {
private val idbChildMessagesByName: Map<String, Map<Int, IdbChildMessageDefinition>> by lazy {
loadIdbChildMessagesByName()
}
fun parse(
context: Context,
bytes: ByteArray
): List<SealField>? = try {
context.buildFields(
VdsSealParser().parse(bytes.toString(Charsets.ISO_8859_1))
)
} catch (_: Exception) {
null
}
private fun Context.buildFields(seal: Seal): List<SealField> = buildList {
add(SealField(R.string.vds_document_type, seal.documentType))
add(SealField(R.string.vds_issuing_country, seal.issuingCountry))
if (seal is IdbSeal) {
addAll(IdbVerifier.verify(this@buildFields, seal))
}
for (message in seal.messageList) {
addAll(message.toFields())
}
}
private fun Message.toFields(): List<SealField> = buildList {
add(
if (value is MessageValue.BytesValue) {
SealField(
"$name (${value.rawBytes.size} bytes)",
value.rawBytes.toHexString().uppercase()
)
} else {
SealField(name, value.toString())
}
)
for (child in childMessages()) {
addAll(child.toFields())
}
}
private fun Message.childMessages(): List<Message> {
val childDefinitionsByTag = idbChildMessagesByName[name] ?: return emptyList()
return runCatching {
DerTlv.parseAll(value.rawBytes).mapNotNull { derTlv ->
val childTag = derTlv.tag.toInt() and 0xff
val childDefinition = childDefinitionsByTag[childTag]
?: return@mapNotNull null
Message(
childTag,
childDefinition.name,
childDefinition.coding,
MessageValue.fromBytes(
derTlv.value,
childDefinition.coding
)
)
}
}.getOrElse {
emptyList()
}
}
private fun loadIdbChildMessagesByName(): Map<String, Map<Int, IdbChildMessageDefinition>> {
val resourceConstantsClass = runCatching {
Class.forName("de.tsenger.vdstools.generated.ResourceConstants")
}.getOrElse {
return emptyMap()
}
val json = runCatching {
resourceConstantsClass.getField(
"IDB_MESSAGE_TYPES_JSON"
).get(null) as String
}.getOrElse {
return emptyMap()
}
val childMessagesByName =
mutableMapOf<String, Map<Int, IdbChildMessageDefinition>>()
val messageTypes = JSONArray(json)
for (i in 0 until messageTypes.length()) {
val messageType = messageTypes.getJSONObject(i)
val messages = messageType.optJSONArray("messages") ?: continue
collectChildMessages(
messageType.getString("name"),
messages,
childMessagesByName
)
}
return childMessagesByName
}
private fun collectChildMessages(
parentName: String,
messages: JSONArray,
childMessagesByName: MutableMap<String, Map<Int, IdbChildMessageDefinition>>
) {
val childMessages = mutableMapOf<Int, IdbChildMessageDefinition>()
for (i in 0 until messages.length()) {
val message = messages.getJSONObject(i)
val childDefinition = message.toChildMessageDefinition()
childMessages[message.getInt("tag")] = childDefinition
val nestedMessages = message.optJSONArray("messages") ?: continue
collectChildMessages(
childDefinition.name,
nestedMessages,
childMessagesByName
)
}
if (childMessages.isNotEmpty()) {
childMessagesByName[parentName] = childMessages
}
}
private fun JSONObject.toChildMessageDefinition(): IdbChildMessageDefinition {
return IdbChildMessageDefinition(
getString("name"),
MessageCoding.valueOf(getString("coding"))
)
}
}
@@ -9,12 +9,13 @@ import androidx.core.content.FileProvider
import androidx.core.net.toUri
import de.markusfisch.android.binaryeye.BuildConfig
import de.markusfisch.android.binaryeye.R
import de.markusfisch.android.binaryeye.app.prefs
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 (
fun Context.startIntentOrToast(intent: Intent): Boolean = if (
!startIntent(intent)
) {
toast(R.string.cannot_resolve_action)
@@ -43,24 +44,27 @@ fun Context.openUrl(url: String, silent: Boolean = false): Boolean {
}
fun Context.openUri(uri: Uri, silent: Boolean = false): Boolean {
val intent = Intent(Intent.ACTION_VIEW, uri).apply {
if (uri.scheme == "content") {
val cleaned = if (prefs.stripTrackingParams) {
uri.stripTrackingParams()
} else {
uri
}
val intent = Intent(Intent.ACTION_VIEW, cleaned).apply {
if (cleaned.scheme == "content") {
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
}
}
return if (silent) {
startIntent(intent)
} else {
execShareIntent(intent)
startIntentOrToast(intent)
}
}
fun String.parseAndNormalizeUri(): Uri = toUri().let {
it.normalizeScheme()
}
fun String.parseAndNormalizeUri(): Uri = toUri().normalizeScheme()
fun Context.shareText(text: String, mimeType: String = "text/plain") {
execShareIntent(Intent(Intent.ACTION_SEND).apply {
startIntentOrToast(Intent(Intent.ACTION_SEND).apply {
putExtra(Intent.EXTRA_TEXT, text)
type = mimeType
})
@@ -123,7 +127,7 @@ private fun Context.getUriForFile(file: File): Uri? = if (
}
private fun Context.shareUri(uri: Uri, mimeType: String) {
execShareIntent(Intent(Intent.ACTION_SEND).apply {
startIntentOrToast(Intent(Intent.ACTION_SEND).apply {
putExtra(Intent.EXTRA_STREAM, uri)
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
type = mimeType
@@ -0,0 +1,44 @@
package de.markusfisch.android.binaryeye.content
import android.net.Uri
private val TRACKING_PARAMS = setOf(
"utm_source", "utm_medium", "utm_campaign", "utm_term", "utm_content",
"utm_id", "utm_name", "utm_referrer", "utm_brand", "utm_social",
"fbclid", "gclid", "dclid", "gclsrc", "yclid", "msclkid",
"mc_eid", "mc_cid",
"_ga", "_gl", "ga_source", "ga_medium",
"igshid", "igsh",
"hsa_acc", "hsa_cam", "hsa_grp", "hsa_ad", "hsa_src",
"hsa_tgt", "hsa_kw", "hsa_mt", "hsa_net", "hsa_ver",
"spm", "scm", "vero_id", "vero_conv"
)
fun Uri.stripTrackingParams(): Uri {
val scheme = scheme?.lowercase()
if (scheme != "http" && scheme != "https") {
return this
}
val queryNames = runCatching {
queryParameterNames
}.getOrDefault(emptySet())
if (queryNames.isEmpty()) {
return this
}
val keep = queryNames.filter { paramName ->
paramName.lowercase() !in TRACKING_PARAMS
}
if (keep.size == queryNames.size) {
return this
}
val builder = buildUpon().clearQuery()
for (paramName in keep) {
getQueryParameters(paramName).forEach { paramValue ->
builder.appendQueryParameter(paramName, paramValue)
}
}
return builder.build()
}
@@ -6,15 +6,22 @@ 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.binaryeye.content.ParsedContentType
import de.markusfisch.android.binaryeye.content.parseData
import de.markusfisch.android.binaryeye.content.parsedContentTypeFromName
import de.markusfisch.android.binaryeye.preference.Preferences
import de.markusfisch.android.binaryeye.zxingcpp.migrateBarcodeFormatName
import de.markusfisch.android.binaryeye.zxingcpp.oldToNewFormatNames
import de.markusfisch.android.zxingcpp.ZxingCpp.BarcodeFormat
class Database {
private lateinit var db: SQLiteDatabase
private lateinit var context: Context
fun open(context: Context) {
this.context = context.applicationContext
db = OpenHelper(context).writableDatabase
fillMissingContentTypes()
}
fun getScans(query: String? = null): Cursor = db.rawQuery(
@@ -24,6 +31,8 @@ class Database {
$SCANS_DATETIME,
$SCANS_NAME,
$SCANS_TEXT,
$SCANS_RAW,
$SCANS_CONTENT_TYPE,
$SCANS_FORMAT
FROM $SCANS
${getWhereClause(query)}
@@ -48,7 +57,8 @@ class Database {
$SCANS_GTIN_COUNTRY,
$SCANS_GTIN_ADD_ON,
$SCANS_GTIN_PRICE,
$SCANS_GTIN_ISSUE_NUMBER
$SCANS_GTIN_ISSUE_NUMBER,
$SCANS_CONTENT_TYPE
FROM $SCANS
${getWhereClause(query)}
ORDER BY $SCANS_PINNED DESC, $SCANS_DATETIME DESC
@@ -60,7 +70,8 @@ class Database {
prefix: String = "WHERE"
) = if (query?.isNotEmpty() == true) {
"""$prefix $SCANS_TEXT LIKE ?
OR $SCANS_NAME LIKE ?"""
OR $SCANS_NAME LIKE ?
OR $SCANS_FORMAT LIKE ?"""
} else {
""
}
@@ -69,7 +80,7 @@ class Database {
query: String?
) = if (query?.isNotEmpty() == true) {
val instr = "%$query%"
arrayOf(instr, instr)
arrayOf(instr, instr, instr)
} else {
null
}
@@ -91,7 +102,8 @@ class Database {
$SCANS_GTIN_COUNTRY,
$SCANS_GTIN_ADD_ON,
$SCANS_GTIN_PRICE,
$SCANS_GTIN_ISSUE_NUMBER
$SCANS_GTIN_ISSUE_NUMBER,
$SCANS_CONTENT_TYPE
FROM $SCANS
WHERE $SCANS_ID IN (${ids.joinToString(",")})
ORDER BY $SCANS_PINNED DESC, $SCANS_DATETIME DESC
@@ -119,7 +131,8 @@ class Database {
$SCANS_GTIN_COUNTRY,
$SCANS_GTIN_ADD_ON,
$SCANS_GTIN_PRICE,
$SCANS_GTIN_ISSUE_NUMBER
$SCANS_GTIN_ISSUE_NUMBER,
$SCANS_CONTENT_TYPE
FROM $SCANS
WHERE $SCANS_ID = ?
""".trimMargin(), arrayOf("$id")
@@ -149,7 +162,8 @@ class Database {
it.getString(SCANS_DATETIME),
it.getLong(SCANS_ID),
it.getString(SCANS_NAME),
it.getInt(SCANS_PINNED) != 0
it.getInt(SCANS_PINNED) != 0,
parsedContentTypeFromName(it.getString(SCANS_CONTENT_TYPE))
)
} else {
null
@@ -206,6 +220,10 @@ class Database {
scan.price?.let { put(SCANS_GTIN_PRICE, it) }
scan.issueNumber?.let { put(SCANS_GTIN_ISSUE_NUMBER, it) }
put(SCANS_PINNED, if (scan.pinned) 1 else 0)
put(
SCANS_CONTENT_TYPE,
getContentType(scan.text, scan.raw, scan.format.name).name
)
}
)
}
@@ -277,8 +295,52 @@ class Database {
db.update(SCANS, cv, "$SCANS_ID = ?", arrayOf("$id"))
}
private fun fillMissingContentTypes() {
Thread {
db.rawQuery(
"""SELECT
$SCANS_ID,
$SCANS_TEXT,
$SCANS_RAW,
$SCANS_FORMAT
FROM $SCANS
WHERE $SCANS_CONTENT_TYPE IS NULL
""".trimMargin(),
null
).use {
while (it.moveToNext()) {
val id = it.getLong(SCANS_ID)
val type = getContentType(
it.getString(SCANS_TEXT),
it.getBlob(SCANS_RAW),
it.getString(SCANS_FORMAT)
)
db.update(
SCANS,
ContentValues().apply {
put(SCANS_CONTENT_TYPE, type.name)
},
"$SCANS_ID = ?",
arrayOf("$id")
)
}
}
}.start()
}
private fun getContentType(
text: String,
raw: ByteArray?,
format: String
) = parseData(
context,
text,
raw ?: text.toByteArray(),
format
).type
private class OpenHelper(context: Context) :
SQLiteOpenHelper(context, FILE_NAME, null, 10) {
SQLiteOpenHelper(context, FILE_NAME, null, 11) {
override fun onCreate(db: SQLiteDatabase) {
db.createScans()
}
@@ -315,6 +377,9 @@ class Database {
if (oldVersion < 10) {
db.addPinnedColumn()
}
if (oldVersion < 11) {
db.addContentTypeColumn()
}
}
}
@@ -349,6 +414,7 @@ class Database {
const val SCANS_GTIN_PRICE = "gtin_price"
const val SCANS_GTIN_ISSUE_NUMBER = "gtin_issue_number"
const val SCANS_PINNED = "pinned"
const val SCANS_CONTENT_TYPE = "content_type"
private fun SQLiteDatabase.createScans() {
execSQL("DROP TABLE IF EXISTS $SCANS".trimMargin())
@@ -373,7 +439,8 @@ class Database {
$SCANS_GTIN_ADD_ON TEXT,
$SCANS_GTIN_PRICE TEXT,
$SCANS_GTIN_ISSUE_NUMBER TEXT,
$SCANS_PINNED INTEGER NOT NULL DEFAULT 0
$SCANS_PINNED INTEGER NOT NULL DEFAULT 0,
$SCANS_CONTENT_TYPE TEXT NOT NULL DEFAULT '${ParsedContentType.UNKNOWN.name}'
)""".trimMargin()
)
}
@@ -415,35 +482,7 @@ class Database {
}
private fun SQLiteDatabase.migrateToNativeFormatNames() {
val mappings = listOf(
"NONE" to "None",
"AZTEC" to "Aztec",
"CODABAR" to "Codabar",
"CODE_39" to "Code39",
"CODE_39_EXT" to "Code39Ext",
"CODE_32" to "Code32",
"PZN" to "PZN",
"CODE_93" to "Code93",
"CODE_128" to "Code128",
"DATA_BAR_OMNI" to "DataBarOmni",
"DATA_BAR_STK" to "DataBarStk",
"DATA_BAR_LTD" to "DataBarLtd",
"DATA_BAR_EXPANDED" to "DataBarExp",
"DATA_BAR_EXP_STK" to "DataBarExpStk",
"DATA_MATRIX" to "DataMatrix",
"DX_FILM_EDGE" to "DXFilmEdge",
"EAN_8" to "EAN8",
"EAN_13" to "EAN13",
"ITF" to "ITF",
"MAXICODE" to "MaxiCode",
"PDF_417" to "PDF417",
"QR_CODE" to "QRCode",
"MICRO_QR_CODE" to "MicroQRCode",
"RMQR_CODE" to "RMQRCode",
"UPC_A" to "UPCA",
"UPC_E" to "UPCE"
)
for ((oldName, newName) in mappings) {
for ((oldName, newName) in oldToNewFormatNames) {
execSQL(
"UPDATE $SCANS SET $SCANS_FORMAT = ? WHERE $SCANS_FORMAT = ?",
arrayOf(newName, oldName)
@@ -472,5 +511,9 @@ class Database {
"ALTER TABLE $SCANS ADD COLUMN $SCANS_PINNED INTEGER NOT NULL DEFAULT 0"
)
}
private fun SQLiteDatabase.addContentTypeColumn() {
execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_CONTENT_TYPE TEXT")
}
}
}
@@ -2,6 +2,8 @@ package de.markusfisch.android.binaryeye.database
import android.os.Bundle
import android.text.format.DateFormat
import de.markusfisch.android.binaryeye.content.ParsedContentType
import de.markusfisch.android.binaryeye.content.parsedContentTypeFromName
import de.markusfisch.android.binaryeye.zxingcpp.migrateBarcodeFormatName
import de.markusfisch.android.zxingcpp.ZxingCpp.BarcodeFormat
import de.markusfisch.android.zxingcpp.ZxingCpp.BitMatrix
@@ -27,7 +29,8 @@ data class Scan(
val dateTime: String = getDateTime(),
var id: Long = 0L,
val label: String? = null,
val pinned: Boolean = false
val pinned: Boolean = false,
val contentType: ParsedContentType = ParsedContentType.UNKNOWN
) {
// Needs to be overwritten manually, as ByteArray is an array and
// this isn't handled well by Kotlin.
@@ -59,7 +62,8 @@ data class Scan(
price == other.price &&
issueNumber == other.issueNumber &&
label == other.label &&
pinned == other.pinned
pinned == other.pinned &&
contentType == other.contentType
}
// Needs to be overwritten manually, as ByteArray is an array and
@@ -82,6 +86,7 @@ data class Scan(
result = 31 * result + (issueNumber?.hashCode() ?: 0)
result = 31 * result + (label?.hashCode() ?: 0)
result = 31 * result + pinned.hashCode()
result = 31 * result + contentType.hashCode()
return result
}
@@ -133,6 +138,7 @@ fun Scan.toBundle() = Bundle().apply {
putLong(SCAN_ID, id)
putString(SCAN_LABEL, label)
putBoolean(SCAN_PINNED, pinned)
putString(SCAN_CONTENT_TYPE, contentType.name)
}
fun Bundle.toScan(): Scan? {
@@ -159,7 +165,8 @@ fun Bundle.toScan(): Scan? {
dateTime = getString(SCAN_DATE_TIME) ?: getDateTime(),
id = getLong(SCAN_ID, 0L),
label = getString(SCAN_LABEL),
pinned = getBoolean(SCAN_PINNED, false)
pinned = getBoolean(SCAN_PINNED, false),
contentType = parsedContentTypeFromName(getString(SCAN_CONTENT_TYPE))
)
} catch (_: IllegalArgumentException) {
null
@@ -207,3 +214,4 @@ private const val SCAN_DATE_TIME = "date_time"
private const val SCAN_ID = "id"
private const val SCAN_LABEL = "label"
private const val SCAN_PINNED = "pinned"
private const val SCAN_CONTENT_TYPE = "content_type"
@@ -1,30 +1,38 @@
package de.markusfisch.android.binaryeye.fragment
import android.app.Activity
import android.app.AlertDialog
import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
import android.content.SharedPreferences.OnSharedPreferenceChangeListener
import android.database.Cursor
import android.net.Uri
import android.net.wifi.WifiManager
import android.os.Build
import android.os.Bundle
import android.provider.OpenableColumns
import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.RequiresApi
import androidx.core.net.toUri
import androidx.preference.EditTextPreference
import androidx.preference.ListPreference
import androidx.preference.MultiSelectListPreference
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
import androidx.preference.PreferenceGroup
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import de.markusfisch.android.binaryeye.R
import de.markusfisch.android.binaryeye.activity.AutomatedActionsActivity
import de.markusfisch.android.binaryeye.activity.IgnoreCodesActivity
import de.markusfisch.android.binaryeye.activity.NetworkSuggestionsActivity
import de.markusfisch.android.binaryeye.activity.ProfilesActivity
import de.markusfisch.android.binaryeye.activity.SplashActivity
import de.markusfisch.android.binaryeye.adapter.prettifyFormatName
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.Preferences
import de.markusfisch.android.binaryeye.preference.UrlPreference
import de.markusfisch.android.binaryeye.view.setPaddingFromWindowInsets
import de.markusfisch.android.binaryeye.view.systemBarRecyclerViewScrollListener
@@ -32,6 +40,13 @@ import de.markusfisch.android.binaryeye.widget.toast
class PreferencesFragment : PreferenceFragmentCompat() {
private var lastProfile: String? = null
private val pickBeepTone = registerForActivityResult(
ActivityResultContracts.OpenDocument()
) { uri ->
if (uri != null) {
setCustomBeepTone(uri)
}
}
private val changeListener = object : OnSharedPreferenceChangeListener {
override fun onSharedPreferenceChanged(
@@ -44,14 +59,16 @@ class PreferencesFragment : PreferenceFragmentCompat() {
prefs.update()
}
when (preference.key) {
"custom_locale" -> {
CUSTOM_LOCALE,
DYNAMIC_COLORS -> {
activity?.restartApp()
return
}
"beep_tone_name" -> beepConfirm()
BEEP_TONE_NAME,
BEEP_STREAM_NAME -> activity?.beepConfirm()
"send_scan_bluetooth" -> if (
SEND_SCAN_BLUETOOTH -> if (
prefs.sendScanBluetooth &&
activity?.hasBluetoothPermission() == false
) {
@@ -83,11 +100,15 @@ class PreferencesFragment : PreferenceFragmentCompat() {
setSummaries(preferenceScreen)
setIcons()
wireBeepTonePicker()
wireProfiles()
wireAutomatedActions()
wireIgnoreCodes()
setBluetoothResources()
wireClearNetworkPreferences()
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
findPreference<PreferenceGroup>("appearance_category")?.isVisible = false
}
}
private fun wireProfiles() {
@@ -100,6 +121,27 @@ class PreferencesFragment : PreferenceFragmentCompat() {
}
}
private fun wireBeepTonePicker() {
findPreference<ListPreference>(BEEP_TONE_NAME)?.apply {
onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue ->
val toneName = newValue as? String ?: return@OnPreferenceChangeListener false
if (toneName == Preferences.CUSTOM_BEEP_TONE_NAME) {
pickBeepTone.launch(arrayOf("audio/*"))
false
} else {
if (prefs.usesCustomBeepTone()) {
clearCustomBeepTone()
}
prefs.beepToneName = toneName
value = toneName
setSummary(this)
activity?.beepConfirm()
false
}
}
}
}
private fun wireAutomatedActions() {
findPreference<Preference>(AUTOMATED_ACTIONS)?.apply {
updateAutomatedActionsSummary(this)
@@ -128,14 +170,14 @@ class PreferencesFragment : PreferenceFragmentCompat() {
if (prefs.sendScanBluetooth &&
activity?.hasBluetoothPermission() == true
) {
findPreference<ListPreference>("send_scan_bluetooth_host")?.let {
findPreference<ListPreference>(SEND_SCAN_BLUETOOTH_HOST)?.let {
setBluetoothHosts(it)
}
}
}
private fun wireClearNetworkPreferences() {
findPreference<Preference>("clear_network_suggestions")?.apply {
findPreference<Preference>(CLEAR_NETWORK_SUGGESTIONS)?.apply {
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.Q) {
// From R+ we can query past network suggestions and
// make them editable.
@@ -151,7 +193,7 @@ class PreferencesFragment : PreferenceFragmentCompat() {
// adding network suggestions on Q as well, so we
// need to keep this option.
setOnPreferenceClickListener {
context?.askToClearNetworkSuggestions()
context.askToClearNetworkSuggestions()
true
}
} else {
@@ -163,7 +205,7 @@ class PreferencesFragment : PreferenceFragmentCompat() {
@RequiresApi(Build.VERSION_CODES.Q)
private fun Context.askToClearNetworkSuggestions() {
AlertDialog.Builder(this)
MaterialAlertDialogBuilder(this)
.setMessage(R.string.really_remove_all_networks)
.setPositiveButton(android.R.string.ok) { _, _ ->
clearNetworkSuggestions()
@@ -218,7 +260,7 @@ class PreferencesFragment : PreferenceFragmentCompat() {
setTargetFragment(this@PreferencesFragment, 0)
show(fm, null)
}
} else if (preference.key == "send_scan_bluetooth_host") {
} else if (preference.key == SEND_SCAN_BLUETOOTH_HOST) {
val ac = activity ?: return
if (ac.hasBluetoothPermission()) {
setBluetoothHosts(preference as ListPreference)
@@ -237,35 +279,58 @@ class PreferencesFragment : PreferenceFragmentCompat() {
}
private fun setSummary(preference: Preference) {
if (preference.key == AUTOMATED_ACTIONS) {
updateAutomatedActionsSummary(preference)
return
}
if (preference.key == IGNORE_CODES) {
updateIgnoreCodesSummary(preference)
return
when (preference.key) {
AUTOMATED_ACTIONS -> return updateAutomatedActionsSummary(preference)
IGNORE_CODES -> return updateIgnoreCodesSummary(preference)
BEEP_TONE_NAME -> {
setBeepToneSummary(preference)
return
}
}
when (preference) {
is UrlPreference -> {
preference.summary = preference.getUrl()
}
is EditTextPreference -> preference.summary = preference.text
is UrlPreference -> preference.summary = preference.getUrl()
is ListPreference -> preference.setSummary(preference.entry)
is MultiSelectListPreference -> preference.summary =
getSortedSummary(preference)
is ListPreference -> {
preference.setSummary(preference.entry)
}
is MultiSelectListPreference -> {
preference.summary = preference.values.joinToString(", ") {
it.replace(Regex("_"), " ")
}
}
is PreferenceGroup -> {
setSummaries(preference)
}
is PreferenceGroup -> setSummaries(preference)
}
}
private fun setBeepToneSummary(preference: Preference) {
preference.summary = if (prefs.usesCustomBeepTone()) {
getBeepToneLabel(prefs.beepToneUri.toUri())
} else {
findPreference<ListPreference>(BEEP_TONE_NAME)?.let {
val index = it.findIndexOfValue(prefs.beepToneName)
if (index >= 0) {
it.entries[index]
} else {
""
}
} ?: ""
}
}
private fun getSortedSummary(preference: MultiSelectListPreference): String {
val labelsByValue = mutableMapOf<String, String>()
val entries = preference.entries
val entryValues = preference.entryValues
val count = minOf(entries.size, entryValues.size)
for (i in 0 until count) {
labelsByValue[entryValues[i].toString()] = entries[i].toString()
}
return preference.values
.map { value ->
labelsByValue[value] ?: value.prettifyFormatName()
}
.sortedBy { it.lowercase() }
.joinToString(", ")
}
private fun setIcons() {
setIcon(PROFILE, R.drawable.ic_action_profile)
setIcon("formats", R.drawable.ic_action_scan)
@@ -279,7 +344,8 @@ class PreferencesFragment : PreferenceFragmentCompat() {
setIcon("show_toast_in_bulk_mode", R.drawable.ic_action_toast)
setIcon("vibrate", R.drawable.ic_action_vibrate)
setIcon("beep", R.drawable.ic_action_beep)
setIcon("beep_tone_name", R.drawable.ic_action_notification_sound)
setIcon(BEEP_TONE_NAME, R.drawable.ic_action_notification_sound)
setIcon(BEEP_STREAM_NAME, R.drawable.ic_action_notification_sound)
setIcon("use_history", R.drawable.ic_action_history)
setIcon("ignore_duplicates_name", R.drawable.ic_action_remove)
setIcon(IGNORE_CODES, R.drawable.ic_action_remove)
@@ -287,9 +353,11 @@ class PreferencesFragment : PreferenceFragmentCompat() {
setIcon("send_scan_active", R.drawable.ic_action_forward)
setIcon("send_scan_url", R.drawable.ic_action_link)
setIcon("send_scan_type", R.drawable.ic_action_link)
setIcon("send_scan_bluetooth", R.drawable.ic_action_bluetooth)
setIcon("send_scan_bluetooth_host", R.drawable.ic_action_bluetooth)
setIcon("send_scan_device_id", R.drawable.ic_label)
setIcon(SEND_SCAN_BLUETOOTH, R.drawable.ic_action_bluetooth)
setIcon(SEND_SCAN_BLUETOOTH_HOST, R.drawable.ic_action_bluetooth)
setIcon("open_immediately", R.drawable.ic_action_open)
setIcon("strip_tracking_params", R.drawable.ic_action_remove)
setIcon("show_meta_data", R.drawable.ic_action_info)
setIcon("show_hex_dump", R.drawable.ic_action_info)
setIcon("show_checksum", R.drawable.ic_action_info)
@@ -298,9 +366,10 @@ class PreferencesFragment : PreferenceFragmentCompat() {
setIcon("default_search_url", R.drawable.ic_action_search)
setIcon("open_with_url", R.drawable.ic_action_link)
setIcon(AUTOMATED_ACTIONS, R.drawable.ic_action_create)
setIcon("clear_network_suggestions", R.drawable.ic_action_wifi)
setIcon(CLEAR_NETWORK_SUGGESTIONS, R.drawable.ic_action_wifi)
setIcon("brighten_screen", R.drawable.ic_action_bright)
setIcon("custom_locale", R.drawable.ic_action_preferences)
setIcon(CUSTOM_LOCALE, R.drawable.ic_action_preferences)
setIcon(DYNAMIC_COLORS, R.drawable.ic_action_preferences)
}
private fun setIcon(key: String, iconResId: Int) {
@@ -334,14 +403,88 @@ class PreferencesFragment : PreferenceFragmentCompat() {
}
private fun updateProfileSummary(preference: Preference) {
preference.summary = prefs.profile ?: getString(R.string.profile_default)
preference.summary = prefs.profileLabel(requireContext(), prefs.profile)
lastProfile = prefs.profile
}
private fun setCustomBeepTone(uri: Uri) {
val context = requireContext()
val resolver = context.contentResolver
clearCustomBeepTone()
try {
resolver.takePersistableUriPermission(
uri,
Intent.FLAG_GRANT_READ_URI_PERMISSION
)
} catch (e: SecurityException) {
val message = e.message
if (!message.isNullOrEmpty()) {
context.toast(message)
}
return
}
prefs.beepToneUri = uri.toString()
prefs.beepToneName = Preferences.CUSTOM_BEEP_TONE_NAME
findPreference<ListPreference>(BEEP_TONE_NAME)?.apply {
value = Preferences.CUSTOM_BEEP_TONE_NAME
setSummary(this)
}
activity?.beepConfirm()
}
private fun clearCustomBeepTone(releaseSelection: Boolean = true) {
if (releaseSelection) {
releaseBeepTonePermission(prefs.beepToneUri)
}
prefs.beepToneUri = ""
}
private fun releaseBeepTonePermission(uriString: String) {
if (uriString.isEmpty()) {
return
}
try {
requireContext().contentResolver.releasePersistableUriPermission(
uriString.toUri(),
Intent.FLAG_GRANT_READ_URI_PERMISSION
)
} catch (_: SecurityException) {
}
}
private fun getBeepToneLabel(uri: Uri): String {
var cursor: Cursor? = null
return try {
cursor = requireContext().contentResolver.query(
uri,
arrayOf(OpenableColumns.DISPLAY_NAME),
null,
null,
null
)
if (cursor?.moveToFirst() == true) {
cursor.getString(0)
} else {
uri.lastPathSegment ?: uri.toString()
}
} catch (_: Exception) {
uri.lastPathSegment ?: uri.toString()
} finally {
cursor?.close()
}
}
companion object {
private const val PROFILE = "profile"
private const val AUTOMATED_ACTIONS = "automated_actions"
private const val BEEP_TONE_NAME = "beep_tone_name"
private const val BEEP_STREAM_NAME = "beep_stream_name"
private const val CLEAR_NETWORK_SUGGESTIONS = "clear_network_suggestions"
private const val CUSTOM_LOCALE = "custom_locale"
private const val DYNAMIC_COLORS = "dynamic_colors"
private const val IGNORE_CODES = "ignore_codes"
private const val SEND_SCAN_BLUETOOTH = "send_scan_bluetooth"
private const val SEND_SCAN_BLUETOOTH_HOST = "send_scan_bluetooth_host"
}
}
@@ -48,11 +48,14 @@ class UrlDialogFragment : PreferenceDialogFragmentCompat() {
}
textView ?: return
textView.text = ""
Scan(
"test",
null,
BarcodeFormat.None
).sendAsync(url, prefs.sendScanType) { code, body ->
textView.context.sendAsync(
Scan(
"test",
null,
BarcodeFormat.None
),
url
) { code, body ->
textView.text = when {
code != null -> "$code"
body != null -> body
@@ -8,7 +8,7 @@ import de.markusfisch.android.binaryeye.R
fun Context.getDashedBorderPaint() = Paint(Paint.ANTI_ALIAS_FLAG).apply {
val dp = resources.displayMetrics.density
color = ContextCompat.getColor(applicationContext, R.color.crop_bound)
color = ContextCompat.getColor(this@getDashedBorderPaint, R.color.crop_bound)
style = Paint.Style.STROKE
strokeWidth = dp * 2f
pathEffect = DashPathEffect(floatArrayOf(10f * dp, 10f * dp), 0f)
@@ -1,6 +1,7 @@
package de.markusfisch.android.binaryeye.graphics
import android.content.res.Resources
import android.content.res.Resources.Theme
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.drawable.BitmapDrawable
@@ -10,8 +11,13 @@ import androidx.core.graphics.createBitmap
fun Resources.getBitmapFromDrawable(
resId: Int
): Bitmap = getBitmapFromDrawable(resId, null)
fun Resources.getBitmapFromDrawable(
resId: Int,
theme: Theme?
): Bitmap = getBitmapFromDrawable(
ResourcesCompat.getDrawable(this, resId, null) ?: throw Resources.NotFoundException(
ResourcesCompat.getDrawable(this, resId, theme) ?: throw Resources.NotFoundException(
"Drawable resource ID #0x${resId.toString(16)}"
)
)
@@ -40,6 +40,15 @@ fun Rect.setFrameRoi(
frameRoiF.bottom.roundToInt()
)
}
val clampedLeft = left.coerceIn(0, frameMetrics.width)
val clampedTop = top.coerceIn(0, frameMetrics.height)
val clampedRight = right.coerceIn(0, frameMetrics.width)
val clampedBottom = bottom.coerceIn(0, frameMetrics.height)
if (clampedLeft >= clampedRight || clampedTop >= clampedBottom) {
setEmpty()
} else {
set(clampedLeft, clampedTop, clampedRight, clampedBottom)
}
}
fun Matrix.setFrameToView(
@@ -88,3 +97,57 @@ fun Matrix.mapPosition(
mapPoints(coords)
return i
}
fun Rect.setCovered(
viewWidth: Int,
viewHeight: Int,
frameMetrics: FrameMetrics
) {
val frameWidth: Int
val frameHeight: Int
when (frameMetrics.orientation) {
90, 270 -> {
frameWidth = frameMetrics.height
frameHeight = frameMetrics.width
}
else -> {
frameWidth = frameMetrics.width
frameHeight = frameMetrics.height
}
}
if (frameWidth < 1 || frameHeight < 1) {
set(0, 0, 0, 0)
return
}
var coveredWidth = frameWidth
var coveredHeight = frameHeight
if (viewWidth.toLong() * coveredWidth <
viewHeight.toLong() * coveredHeight
) {
coveredWidth = coveredWidth * viewHeight / coveredHeight
coveredHeight = viewHeight
} else {
coveredHeight = coveredHeight * viewWidth / coveredWidth
coveredWidth = viewWidth
}
val left = (viewWidth - coveredWidth) / 2
val top = (viewHeight - coveredHeight) / 2
set(
left,
top,
left + coveredWidth,
top + coveredHeight
)
}
fun Matrix.mapViewYToFrame(viewY: Float): Float {
val values = FloatArray(9)
getValues(values)
val scaleY = values[Matrix.MSCALE_Y]
return if (scaleY != 0f) {
(viewY - values[Matrix.MTRANS_Y]) / scaleY
} else {
viewY
}
}
@@ -1,21 +1,80 @@
package de.markusfisch.android.binaryeye.media
import android.content.Context
import android.media.AudioAttributes
import android.media.AudioManager
import android.media.MediaPlayer
import android.media.ToneGenerator
import androidx.core.net.toUri
import de.markusfisch.android.binaryeye.app.prefs
private var confirmToneGenerator: ToneGenerator? = null
private var confirmToneStream = AudioManager.STREAM_MUSIC
private var confirmMediaPlayer: MediaPlayer? = null
private var errorToneGenerator: ToneGenerator? = null
fun beepConfirm() {
val tg = confirmToneGenerator ?: ToneGenerator(
AudioManager.STREAM_MUSIC,
ToneGenerator.MAX_VOLUME
)
fun Context.beepConfirm() {
val stream = prefs.beepStream()
if (prefs.usesCustomBeepTone() &&
playCustomConfirmTone(prefs.beepToneUri, stream)
) {
return
}
val tg = if (confirmToneGenerator == null || confirmToneStream != stream) {
confirmToneGenerator?.release()
ToneGenerator(stream, ToneGenerator.MAX_VOLUME).also {
confirmToneGenerator = it
confirmToneStream = stream
}
} else {
confirmToneGenerator!!
}
confirmToneGenerator = tg
tg.startTone(prefs.beepTone())
}
private fun Context.playCustomConfirmTone(
uriString: String,
stream: Int
): Boolean {
return try {
confirmToneGenerator?.release()
confirmToneGenerator = null
confirmToneStream = stream
confirmMediaPlayer?.release()
confirmMediaPlayer = MediaPlayer().also { mp ->
mp.apply {
setAudioAttributes(
AudioAttributes.Builder()
.setLegacyStreamType(stream)
.build()
)
setDataSource(this@playCustomConfirmTone, uriString.toUri())
setOnCompletionListener { completed ->
if (confirmMediaPlayer === completed) {
confirmMediaPlayer = null
}
completed.release()
}
setOnErrorListener { failed, _, _ ->
if (confirmMediaPlayer === failed) {
confirmMediaPlayer = null
}
failed.release()
true
}
prepare()
start()
}
}
true
} catch (_: Exception) {
confirmMediaPlayer?.release()
confirmMediaPlayer = null
false
}
}
fun beepError() {
val tg = errorToneGenerator ?: ToneGenerator(
AudioManager.STREAM_ALARM,
@@ -28,6 +87,9 @@ fun beepError() {
fun releaseToneGenerators() {
confirmToneGenerator?.release()
confirmToneGenerator = null
confirmToneStream = AudioManager.STREAM_MUSIC
confirmMediaPlayer?.release()
confirmMediaPlayer = null
errorToneGenerator?.release()
errorToneGenerator = null
}
@@ -1,5 +1,8 @@
package de.markusfisch.android.binaryeye.net
import android.content.Context
import de.markusfisch.android.binaryeye.app.prefs
import de.markusfisch.android.binaryeye.content.openUrl
import de.markusfisch.android.binaryeye.database.Scan
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@@ -14,61 +17,95 @@ import java.net.HttpURLConnection
import java.net.ProtocolException
import java.net.URL
fun Scan.sendAsync(
url: String,
type: String,
private const val SEND_TYPE_GET_ADD_CONTENT = "0"
private const val SEND_TYPE_GET_QUERY_STRING = "1"
private const val SEND_TYPE_POST_FORM = "2"
private const val SEND_TYPE_POST_JSON = "3"
private const val SEND_TYPE_EXTERNAL_BROWSER = "4"
fun Context.sendAsync(
scan: Scan,
url: String = prefs.sendScanUrl,
type: String = prefs.sendScanType,
callback: (Int?, String?) -> Unit
) {
val extras = mapOf(
"deviceId" to prefs.sendScanDeviceId.trim()
)
if (type == SEND_TYPE_EXTERNAL_BROWSER) {
openUrl(buildUrl(url, scan.asUrlArguments(extras)))
return
}
CoroutineScope(Dispatchers.IO).launch(Dispatchers.IO) {
val response = send(url, type)
val response = scan.send(url, type, extras) ?: return@launch
withContext(Dispatchers.Main) {
callback(response.code, response.body)
}
}
}
private fun Scan.send(url: String, type: String): Response {
return when (type) {
"1" -> request(
url + "?" + asUrlArguments()
private fun Scan.send(
url: String,
type: String,
extras: Map<String, String>
): Response? = when (type) {
SEND_TYPE_GET_ADD_CONTENT -> request(
buildUrl(url, text.urlEncode())
)
SEND_TYPE_GET_QUERY_STRING -> request(
buildUrl(url, asUrlArguments(extras))
)
SEND_TYPE_POST_FORM -> request(url) { con ->
con.requestMethod = "POST"
con.setRequestProperty(
"Content-Type",
"application/x-www-form-urlencoded"
)
"2" -> request(url) { con ->
con.requestMethod = "POST"
con.setRequestProperty(
"Content-Type",
"application/x-www-form-urlencoded"
)
con.outputStream.apply {
write(asUrlArguments().toByteArray())
close()
}
con.outputStream.apply {
write(asUrlArguments(extras).toByteArray())
close()
}
"3" -> request(url) { con ->
con.requestMethod = "POST"
con.setRequestProperty("Content-Type", "application/json")
con.outputStream.apply {
write(asJson().toString().toByteArray())
close()
}
}
else -> request(
url + text.urlEncode()
)
}
SEND_TYPE_POST_JSON -> request(url) { con ->
con.requestMethod = "POST"
con.setRequestProperty("Content-Type", "application/json")
con.outputStream.apply {
write(asJson(extras).toString().toByteArray())
close()
}
}
else -> null
}
private fun Scan.asUrlArguments(): String = getMap().map { (k, v) ->
private fun buildUrl(url: String, arguments: String): String {
if (arguments.isEmpty()) {
return url
}
val separator = when {
url.endsWith("?") || url.endsWith("&") -> ""
url.contains("?") -> "&"
else -> "?"
}
return url + separator + arguments
}
private fun Scan.asUrlArguments(
extras: Map<String, String>
): String = getMap(extras).map { (k, v) ->
"${k}=${v.urlEncode()}"
}.joinToString("&")
private fun Scan.asJson() = JSONObject().apply {
getMap().forEach { (k, v) -> put(k, v) }
private fun Scan.asJson(
extras: Map<String, String>
) = JSONObject().apply {
getMap(extras).forEach { (k, v) -> put(k, v) }
}
private fun Scan.getMap(): Map<String, String> = mapOf(
private fun Scan.getMap(extras: Map<String, String>): Map<String, String> = (mapOf(
"content" to text,
"raw" to raw?.toHexString(),
"format" to format.name,
@@ -81,8 +118,8 @@ private fun Scan.getMap(): Map<String, String> = mapOf(
"addOn" to addOn,
"price" to price,
"issueNumber" to issueNumber,
"timestamp" to dateTime
).filterNullValues()
"timestamp" to dateTime,
) + extras).filterNullValues()
private fun Map<String, String?>.filterNullValues() =
filterValues { it != null }.mapValues { it.value as String }
@@ -0,0 +1,14 @@
package de.markusfisch.android.binaryeye.os
import android.app.Activity
import android.os.Build
import android.view.WindowManager
@Suppress("DEPRECATION")
fun Activity.showWhenLocked() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
setShowWhenLocked(true)
} else {
window.addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED)
}
}
@@ -7,11 +7,17 @@ import android.graphics.Point
import android.media.ToneGenerator
import android.preference.PreferenceManager
import androidx.core.content.edit
import de.markusfisch.android.binaryeye.R
import de.markusfisch.android.binaryeye.automation.AutomatedAction
import de.markusfisch.android.binaryeye.automation.AutomatedAction.Companion.fromJsonArray
import de.markusfisch.android.binaryeye.automation.AutomatedAction.Companion.toJsonArray
import de.markusfisch.android.binaryeye.zxingcpp.migrateBarcodeFormatName
import de.markusfisch.android.zxingcpp.ZxingCpp.BarcodeFormat
import org.json.JSONArray
import org.json.JSONObject
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
import de.markusfisch.android.binaryeye.preference.IgnoreCode.Companion.fromJsonArray as ignoreCodesFromJsonArray
import de.markusfisch.android.binaryeye.preference.IgnoreCode.Companion.toJsonArray as ignoreCodesToJsonArray
@@ -31,7 +37,7 @@ class Preferences {
var barcodeFormats = setOf(
BarcodeFormat.Aztec.name,
BarcodeFormat.Codabar.name,
BarcodeFormat.Code39.name,
BarcodeFormat.Code39Std.name,
BarcodeFormat.Code39Ext.name,
BarcodeFormat.Code32.name,
BarcodeFormat.PZN.name,
@@ -50,6 +56,9 @@ class Preferences {
BarcodeFormat.ITF.name,
BarcodeFormat.MaxiCode.name,
BarcodeFormat.PDF417.name,
BarcodeFormat.Telepen.name,
BarcodeFormat.TelepenAlpha.name,
BarcodeFormat.TelepenNumeric.name,
BarcodeFormat.QRCode.name,
BarcodeFormat.MicroQRCode.name,
BarcodeFormat.RMQRCode.name,
@@ -115,6 +124,16 @@ class Preferences {
apply(BEEP_TONE_NAME, value)
field = value
}
var beepStreamName = "stream_music"
set(value) {
apply(BEEP_STREAM_NAME, value)
field = value
}
var beepToneUri = ""
set(value) {
apply(BEEP_TONE_URI, value)
field = value
}
var useHistory = false
set(value) {
apply(USE_HISTORY, value)
@@ -137,6 +156,11 @@ class Preferences {
apply(OPEN_IMMEDIATELY, value)
field = value
}
var stripTrackingParams = true
set(value) {
apply(STRIP_TRACKING_PARAMS, value)
field = value
}
var showMetaData = true
set(value) {
apply(SHOW_META_DATA, value)
@@ -187,6 +211,11 @@ class Preferences {
apply(SEND_SCAN_TYPE, value)
field = value
}
var sendScanDeviceId: String = ""
set(value) {
apply(SEND_SCAN_DEVICE_ID, value)
field = value
}
var sendScanBluetooth = false
set(value) {
apply(SEND_SCAN_BLUETOOTH, value)
@@ -239,6 +268,11 @@ class Preferences {
apply(PREVIEW_SCALE, value)
field = value
}
var dynamicColors = true
set(value) {
apply(DYNAMIC_COLORS, value)
field = value
}
var automatedActions = mutableListOf<AutomatedAction>()
private set
@@ -249,6 +283,7 @@ class Preferences {
defaultPreferences = PreferenceManager.getDefaultSharedPreferences(context)
loadProfiles()
addPredefinedProfiles(context)
load(context, defaultPreferences.getString(PROFILE, null))
}
@@ -300,14 +335,196 @@ class Preferences {
}
}
fun profileLabel(context: Context, name: String?) = when (name) {
null -> context.getString(R.string.profile_default)
PROFILE_EXPERT -> context.getString(R.string.mode_expert)
PROFILE_SIMPLE -> context.getString(R.string.mode_simple)
else -> name
}
fun loadExpertProfile(context: Context) {
load(context, PROFILE_EXPERT)
}
fun loadSimpleProfile(context: Context) {
load(context, PROFILE_SIMPLE)
}
fun toJson(context: Context, name: String?): String {
val defaults = Preferences()
val p = if (name.isNullOrEmpty()) {
defaultPreferences
} else {
context.getSharedPreferences(name, Context.MODE_PRIVATE)
}
val json = JSONObject()
json.put("type", PROFILE_EXPORT_TYPE)
json.put("name", name ?: "")
json.put(BARCODE_FORMATS, JSONArray().apply {
(p.getStringSet(BARCODE_FORMATS, null) ?: defaults.barcodeFormats).forEach {
put(it)
}
})
json.put(SHOW_CROP_HANDLE, p, defaults.showCropHandle)
json.put(SHOW_CROSSHAIRS, p, defaults.showCrosshairs)
json.put(ZOOM_BY_SWIPING, p, defaults.zoomBySwiping)
json.put(AUTO_ROTATE, p, defaults.autoRotate)
json.put(TRY_HARDER, p, defaults.tryHarder)
json.put(BULK_MODE, p, defaults.bulkMode)
json.put(BULK_MODE_DELAY, p, defaults.bulkModeDelay)
json.put(SHOW_TOAST_IN_BULK_MODE, p, defaults.showToastInBulkMode)
json.put(VIBRATE, p, defaults.vibrate)
json.put(BEEP, p, defaults.beep)
json.put(BEEP_TONE_NAME, p, defaults.beepToneName)
json.put(BEEP_STREAM_NAME, p, defaults.beepStreamName)
json.put(BEEP_TONE_URI, p, defaults.beepToneUri)
json.put(USE_HISTORY, p, defaults.useHistory)
json.put(IGNORE_DUPLICATES_NAME, p, defaults.ignoreDuplicatesName)
json.put(IGNORE_CODES, p, ignoreCodesToJsonArray(defaults.ignoreCodes))
json.put(COPY_IMMEDIATELY, p, defaults.copyImmediately)
json.put(OPEN_IMMEDIATELY, p, defaults.openImmediately)
json.put(STRIP_TRACKING_PARAMS, p, defaults.stripTrackingParams)
json.put(SHOW_META_DATA, p, defaults.showMetaData)
json.put(SHOW_HEX_DUMP, p, defaults.showHexDump)
json.put(SHOW_CHECKSUM, p, defaults.showChecksum)
json.put(SHOW_RECREATION, p, defaults.showRecreation)
json.put(CLOSE_AUTOMATICALLY, p, defaults.closeAutomatically)
json.put(DEFAULT_SEARCH_URL, p, defaults.defaultSearchUrl)
json.put(OPEN_WITH_URL, p, defaults.openWithUrl)
json.put(SEND_SCAN_ACTIVE, p, defaults.sendScanActive)
json.put(SEND_SCAN_URL, p, defaults.sendScanUrl)
json.put(SEND_SCAN_TYPE, p, defaults.sendScanType)
json.put(SEND_SCAN_DEVICE_ID, p, defaults.sendScanDeviceId)
json.put(SEND_SCAN_BLUETOOTH, p, defaults.sendScanBluetooth)
json.put(SEND_SCAN_BLUETOOTH_HOST, p, defaults.sendScanBluetoothHost)
json.put(CUSTOM_LOCALE, p, defaults.customLocale)
json.put(INDEX_OF_LAST_SELECTED_FORMAT, p, defaults.indexOfLastSelectedFormat)
json.put(INDEX_OF_LAST_SELECTED_EC_LEVEL, p, defaults.indexOfLastSelectedEcLevel)
json.put(FREE_ROTATION, p, defaults.freeRotation)
json.put(EXPAND_ESCAPE_SEQUENCES, p, defaults.expandEscapeSequences)
json.put(ADD_QUIET_ZONE, p, defaults.addQuietZone)
json.put(BRIGHTEN_SCREEN, p, defaults.brightenScreen)
json.put(PREVIEW_SCALE, p, defaults.previewScale)
json.put(AUTOMATED_ACTIONS, p.getString(AUTOMATED_ACTIONS, null) ?: "[]")
return json.toString()
}
fun importProfile(context: Context, text: String): String? {
val json = try {
JSONObject(text)
} catch (_: Exception) {
return null
}
if (json.optString("type") != PROFILE_EXPORT_TYPE) {
return null
}
var sanitized = json.optString("name").ifEmpty {
"imported"
}.replace(Regex("[^a-zA-Z0-9_-]+"), "_")
.trim('_')
if (sanitized.isEmpty()) {
sanitized = "imported"
}
val name = if (profiles.contains(sanitized)) {
val timestamp = SimpleDateFormat(
"yyyyMMdd_HHmmss",
Locale.US
).format(Date())
"${sanitized}_$timestamp"
} else {
sanitized
}
if (!addProfile(name)) {
return null
}
val target = context.getSharedPreferences(
name,
Context.MODE_PRIVATE
)
try {
target.edit {
val formatsArray = json.optJSONArray(BARCODE_FORMATS)
if (formatsArray != null) {
val formats = mutableSetOf<String>()
for (i in 0 until formatsArray.length()) {
val format = formatsArray.getString(i)
.migrateBarcodeFormatName()
try {
formats.add(BarcodeFormat.valueOf(format).name)
} catch (_: IllegalArgumentException) {
// Ignore unknown formats.
}
}
if (formats.isNotEmpty()) {
putStringSet(BARCODE_FORMATS, formats)
}
}
putBoolean(SHOW_CROP_HANDLE, json)
putBoolean(SHOW_CROSSHAIRS, json)
putBoolean(ZOOM_BY_SWIPING, json)
putBoolean(AUTO_ROTATE, json)
putBoolean(TRY_HARDER, json)
putBoolean(BULK_MODE, json)
putString(BULK_MODE_DELAY, json)
putBoolean(SHOW_TOAST_IN_BULK_MODE, json)
putBoolean(VIBRATE, json)
putBoolean(BEEP, json)
putString(BEEP_TONE_NAME, json)
putString(BEEP_STREAM_NAME, json)
putString(BEEP_TONE_URI, json)
putBoolean(USE_HISTORY, json)
putString(IGNORE_DUPLICATES_NAME, json)
putString(IGNORE_CODES, json)
putBoolean(COPY_IMMEDIATELY, json)
putBoolean(OPEN_IMMEDIATELY, json)
putBoolean(STRIP_TRACKING_PARAMS, json)
putBoolean(SHOW_META_DATA, json)
putBoolean(SHOW_HEX_DUMP, json)
putString(SHOW_CHECKSUM, json)
putBoolean(SHOW_RECREATION, json)
putBoolean(CLOSE_AUTOMATICALLY, json)
putString(DEFAULT_SEARCH_URL, json)
putString(OPEN_WITH_URL, json)
putBoolean(SEND_SCAN_ACTIVE, json)
putString(SEND_SCAN_URL, json)
putString(SEND_SCAN_TYPE, json)
putString(SEND_SCAN_DEVICE_ID, json)
putBoolean(SEND_SCAN_BLUETOOTH, json)
putString(SEND_SCAN_BLUETOOTH_HOST, json)
putString(CUSTOM_LOCALE, json)
putInt(INDEX_OF_LAST_SELECTED_FORMAT, json)
putInt(INDEX_OF_LAST_SELECTED_EC_LEVEL, json)
putBoolean(FREE_ROTATION, json)
putBoolean(EXPAND_ESCAPE_SEQUENCES, json)
putBoolean(ADD_QUIET_ZONE, json)
putBoolean(BRIGHTEN_SCREEN, json)
putFloat(PREVIEW_SCALE, json)
putString(AUTOMATED_ACTIONS, json)
}
} catch (_: Exception) {
removeProfile(name)
target.edit {
clear()
}
return null
}
return name
}
fun update() {
preferences.getStringSet(BARCODE_FORMATS, barcodeFormats)?.let {
val migrated = migrateBarcodeFormats(it)
if (migrated != it) {
apply(BARCODE_FORMATS, migrated)
}
barcodeFormats = addFormatsOnUpdate(
migrated,
migrateCode39ToCode39Std(
migrateBarcodeFormatNames(it)
),
BarcodeFormat.Code39Std,
BarcodeFormat.Code39Ext,
BarcodeFormat.Code32,
BarcodeFormat.PZN,
@@ -317,7 +534,10 @@ class Preferences {
BarcodeFormat.DataBarExp,
BarcodeFormat.DataBarExpStk,
BarcodeFormat.RMQRCode,
BarcodeFormat.DXFilmEdge
BarcodeFormat.DXFilmEdge,
BarcodeFormat.Telepen,
BarcodeFormat.TelepenAlpha,
BarcodeFormat.TelepenNumeric
)
}
@@ -346,6 +566,12 @@ class Preferences {
preferences.getString(BEEP_TONE_NAME, beepToneName)?.also {
beepToneName = it
}
preferences.getString(BEEP_STREAM_NAME, beepStreamName)?.also {
beepStreamName = it
}
preferences.getString(BEEP_TONE_URI, beepToneUri)?.also {
beepToneUri = it
}
useHistory = preferences.getBoolean(USE_HISTORY, useHistory)
// Map old setting to new one if it wasn't on default value.
@@ -379,6 +605,10 @@ class Preferences {
OPEN_IMMEDIATELY,
openImmediately
)
stripTrackingParams = preferences.getBoolean(
STRIP_TRACKING_PARAMS,
stripTrackingParams
)
showMetaData = preferences.getBoolean(SHOW_META_DATA, showMetaData)
showHexDump = preferences.getBoolean(SHOW_HEX_DUMP, showHexDump)
preferences.getString(SHOW_CHECKSUM, showChecksum)?.also {
@@ -405,6 +635,9 @@ class Preferences {
preferences.getString(SEND_SCAN_TYPE, sendScanType)?.also {
sendScanType = it
}
preferences.getString(SEND_SCAN_DEVICE_ID, sendScanDeviceId)?.also {
sendScanDeviceId = it
}
sendScanBluetooth = preferences.getBoolean(
SEND_SCAN_BLUETOOTH,
sendScanBluetooth
@@ -440,8 +673,9 @@ class Preferences {
PREVIEW_SCALE,
previewScale
)
dynamicColors = preferences.getBoolean(DYNAMIC_COLORS, dynamicColors)
automatedActions = fromJsonArray(
preferences.getString(AUTOMATED_ACTIONS, "[]") ?: "[]"
preferences.getString(AUTOMATED_ACTIONS, null) ?: "[]"
)
}
@@ -458,27 +692,6 @@ class Preferences {
fun shouldIgnoreHistoryContent(content: String, format: String): Boolean =
ignoreCodes.any { it.matches(content, format) }
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)
}
add(format.name)
}
}
}
private fun migrateBarcodeFormats(formats: Set<String>): Set<String> {
return formats.mapTo(mutableSetOf()) { format ->
format.migrateBarcodeFormatName()
}
}
fun restoreCropHandle(
name: String,
viewWidth: Int,
@@ -501,12 +714,140 @@ class Preferences {
else -> ToneGenerator.TONE_PROP_BEEP
}
fun beepStream() = when (beepStreamName) {
"stream_notification" -> android.media.AudioManager.STREAM_NOTIFICATION
else -> android.media.AudioManager.STREAM_MUSIC
}
fun usesCustomBeepTone() = beepToneName == CUSTOM_BEEP_TONE_NAME &&
beepToneUri.isNotEmpty()
fun ignoreDuplicates() = when (ignoreDuplicatesName) {
"ignore_consecutive_duplicates" -> IgnoreDuplicates.Consecutive
"ignore_any_duplicates" -> IgnoreDuplicates.Any
else -> IgnoreDuplicates.Never
}
private fun migrateCode39ToCode39Std(
restored: Set<String>
) = restored.toMutableSet().apply {
// Migrate Code39 to the more specific Code39Std.
if (contains(BarcodeFormat.Code39.name)) {
remove(BarcodeFormat.Code39.name)
if (!contains(BarcodeFormat.Code39Std.name)) {
add(BarcodeFormat.Code39Std.name)
}
}
}
private fun migrateBarcodeFormatNames(formats: Set<String>): Set<String> {
return formats.mapTo(mutableSetOf()) { format ->
format.migrateBarcodeFormatName()
}
}
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)
}
add(format.name)
}
}
}
private fun addPredefinedProfiles(context: Context) {
val name = "predefined_profiles_added"
if (defaultPreferences.getBoolean(name, false)) {
return
}
defaultPreferences.edit {
putBoolean(name, true)
}
profiles.add(PROFILE_EXPERT)
context.getSharedPreferences(PROFILE_EXPERT, Context.MODE_PRIVATE)
.edit {
clear()
applyExpertProfileSettings()
}
profiles.add(PROFILE_SIMPLE)
context.getSharedPreferences(PROFILE_SIMPLE, Context.MODE_PRIVATE)
.edit {
clear()
applySimpleProfileSettings()
}
saveProfiles()
}
private fun SharedPreferences.Editor.applyExpertProfileSettings() {
val defaults = Preferences()
putStringSet(BARCODE_FORMATS, defaults.barcodeFormats)
putBoolean(SHOW_CROP_HANDLE, defaults.showCropHandle)
putBoolean(SHOW_CROSSHAIRS, defaults.showCrosshairs)
putBoolean(ZOOM_BY_SWIPING, defaults.zoomBySwiping)
putBoolean(AUTO_ROTATE, defaults.autoRotate)
putBoolean(TRY_HARDER, defaults.tryHarder)
putBoolean(BULK_MODE, defaults.bulkMode)
putString(BULK_MODE_DELAY, defaults.bulkModeDelay)
putBoolean(SHOW_TOAST_IN_BULK_MODE, defaults.showToastInBulkMode)
putBoolean(VIBRATE, defaults.vibrate)
putBoolean(BEEP, defaults.beep)
putString(BEEP_TONE_NAME, defaults.beepToneName)
putString(BEEP_STREAM_NAME, defaults.beepStreamName)
putString(BEEP_TONE_URI, defaults.beepToneUri)
putBoolean(USE_HISTORY, defaults.useHistory)
putString(IGNORE_DUPLICATES_NAME, defaults.ignoreDuplicatesName)
putString(IGNORE_CODES, ignoreCodesToJsonArray(defaults.ignoreCodes))
putBoolean(COPY_IMMEDIATELY, defaults.copyImmediately)
putBoolean(OPEN_IMMEDIATELY, defaults.openImmediately)
putBoolean(STRIP_TRACKING_PARAMS, defaults.stripTrackingParams)
putBoolean(SHOW_META_DATA, defaults.showMetaData)
putBoolean(SHOW_HEX_DUMP, defaults.showHexDump)
putString(SHOW_CHECKSUM, defaults.showChecksum)
putBoolean(SHOW_RECREATION, defaults.showRecreation)
putBoolean(CLOSE_AUTOMATICALLY, defaults.closeAutomatically)
putString(DEFAULT_SEARCH_URL, defaults.defaultSearchUrl)
putString(OPEN_WITH_URL, defaults.openWithUrl)
putBoolean(SEND_SCAN_ACTIVE, defaults.sendScanActive)
putString(SEND_SCAN_URL, defaults.sendScanUrl)
putString(SEND_SCAN_TYPE, defaults.sendScanType)
putString(SEND_SCAN_DEVICE_ID, defaults.sendScanDeviceId)
putBoolean(SEND_SCAN_BLUETOOTH, defaults.sendScanBluetooth)
putString(SEND_SCAN_BLUETOOTH_HOST, defaults.sendScanBluetoothHost)
putString(CUSTOM_LOCALE, defaults.customLocale)
putInt(INDEX_OF_LAST_SELECTED_FORMAT, defaults.indexOfLastSelectedFormat)
putInt(INDEX_OF_LAST_SELECTED_EC_LEVEL, defaults.indexOfLastSelectedEcLevel)
putBoolean(FREE_ROTATION, defaults.freeRotation)
putBoolean(EXPAND_ESCAPE_SEQUENCES, defaults.expandEscapeSequences)
putBoolean(ADD_QUIET_ZONE, defaults.addQuietZone)
putBoolean(BRIGHTEN_SCREEN, defaults.brightenScreen)
putFloat(PREVIEW_SCALE, defaults.previewScale)
putString(AUTOMATED_ACTIONS, toJsonArray(defaults.automatedActions))
}
private fun SharedPreferences.Editor.applySimpleProfileSettings() {
applyExpertProfileSettings()
putBoolean(SHOW_CROP_HANDLE, false)
putBoolean(ZOOM_BY_SWIPING, false)
putBoolean(BEEP, true)
putBoolean(COPY_IMMEDIATELY, true)
putBoolean(OPEN_IMMEDIATELY, true)
putBoolean(SHOW_META_DATA, false)
putBoolean(SHOW_HEX_DUMP, false)
putString(DEFAULT_SEARCH_URL, SIMPLE_SEARCH_URL)
putString(OPEN_WITH_URL, SIMPLE_SEARCH_URL)
putBoolean(EXPAND_ESCAPE_SEQUENCES, false)
putBoolean(BRIGHTEN_SCREEN, true)
}
private fun apply(label: String, value: Boolean) {
preferences.edit {
putBoolean(label, value)
@@ -549,8 +890,12 @@ class Preferences {
Consecutive, Any, Never
}
private const val PROFILE_EXPORT_TYPE = "binaryeye_profile"
private const val PROFILES = "profiles"
private const val PROFILE = "profile"
private const val PROFILE_EXPERT = "builtin:expert"
private const val PROFILE_SIMPLE = "builtin:simple"
private const val SIMPLE_SEARCH_URL = "https://www.google.com/search?q="
private const val BARCODE_FORMATS = "formats"
private const val SHOW_CROP_HANDLE = "show_crop_handle"
private const val SHOW_CROSSHAIRS = "show_crosshairs"
@@ -563,10 +908,13 @@ class Preferences {
private const val VIBRATE = "vibrate"
private const val BEEP = "beep"
private const val BEEP_TONE_NAME = "beep_tone_name"
private const val BEEP_STREAM_NAME = "beep_stream_name"
private const val BEEP_TONE_URI = "beep_tone_uri"
private const val USE_HISTORY = "use_history"
private const val IGNORE_DUPLICATES_NAME = "ignore_duplicates_name"
private const val IGNORE_CODES = "ignore_codes"
private const val OPEN_IMMEDIATELY = "open_immediately"
private const val STRIP_TRACKING_PARAMS = "strip_tracking_params"
private const val COPY_IMMEDIATELY = "copy_immediately"
private const val SHOW_META_DATA = "show_meta_data"
private const val SHOW_HEX_DUMP = "show_hex_dump"
@@ -578,6 +926,7 @@ 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_DEVICE_ID = "send_scan_device_id"
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"
@@ -589,6 +938,48 @@ class Preferences {
private const val BRIGHTEN_SCREEN = "brighten_screen"
private const val PREVIEW_SCALE = "preview_scale"
private const val AUTOMATED_ACTIONS = "automated_actions"
private const val DEFAULT_IGNORE_CODE_PATTERN = "^FIDO://.*"
private const val DYNAMIC_COLORS = "dynamic_colors"
private const val DEFAULT_IGNORE_CODE_PATTERN = "(?i)^fido:/.*"
const val CUSTOM_BEEP_TONE_NAME = "tone_custom_file"
}
}
private fun JSONObject.put(key: String, p: SharedPreferences, default: Boolean) {
put(key, p.getBoolean(key, default))
}
private fun JSONObject.put(key: String, p: SharedPreferences, default: String) {
put(key, p.getString(key, null) ?: default)
}
private fun JSONObject.put(key: String, p: SharedPreferences, default: Int) {
put(key, p.getInt(key, default))
}
private fun JSONObject.put(key: String, p: SharedPreferences, default: Float) {
put(key, p.getFloat(key, default))
}
private fun SharedPreferences.Editor.putBoolean(key: String, json: JSONObject) {
if (json.has(key)) {
putBoolean(key, json.getBoolean(key))
}
}
private fun SharedPreferences.Editor.putString(key: String, json: JSONObject) {
if (json.has(key)) {
putString(key, json.getString(key))
}
}
private fun SharedPreferences.Editor.putInt(key: String, json: JSONObject) {
if (json.has(key)) {
putInt(key, json.getInt(key))
}
}
private fun SharedPreferences.Editor.putFloat(key: String, json: JSONObject) {
if (json.has(key)) {
putFloat(key, json.getDouble(key).toFloat())
}
}
@@ -29,10 +29,14 @@ fun Context.errorFeedback() {
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
return if (prefs.beepStream() == AudioManager.STREAM_MUSIC) {
am.getStreamVolume(AudioManager.STREAM_MUSIC) == 0
} else {
when (am.ringerMode) {
AudioManager.RINGER_MODE_SILENT,
AudioManager.RINGER_MODE_VIBRATE -> true
else -> false
else -> false
}
}
}
@@ -2,16 +2,18 @@ package de.markusfisch.android.binaryeye.view
import android.content.Context
import android.content.ContextWrapper
import android.graphics.Color
import android.os.Build
import android.view.View
import android.view.WindowManager
import android.widget.AbsListView
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.Toolbar
import androidx.core.content.ContextCompat
import androidx.core.graphics.ColorUtils
import androidx.core.graphics.drawable.toDrawable
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.color.MaterialColors
import de.markusfisch.android.binaryeye.R
val systemBarListViewScrollListener = object : AbsListView.OnScrollListener {
@@ -77,7 +79,7 @@ fun AppCompatActivity.initBars() {
setSupportActionBar(this)
},
findViewById<View>(R.id.navbar)?.apply {
setBackgroundColor(translucentPrimaryColor)
setBackgroundColor(getTranslucentPrimaryColor(this@initBars))
}
)
}
@@ -91,18 +93,23 @@ fun unlockStatusBarColor() {
statusBarColorLocked = false
}
private var translucentPrimaryColor = 0
private fun getTranslucentPrimaryColor(context: Context): Int {
return ColorUtils.setAlphaComponent(
MaterialColors.getColor(
context,
com.google.android.material.R.attr.colorSurfaceContainerHigh,
Color.BLACK
),
0xd8
)
}
fun colorSystemAndToolBars(
context: Context,
scrolled: Boolean = false,
scrollable: Boolean = false
) {
if (translucentPrimaryColor == 0) {
translucentPrimaryColor = ContextCompat.getColor(
context,
R.color.primary_translucent
)
}
val translucentPrimaryColor = getTranslucentPrimaryColor(context)
val topColor = if (scrolled) translucentPrimaryColor else 0
val activity = getAppCompatActivity(context) ?: return
// System bars no longer have a background from SDK35+.
@@ -125,6 +132,7 @@ fun colorSystemAndToolBars(
} else {
View.GONE
}
setBackgroundColor(translucentPrimaryColor)
}
}
activity.supportActionBar?.setBackgroundDrawable(topColor.toDrawable())
@@ -43,17 +43,22 @@ fun setPaddingFromWindowInsets(
}
}
fun View.setPaddingFromWindowInsets() {
doOnApplyWindowInsets { v, insets -> v.setPadding(insets) }
fun View.setPaddingFromWindowInsets(bottom: Boolean = true) {
doOnApplyWindowInsets { v, insets, windowInsets ->
if (!bottom) {
insets.bottom = getImeBottomInsetWithoutSystemBars(windowInsets)
}
v.setPadding(insets)
}
}
// A slight variation of the idea from Google's Chris Banes to avoid
// adding the toolbar height for every view that needs to be inset.
// For the original post see here:
// https://medium.com/androiddevelopers/windowinsets-listeners-to-layouts-8f9ccc8fa4d1
fun View.doOnApplyWindowInsets(f: (View, Rect) -> Unit) {
fun View.doOnApplyWindowInsets(f: (View, Rect, WindowInsetsCompat) -> Unit) {
ViewCompat.setOnApplyWindowInsetsListener(this) { v, insets ->
f(v, insetsWithToolbar(insets))
f(v, insetsWithToolbar(insets), insets)
insets
}
// It's important to explicitly request the insets (again) in
@@ -70,6 +75,11 @@ private fun insetsWithToolbar(insets: WindowInsetsCompat? = null) = Rect(
insets?.systemWindowInsetBottom ?: 0
)
private fun getImeBottomInsetWithoutSystemBars(insets: WindowInsetsCompat) =
(insets.getInsets(WindowInsetsCompat.Type.ime()).bottom -
insets.getInsets(WindowInsetsCompat.Type.systemBars()).bottom)
.coerceAtLeast(0)
private fun View.requestApplyInsetsWhenAttached() {
if (isAttachedToWindow) {
ViewCompat.requestApplyInsets(this)
@@ -3,6 +3,7 @@ package de.markusfisch.android.binaryeye.widget
import android.annotation.SuppressLint
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.Path
import android.graphics.Point
@@ -12,8 +13,9 @@ import android.os.Build
import android.util.AttributeSet
import android.view.MotionEvent
import android.view.View
import androidx.core.content.ContextCompat
import androidx.core.graphics.ColorUtils
import androidx.core.graphics.withSave
import com.google.android.material.color.MaterialColors
import de.markusfisch.android.binaryeye.R
import de.markusfisch.android.binaryeye.app.prefs
import de.markusfisch.android.binaryeye.graphics.getBitmapFromDrawable
@@ -34,18 +36,27 @@ class DetectorView : View {
coordinatesLast = 0
invalidate()
}
private val overlayColor = ColorUtils.setAlphaComponent(
MaterialColors.getColor(
context,
com.google.android.material.R.attr.colorOnSurface,
Color.WHITE
),
0x99
)
private val dotPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = ContextCompat.getColor(context, R.color.dot)
color = overlayColor
style = Paint.Style.FILL
}
private val crosshairPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = ContextCompat.getColor(context, R.color.dot)
color = overlayColor
style = Paint.Style.STROKE
strokeWidth = context.resources.displayMetrics.density
}
private val dotRadius: Float
private val handleBitmap = resources.getBitmapFromDrawable(
R.drawable.button_crop
R.drawable.button_crop,
context.theme
)
private val handleXRadius = handleBitmap.width / 2
private val handleYRadius = handleBitmap.height / 2
@@ -197,7 +208,7 @@ class DetectorView : View {
val mn = min(center.x, center.y) * .8f
handlePos.set(
(center.x + mn).roundToInt(),
(center.y + mn).roundToInt()
min((center.y + mn).roundToInt(), handleHome.y)
)
handleActive = true
}
@@ -236,8 +247,8 @@ class DetectorView : View {
viewWidth = right - left
viewHeight = bottom - top
center.set(
left + (viewWidth / 2),
top + (viewHeight / 2)
viewWidth / 2,
viewHeight / 2
)
minY = padding * 2
maxY = viewHeight - minY
@@ -261,6 +272,12 @@ class DetectorView : View {
}
}
fun getHorizontalCrosshairY() = if (handleActive) {
(roi.top + roi.bottom) * .5f
} else {
viewHeight * .5f
}
private fun Canvas.drawDots() {
var i = 0
while (i < coordinatesLast) {
@@ -1,36 +1,40 @@
package de.markusfisch.android.binaryeye.zxingcpp
val oldToNewFormatNames = mapOf(
"NONE" to "None",
"AZTEC" to "Aztec",
"CODABAR" to "Codabar",
"CODE_39" to "Code39",
"CODE_39_EXT" to "Code39Ext",
"CODE_32" to "Code32",
"PZN" to "PZN",
"CODE_93" to "Code93",
"CODE_128" to "Code128",
"DATA_BAR" to "DataBar",
"DATA_BAR_OMNI" to "DataBarOmni",
"DATA_BAR_STK" to "DataBarStk",
"DATA_BAR_LTD" to "DataBarLtd",
"DATA_BAR_EXPANDED" to "DataBarExp",
"DATA_BAR_EXP_STK" to "DataBarExpStk",
"DATA_MATRIX" to "DataMatrix",
"DX_FILM_EDGE" to "DXFilmEdge",
"EAN_8" to "EAN8",
"EAN_13" to "EAN13",
"ITF" to "ITF",
"MAXICODE" to "MaxiCode",
"PDF_417" to "PDF417",
"TELEPEN" to "Telepen",
"TELEPEN_ALPHA" to "TelepenAlpha",
"TELEPEN_NUMERIC" to "TelepenNumeric",
"QR_CODE" to "QRCode",
"MICRO_QR_CODE" to "MicroQRCode",
"RMQR_CODE" to "RMQRCode",
"UPC_A" to "UPCA",
"UPC_E" to "UPCE"
)
/**
* Translate legacy barcode format names from ZXingC++ before 3.x
* to current BarcodeFormat enum names.
*/
fun String.migrateBarcodeFormatName() = when (this) {
"NONE" -> "None"
"AZTEC" -> "Aztec"
"CODABAR" -> "Codabar"
"CODE_39" -> "Code39"
"CODE_39_EXT" -> "Code39Ext"
"CODE_32" -> "Code32"
"PZN" -> "PZN"
"CODE_93" -> "Code93"
"CODE_128" -> "Code128"
"DATA_BAR" -> "DataBar"
"DATA_BAR_OMNI" -> "DataBarOmni"
"DATA_BAR_STK" -> "DataBarStk"
"DATA_BAR_LTD" -> "DataBarLtd"
"DATA_BAR_EXPANDED" -> "DataBarExp"
"DATA_BAR_EXP_STK" -> "DataBarExpStk"
"DATA_MATRIX" -> "DataMatrix"
"DX_FILM_EDGE" -> "DXFilmEdge"
"EAN_8" -> "EAN8"
"EAN_13" -> "EAN13"
"ITF" -> "ITF"
"MAXICODE" -> "MaxiCode"
"PDF_417" -> "PDF417"
"QR_CODE" -> "QRCode"
"MICRO_QR_CODE" -> "MicroQRCode"
"RMQR_CODE" -> "RMQRCode"
"UPC_A" -> "UPCA"
"UPC_E" -> "UPCE"
else -> this
}
fun String.migrateBarcodeFormatName() = oldToNewFormatNames[this] ?: this
+3
View File
@@ -0,0 +1,3 @@
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="?attr/colorSecondaryContainer"/>
</selector>
@@ -0,0 +1,3 @@
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="?attr/colorSecondaryContainer"/>
</selector>
@@ -0,0 +1,3 @@
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="?attr/colorOnSecondaryContainer"/>
</selector>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.0 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 26 KiB

@@ -1,5 +1,5 @@
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@color/primary"/>
<item android:drawable="?attr/colorSurface"/>
<item>
<bitmap
android:gravity="center"
@@ -1,5 +1,5 @@
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@color/background_color"/>
<item android:drawable="?attr/colorSurface"/>
<item>
<bitmap
android:gravity="top|right"
+24 -11
View File
@@ -1,12 +1,25 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" xmlns:aapt="http://schemas.android.com/aapt" android:width="56dp" android:height="56dp" android:viewportWidth="56" android:viewportHeight="56">
<path android:pathData="M4 32a24 24 0 1 1 48 0 24 24 0 1 1-48 0">
<aapt:attr name="android:fillColor">
<gradient android:gradientRadius="24" android:centerX="28" android:centerY="32" android:type="radial">
<item android:offset="0" android:color="#FF000000"/>
<item android:offset="1" android:color="#00000000"/>
</gradient>
</aapt:attr>
</path>
<path android:pathData="M4 28a24 24 0 1 1 48 0 24 24 0 1 1-48 0" android:fillColor="#B6D46F"/>
<path android:pathData="M33 31h2v-8c0-1.1-0.9-2-2-2h-8v2h8v8zm-10 2V17h-2v4h-4v2h4v10c0 1.1 0.9 2 2 2h10v4h2v-4h4v-2H23z" android:fillColor="#FFFFFF"/>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="56dp"
android:height="56dp"
android:viewportWidth="56"
android:viewportHeight="56">
<path android:pathData="M4 32a24 24 0 1 1 48 0 24 24 0 1 1-48 0">
<aapt:attr name="android:fillColor">
<gradient
android:gradientRadius="24"
android:centerX="28"
android:centerY="32"
android:type="radial">
<item android:offset="0" android:color="#FF000000"/>
<item android:offset="1" android:color="#00000000"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="M4 28a24 24 0 1 1 48 0 24 24 0 1 1-48 0"
android:fillColor="@color/crop_handle_fill"/>
<path
android:pathData="M33 31h2v-8c0-1.1-0.9-2-2-2h-8v2h8v8zm-10 2V17h-2v4h-4v2h4v10c0 1.1 0.9 2 2 2h10v4h2v-4h4v-2H23z"
android:fillColor="@color/crop_handle_icon"/>
</vector>
+2 -2
View File
@@ -4,13 +4,13 @@
<item android:state_pressed="false">
<shape android:shape="rectangle">
<corners android:radius="2dp"/>
<solid android:color="@color/accent"/>
<solid android:color="?attr/colorSecondary"/>
</shape>
</item>
<item android:state_pressed="true">
<shape android:shape="rectangle">
<corners android:radius="2dp"/>
<solid android:color="@color/accent_dark"/>
<solid android:color="?attr/colorSecondaryContainer"/>
</shape>
</item>
</selector>
+5 -11
View File
@@ -1,16 +1,10 @@
<selector
<ripple
xmlns:android="http://schemas.android.com/apk/res/android"
android:constantSize="true">
<item android:state_pressed="false">
android:color="?attr/colorControlHighlight">
<item>
<shape android:shape="rectangle">
<corners android:radius="2dp"/>
<solid android:color="@color/selected_row"/>
<solid android:color="?attr/colorSecondaryContainer"/>
</shape>
</item>
<item android:state_pressed="true">
<shape android:shape="rectangle">
<corners android:radius="2dp"/>
<solid android:color="@color/pressed_row"/>
</shape>
</item>
</selector>
</ripple>
+1 -1
View File
@@ -1,3 +1,3 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:autoMirrored="true" android:height="24dp" android:tint="#FFFFFF" android:viewportHeight="960" android:viewportWidth="960" android:width="24dp">
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:autoMirrored="true" android:height="24dp" android:tint="?attr/colorControlNormal" android:viewportHeight="960" android:viewportWidth="960" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M313 520l224 224-57 56-320-320 320-320 57 56-224 224h487v80H313z"/>
</vector>
+1 -1
View File
@@ -1,3 +1,3 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#FFFFFF" android:viewportHeight="960" android:viewportWidth="960" android:width="24dp">
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="?attr/colorControlNormal" android:viewportHeight="960" android:viewportWidth="960" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M480 340q13 0 21.5-9t8.5-21q0-13-8.5-21.5T480 280q-12 0-21 8.5t-9 21.5q0 12 9 21t21 9zM172 595q-15-26-23.5-54.5T140 480q0-32 8.5-60.5T172 365l64 50q-8 15-12 31t-4 34q0 18 4 34t12 31l-64 50zM62 681q-29-44-45.5-94.5T0 480q0-56 16.5-106.5T62 279l63 50q-21 33-33 71t-12 80q0 42 12 80.5t33 71.5l-63 49zm298 119q-33 0-56.5-23.5T280 720V240q0-33 23.5-56.5T360 160h240q33 0 56.5 23.5T680 240v480q0 33-23.5 56.5T600 800H360zm0-80h240V240H360v480zm427-125l-64-50q8-15 12.5-31t4.5-34q0-18-4.5-34T723 415l64-50q15 26 24 54.5t9 60.5q0 32-9 60.5T787 595zm111 85l-63-49q21-33 33-71t12-80q0-42-12-80.5T835 328l63-49q29 43 45.5 93.5T960 479q0 56-16.5 106.5T898 680zm-538 40V240v480z"/>
</vector>
@@ -1,3 +1,3 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#FFFFFF" android:viewportHeight="960" android:viewportWidth="960" android:width="24dp">
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="?attr/colorControlNormal" android:viewportHeight="960" android:viewportWidth="960" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M440 880V576L256 760l-56-56 224-224-224-224 56-56 184 184V80h40l228 228-172 172 172 172-228 228h-40zm80-496l76-76-76-74v150zm0 342l76-74-76-76v150z"/>
</vector>
@@ -1,3 +1,3 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#FFFFFF" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="?attr/colorControlNormal" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="#fff" android:pathData="M20 8.69V4h-4.69L12 0.69 8.69 4H4v4.69L0.69 12 4 15.31V20h4.69L12 23.31 15.31 20H20v-4.69L23.31 12 20 8.69zM12 18c-3.31 0-6-2.69-6-6s2.69-6 6-6 6 2.69 6 6-2.69 6-6 6zm0-10c-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4-1.79-4-4-4z"/>
</vector>
@@ -1,3 +1,3 @@
<vector android:height="24dp" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<vector android:height="24dp" android:tint="?attr/colorControlNormal" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FFFFFF" android:pathData="M14 10H2v2h12v-2zm0-4H2v2h12V6zm4 8v-4h-2v4h-4v2h4v4h2v-4h4v-2h-4zM2 16h8v-2H2v2z"/>
</vector>
+1 -1
View File
@@ -1,3 +1,3 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24.0" android:viewportHeight="24.0">
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:tint="?attr/colorControlNormal" android:width="24dp" android:height="24dp" android:viewportWidth="24.0" android:viewportHeight="24.0">
<path android:fillColor="#FFFFFFFF" android:pathData="M16 1H4C2.9 1 2 1.9 2 3v14h2V3h12V1zm3 4H8C6.9 5 6 5.9 6 7v14c0 1.1 0.9 2 2 2h11c1.1 0 2-0.9 2-2V7c0-1.1-0.9-2-2-2zm0 16H8V7h11v14z"/>
</vector>
@@ -1,3 +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">
<vector android:height="24dp" android:tint="?attr/colorControlNormal" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FFFFFF" android:pathData="M12.65 10C11.83 7.67 9.61 6 7 6c-3.31 0-6 2.69-6 6s2.69 6 6 6c2.61 0 4.83-1.67 5.65-4H17v4h4v-4h2v-4H12.65zM7 14c-1.1 0-2-0.9-2-2s0.9-2 2-2 2 0.9 2 2-0.9 2-2 2z"/>
</vector>
@@ -1,3 +1,3 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24.0" android:viewportHeight="24.0">
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:tint="?attr/colorControlNormal" android:width="24dp" android:height="24dp" android:viewportWidth="24.0" android:viewportHeight="24.0">
<path android:fillColor="#FFFFFFFF" android:pathData="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/>
</vector>
+1 -1
View File
@@ -1,3 +1,3 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#FFFFFF" android:viewportHeight="960" android:viewportWidth="960" android:width="24dp">
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="?attr/colorControlNormal" android:viewportHeight="960" android:viewportWidth="960" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M680 920V760H280q-33 0-56.5-23.5T200 680V280H40v-80h160V40h80v640h640v80H760v160h-80zm0-320V280H360v-80h320q33 0 56.5 23.5T760 280v320h-80z"/>
</vector>
@@ -1,3 +1,3 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#FFFFFF" android:viewportHeight="960" android:viewportWidth="960" android:width="24dp">
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="?attr/colorControlNormal" android:viewportHeight="960" android:viewportWidth="960" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M340.5 811.5Q275 783 226 734t-77.5-114.5Q120 554 120 480q0-44 10-85.5t29-78q19-36.5 45.5-68T264 192l272 272-56 56-216-216q-30 36-47 80.5T200 480q0 116 82 198t198 82q116 0 198-82t82-198q0-107-68.5-184.5T520 204v76h-80V120h40q74 0 139.5 28.5T734 226q49 49 77.5 114.5T840 480q0 74-28.5 139.5T734 734q-49 49-114.5 77.5T480 840q-74 0-139.5-28.5zm-89-303Q240 497 240 480t11.5-28.5Q263 440 280 440t28.5 11.5Q320 463 320 480t-11.5 28.5Q297 520 280 520t-28.5-11.5zm200 200Q440 697 440 680t11.5-28.5Q463 640 480 640t28.5 11.5Q520 663 520 680t-11.5 28.5Q497 720 480 720t-28.5-11.5zm200-200Q640 497 640 480t11.5-28.5Q663 440 680 440t28.5 11.5Q720 463 720 480t-11.5 28.5Q697 520 680 520t-28.5-11.5z"/>
</vector>
+1 -1
View File
@@ -1,3 +1,3 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#FFFFFF" android:viewportHeight="960" android:viewportWidth="960" android:width="24dp">
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="?attr/colorControlNormal" android:viewportHeight="960" android:viewportWidth="960" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M440 160v487L216 423l-56 57 320 320 320-320-56-57-224 224V160h-80z"/>
</vector>
+1 -1
View File
@@ -1,3 +1,3 @@
<vector android:height="24dp" android:tint="#FFFFFF" android:viewportHeight="24.0" android:viewportWidth="24.0" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<vector android:height="24dp" android:tint="?attr/colorControlNormal" android:viewportHeight="24.0" android:viewportWidth="24.0" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c0.39-0.39 0.39-1.02 0-1.41l-2.34-2.34c-0.39-0.39-1.02-0.39-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z"/>
</vector>
@@ -1,3 +1,3 @@
<vector android:height="24dp" android:tint="#FFFFFF" android:viewportHeight="24.0" android:viewportWidth="24.0" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<vector android:height="24dp" android:tint="?attr/colorControlNormal" 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="M9 3L5 6.99h3V14h2V6.99h3L9 3zm7 14.01V10h-2v7.01h-3L15 21l4-3.99h-3z"/>
</vector>
@@ -1,3 +1,3 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24.0" android:viewportHeight="24.0">
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:tint="?attr/colorControlNormal" android:width="24dp" android:height="24dp" android:viewportWidth="24.0" android:viewportHeight="24.0">
<path android:fillColor="#FFFFFFFF" android:pathData="M7 2v11h3v9l7-12h-4l4-8z"/>
</vector>
@@ -1,3 +1,3 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:autoMirrored="true" android:height="24dp" android:tint="#FFFFFF" android:viewportHeight="960" android:viewportWidth="960" android:width="24dp">
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:autoMirrored="true" android:height="24dp" android:tint="?attr/colorControlNormal" android:viewportHeight="960" android:viewportWidth="960" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M640 680l-57-56 184-184-184-184 57-56 240 240-240 240zM80 760V600q0-83 58.5-141.5T280 400h247L383 256l57-56 240 240-240 240-57-56 144-144H280q-50 0-85 35t-35 85v160H80z"/>
</vector>
@@ -1,3 +1,3 @@
<vector android:height="24dp" android:tint="#FFFFFF" android:viewportHeight="24.0" android:viewportWidth="24.0" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<vector android:height="24dp" android:tint="?attr/colorControlNormal" android:viewportHeight="24.0" android:viewportWidth="24.0" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M13 3c-4.97 0-9 4.03-9 9H1l3.89 3.89 0.07 0.14L9 12H6c0-3.87 3.13-7 7-7s7 3.13 7 7-3.13 7-7 7c-1.93 0-3.68-0.79-4.94-2.06l-1.42 1.42C8.27 19.99 10.51 21 13 21c4.97 0 9-4.03 9-9s-4.03-9-9-9zm-1 5v5l4.28 2.54 0.72-1.21-3.5-2.08V8H12z"/>
</vector>
@@ -1,3 +1,3 @@
<vector android:height="24dp" android:tint="#FFFFFF" android:viewportHeight="24.0" android:viewportWidth="24.0" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<vector android:height="24dp" android:tint="?attr/colorControlNormal" 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.49v-1.98h2.97v-2.97h1.98v2.97h2.97v1.98l-2.952 0.018-0.018 2.952h-1.98l-0.018-2.952-2.952-0.018zm-1.19-3.74L11 12.4V7h2v4.6l1.4 1.4-1.05 1.75zM3 10V4h2v2.35c0.85-1.067 1.888-1.892 3.113-2.475C9.338 3.292 10.633 3 12 3c2.5 0 4.625 0.875 6.375 2.625S21 9.5 21 12h-2c0-1.95-0.679-3.604-2.037-4.963C15.604 5.679 13.95 5 12 5c-1.15 0-2.225 0.267-3.225 0.8S6.933 7.067 6.25 8H9v2H3zm9 11c-2.3 0-4.304-0.763-6.012-2.288C4.279 17.187 3.3 15.283 3.05 13H5.1c0.233 1.733 1.004 3.167 2.313 4.3C8.721 18.433 10.25 19 12 19c0.183 0 0.354-0.004 0.513-0.013 0.158-0.008 0.32-0.029 0.487-0.062v2.025c-0.167 0.017-0.329 0.029-0.487 0.037C12.354 20.996 12.183 21 12 21z"/>
</vector>
+1 -1
View File
@@ -1,3 +1,3 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24.0" android:viewportHeight="24.0">
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:tint="?attr/colorControlNormal" android:width="24dp" android:height="24dp" android:viewportWidth="24.0" android:viewportHeight="24.0">
<path android:fillColor="#FFFFFFFF" android:pathData="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-6h2v6zm0-8h-2V7h2v2z"/>
</vector>
+1 -1
View File
@@ -1,3 +1,3 @@
<vector android:height="24dp" android:tint="#FFFFFF" android:viewportHeight="24.0" android:viewportWidth="24.0" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<vector android:height="24dp" android:tint="?attr/colorControlNormal" android:viewportHeight="24.0" android:viewportWidth="24.0" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M3.9 12c0-1.71 1.39-3.1 3.1-3.1h4V7H7c-2.76 0-5 2.24-5 5s2.24 5 5 5h4v-1.9H7c-1.71 0-3.1-1.39-3.1-3.1zM8 13h8v-2H8v2zm9-6h-4v1.9h4c1.71 0 3.1 1.39 3.1 3.1s-1.39 3.1-3.1 3.1h-4V17h4c2.76 0 5-2.24 5-5s-2.24-5-5-5z"/>
</vector>
+1 -1
View File
@@ -1,3 +1,3 @@
<vector android:height="24dp" android:tint="#FFFFFF" android:viewportHeight="24.0" android:viewportWidth="24.0" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<vector android:height="24dp" android:tint="?attr/colorControlNormal" android:viewportHeight="24.0" android:viewportWidth="24.0" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M20 4H4C2.9 4 2.01 4.9 2.01 6L2 18c0 1.1 0.9 2 2 2h16c1.1 0 2-0.9 2-2V6c0-1.1-0.9-2-2-2zm0 14H4V8l8 5 8-5v10zm-8-7L4 6h16l-8 5z"/>
</vector>
+1 -1
View File
@@ -1,3 +1,3 @@
<vector android:height="24dp" android:tint="#FFFFFF" android:viewportHeight="24.0" android:viewportWidth="24.0" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<vector android:height="24dp" android:tint="?attr/colorControlNormal" 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="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z"/>
</vector>
@@ -1,3 +1,3 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#FFFFFF" android:viewportHeight="960" android:viewportWidth="960" android:width="24dp">
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="?attr/colorControlNormal" android:viewportHeight="960" android:viewportWidth="960" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M501 460zm-21 420q-33 0-56.5-23.5T400 800h160q0 33-23.5 56.5T480 880zM160 760v-80h80V400q0-83 50-147.5T420 168v-28q0-25 17.5-42.5T480 80q25 0 42.5 17.5T540 140v28q29 7 53.5 20.5T639 220l-57 57q-21-17-47-27t-55-10q-66 0-113 47t-47 113v280h480v80H160zm496-219l-56-57q10-10 15-24t5-32q0-18-5-32.5T600 371l56-56q21 21 32.5 50t11.5 63q0 34-11.5 63T656 541zm43 43q29-29 45-70t16-86q0-45-16-86t-45-70l57-56q41 41 62.5 95.5T840 428q0 62-21.5 116.5T756 640l-57-56z"/>
</vector>
+1 -1
View File
@@ -1,3 +1,3 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24.0" android:viewportHeight="24.0">
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:tint="?attr/colorControlNormal" android:width="24dp" android:height="24dp" android:viewportWidth="24.0" android:viewportHeight="24.0">
<path android:fillColor="#FFFFFFFF" android:pathData="M19 4H5C3.89 4 3 4.9 3 6v12c0 1.1 0.89 2 2 2h4v-2H5V8h14v10h-4v2h4c1.1 0 2-0.9 2-2V6c0-1.1-0.89-2-2-2zm-7 6l-4 4h3v6h2v-6h3l-4-4z"/>
</vector>
@@ -1,3 +1,3 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#FFFFFF" android:viewportHeight="24.0" android:viewportWidth="24.0" android:width="24dp">
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="?attr/colorControlNormal" android:viewportHeight="24.0" android:viewportWidth="24.0" android:width="24dp">
<path android:fillColor="#FF000000" android:pathData="M14 10H2v2h12v-2zm0-4H2v2h12V6zm4 8v-4h-2v4h-4v2h4v4h2v-4h4v-2h-4zM2 16h8v-2H2v2z"/>
</vector>
@@ -1,3 +1,3 @@
<vector android:height="24dp" android:tint="#FFFFFF" android:viewportHeight="24.0" android:viewportWidth="24.0" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<vector android:height="24dp" android:tint="?attr/colorControlNormal" android:viewportHeight="24.0" android:viewportWidth="24.0" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M6 2C4.9 2 4.01 2.9 4.01 4L4 20c0 1.1 0.89 2 1.99 2H18c1.1 0 2-0.9 2-2V8l-6-6H6zm7 7V3.5L18.5 9H13z"/>
</vector>
@@ -1,3 +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">
<vector android:height="24dp" android:tint="?attr/colorControlNormal" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M19.1 12.9a2.8 2.8 0 0 0 0.1-0.9 2.8 2.8 0 0 0-0.1-0.9l2.1-1.6a0.7 0.7 0 0 0 0.1-0.6l-1.9-3.4a0.7 0.7 0 0 0-0.6-0.2l-2.4 1a6.5 6.5 0 0 0-1.6-0.9l-0.4-2.6a0.5 0.5 0 0 0-0.5-0.4h-3.8a0.5 0.5 0 0 0-0.5 0.4L9.3 5.4a5.6 5.6 0 0 0-1.7 0.9l-2.4-1a0.4 0.4 0 0 0-0.5 0.2l-2 3.4c-0.1 0.2 0 0.4 0.2 0.6l2 1.6A2.8 2.8 0 0 0 4.8 12a2.8 2.8 0 0 0 0.1 0.9l-2.1 1.6a0.7 0.7 0 0 0-0.1 0.6l1.9 3.4a0.7 0.7 0 0 0 0.6 0.2l2.4-1a6.5 6.5 0 0 0 1.6 0.9l0.4 2.6a0.5 0.5 0 0 0 0.5 0.4h3.8a0.5 0.5 0 0 0 0.5-0.4l0.3-2.6a5.6 5.6 0 0 0 1.7-0.9l2.4 1a0.4 0.4 0 0 0 0.5-0.2l2-3.4c0.1-0.2 0-0.4-0.2-0.6zM12 15.6a3.6 3.6 0 1 1 3.6-3.6 3.6 3.6 0 0 1-3.6 3.6z"/>
</vector>
@@ -1,3 +1,3 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#FFFFFF" android:viewportHeight="960" android:viewportWidth="960" android:width="24dp">
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="?attr/colorControlNormal" android:viewportHeight="960" android:viewportWidth="960" android:width="24dp">
<path android:fillColor="#fff" android:pathData="M480 480q-66 0-113-47t-47-113q0-66 47-113t113-47q66 0 113 47t47 113q0 66-47 113t-113 47zM160 800V688q0-34 17.5-62.5T224 582q62-31 126-46.5T480 520q66 0 130 15.5T736 582q29 15 46.5 43.5T800 688v112H160zm80-80h480v-32q0-11-5.5-20T700 654q-54-27-109-40.5T480 600q-56 0-111 13.5T260 654q-9 5-14.5 14t-5.5 20v32zm240-320q33 0 56.5-23.5T560 320q0-33-23.5-56.5T480 240q-33 0-56.5 23.5T400 320q0 33 23.5 56.5T480 400zm0-80zm0 400z"/>
</vector>
@@ -1,3 +1,3 @@
<vector android:height="24dp" android:tint="#FFFFFF" android:viewportHeight="24.0" android:viewportWidth="24.0" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<vector android:height="24dp" android:tint="?attr/colorControlNormal" android:viewportHeight="24.0" android:viewportWidth="24.0" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M6 19c0 1.1 0.9 2 2 2h8c1.1 0 2-0.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"/>
</vector>
@@ -1,3 +1,3 @@
<vector android:height="24dp" android:tint="#FFFFFF" android:viewportHeight="24.0" android:viewportWidth="24.0" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<vector android:height="24dp" android:tint="?attr/colorControlNormal" android:viewportHeight="24.0" android:viewportWidth="24.0" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M15.55 5.55L11 1v3.07C7.06 4.56 4 7.92 4 12s3.05 7.44 7 7.93v-2.02C8.16 17.43 6 14.97 6 12s2.16-5.43 5-5.91V10l4.55-4.45zM19.93 11c-0.17-1.39-0.72-2.73-1.62-3.89l-1.42 1.42c0.54 0.75 0.88 1.6 1.02 2.47h2.02zM13 17.9v2.02c1.39-0.17 2.74-0.71 3.9-1.61l-1.44-1.44c-0.75 0.54-1.59 0.89-2.46 1.03zm3.89-2.42l1.42 1.41c0.9-1.16 1.45-2.5 1.62-3.89h-2.02c-0.14 0.87-0.48 1.72-1.02 2.48z"/>
</vector>
@@ -1,3 +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">
<vector android:height="24dp" android:tint="?attr/colorControlNormal" 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>
@@ -1,3 +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">
<vector android:height="24dp" android:tint="?attr/colorControlNormal" 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 -1
View File
@@ -1,3 +1,3 @@
<vector android:height="24dp" android:tint="#FFFFFF" android:viewportHeight="24.0" android:viewportWidth="24.0" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<vector android:height="24dp" android:tint="?attr/colorControlNormal" android:viewportHeight="24.0" android:viewportWidth="24.0" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M17 3H5C3.89 3 3 3.9 3 5v14c0 1.1 0.89 2 2 2h14c1.1 0 2-0.9 2-2V7l-4-4zm-5 16c-1.66 0-3-1.34-3-3s1.34-3 3-3 3 1.34 3 3-1.34 3-3 3zm3-10H5V5h10v4z"/>
</vector>
+1 -1
View File
@@ -1,3 +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">
<vector android:height="24dp" android:tint="?attr/colorControlNormal" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M3 11h8V3H3v8zm2-6h4v4H5V5zM3 21h8v-8H3v8zm2-6h4v4H5v-4zm8-12v8h8V3h-8zm6 6h-4V5h4v4zm0 10h2v2h-2zm-6-6h2v2h-2zm2 2h2v2h-2zm-2 2h2v2h-2zm2 2h2v2h-2zm2-2h2v2h-2zm0-4h2v2h-2zm2 2h2v2h-2z"/>
</vector>

Some files were not shown because too many files have changed in this diff Show More