Compare commits

..
204 Commits
Author SHA1 Message Date
Markus Fisch dc819a6e1a Advance version number to 1.63.1 2024-01-13 13:47:32 +01:00
Markus Fisch 5b51072f1a Include custom label in json/csv exports/shares 2024-01-13 13:29:45 +01:00
solokotandGitHub 1b10e82cb2 Update Russian translation 2024-01-12 08:55:55 +01:00
Markus Fisch eeae1eb0ad Advance version number to 1.63.0 2024-01-11 20:27:27 +01:00
Markus Fisch 21c29ca373 Set sendScanActive to false by default
Was enabled by default because this setting came after `sendScanUrl`
and I didn't want to annoy users using this feature already.

But it's probably too suspicious to have it enabled by default, even
if it does nothing, of course.
2024-01-11 17:10:22 +01:00
Markus Fisch 8bcb72e91b Use getParcelableExtra(String, Class) on Tiramisu+ 2024-01-11 00:23:15 +01:00
Markus Fisch 8f345fbfbe Remove redundant suppress annotation 2024-01-11 00:14:04 +01:00
Markus Fisch 2c88dbca02 Reformat scanWithinBounds()
Apply autoformat.
2024-01-11 00:12:45 +01:00
Markus Fisch bd1c629c85 Reformat imports
Let Android Studio do its thing.
2024-01-11 00:11:48 +01:00
Markus Fisch f89e6612c4 Replace Enum.values() with Enum.entries
Following the recommendation.
2024-01-11 00:09:27 +01:00
Markus Fisch 7a0cd0ce19 Use isNullOrEmpty instead of checking both 2024-01-11 00:08:22 +01:00
Markus Fisch 8544affa1e Edit scan label in DecodeFragment
Which makes this feature more prominent and also easier to access.
2024-01-10 23:56:44 +01:00
Markus Fisch 48d3588341 Add a button to add generated codes to the history
So generated barcodes can be saved, too.
2024-01-10 23:37:51 +01:00
Markus Fisch 783f6e4892 Refactor edit_scan to edit_label
This action is used to edit the (optional) label of a scan.
2024-01-10 23:03:24 +01:00
Markus Fisch 032cd64daf Avoid asking multiple times for camera permission
Otherwise ActivityCompat.requestPermissions() will trigger onResume()
again and again.
2024-01-11 10:59:29 +01:00
Markus Fisch 96626e6b00 Move origin subtraction out of min()
Simplifies the calculation of the width and height of the crop
section.
2024-01-10 22:38:31 +01:00
Markus Fisch 11ae65ec3e Refactor Bitmap.erect to rotate
The original task of this function was to straighten a picture
lying on its side. However, the function is now used for any rotation.
2024-01-10 22:36:02 +01:00
Markus Fisch 91e3acb647 Disable free rotation by default
This is probably too delicate to use, most barcodes are still
recognized when skewed (a little), and it can be quite an expensive
operation to rotate a very big image. So it's better to disable
this feature by default (for new installations).
2024-01-09 20:25:21 +01:00
Markus Fisch 40c2f5947b Crop imported images in background thread
Because rotating and cropping a big photo can take some time.
2024-01-09 20:03:26 +01:00
Markus Fisch 58b9466cfc Fix margin/error correction for recreated codes
The `margin` argument position was incorrectly used for the
error correction level.
2024-01-09 19:28:50 +01:00
Markus Fisch 12210e1768 Update CameraView library 2024-01-09 12:13:12 +01:00
Sergiy StuparandGitHub ad2ac457c9 Update Ukrainian translation 2024-01-09 12:06:05 +01:00
Markus Fisch 634b6e8d2b Limit duration of error beep to 1s
Since `ToneGenerator.TONE_SUP_ERROR` keeps on playing which
is quite annoying.
2024-01-03 19:21:29 +01:00
CertainBotandGitHub 6dd47f4dd0 Update Spanish translation 2023-12-19 22:00:31 +01:00
Markus Fisch 8426f66cfb Fix indent in settings.gradle
Match this with the rest of the project.
2023-12-18 12:27:34 +01:00
Meen BeeseandGitHub 3dfa84a33e Simplification and clean up of Gradle files
* Some linting suggestions by Android Studio
* Move all repository declarations to settings
2023-12-18 09:41:07 +01:00
Markus Fisch 38f658e253 Advance version number to 1.62.3 2023-12-15 12:18:14 +01:00
Markus Fisch 287d99e2b4 Make clicks select list items when in action mode
Avoids having to do long presses for every item you want to add
to the selection.
2023-12-14 22:57:31 +01:00
Markus Fisch 5cfcde9d4a Update pt-br translation
Thanks to m_s_
2023-12-12 23:05:47 +01:00
Markus Fisch b2730259fc Advance version number to 1.62.2 2023-12-12 21:56:37 +01:00
Markus Fisch 61a2d3023c Add rMQR Code on existing installations
So it's working immediately after an update.
2023-12-12 21:50:28 +01:00
CertainBotandGitHub 239ac7bd1a Update Spanish translation
- New strings translated. Typos ammended.
- Términos nuevos traducidos. Corrección de erratas.
2023-12-11 21:58:35 +01:00
Markus Fisch 4048314cf7 Advance version number to 1.62.1 2023-12-11 20:19:18 +01:00
Markus Fisch 3dbd772847 Add rMQR Code to barcode format resources
To make it selectable, too.
2023-12-11 20:12:23 +01:00
Markus Fisch 3a08465c35 Add rMQR Code list of supported codes in README 2023-12-10 18:24:08 +01:00
Markus Fisch 92e3669ee5 Advance version number to 1.62.0 2023-12-10 15:49:33 +01:00
Markus Fisch 8b297f4768 Ignore plurals warning for qr_version_and_modules
Because a QR Code always has more than one module.
2023-12-10 15:45:13 +01:00
Markus Fisch 42b7f1179b Remove incomplete zh-rTW locales translation
Because this got incomplete and so won't work with the values
array anymore.

Also it's probably a really bad idea to translate locales anyway.
If you pick a wrong one by accident, you'll have a hard time
changing it back to something you can understand.
2023-12-10 15:40:37 +01:00
Markus Fisch 9d6854c282 Suppress false-positive warning about TileService
`TileService#startActivityAndCollapse(Intent)` is only ever run
below UpsideDownCake, but Lint doesn't understand this yet.
2023-12-10 15:37:48 +01:00
Markus Fisch d943b7deff Suppress warning about commit()ing prefs
Because `commit()` is intentional where it's used.
2023-12-10 15:36:10 +01:00
Markus Fisch a138da9d9b Update Kotlin version 2023-12-10 15:35:28 +01:00
Markus Fisch 194338a0d2 Remove "many" quantity from French plural
Lint says this isn't relevant to French.
2023-12-10 15:34:27 +01:00
Markus Fisch 7eb8fdcce1 Add Brave search to and de/fa/tr locales
This was missing from the original commit.
2023-12-10 15:33:21 +01:00
Markus Fisch 2bf1796340 Improve naming of shared wrapper files
And add extensions so it's easier to deal with these files
in other contexts.
2023-12-10 15:14:31 +01:00
Markus Fisch cc42015da2 Wipe share file in HistoryFragment too 2023-12-09 22:20:33 +01:00
Markus Fisch 648c39c439 Rename share file to just "contents"
A better fit when sharing the history in this file.
2023-12-09 22:20:00 +01:00
Markus Fisch 8c25b05cda Remove menu group to make actions fold 2023-12-09 22:16:16 +01:00
Markus Fisch 780bebc9d1 Share history as a file
To make sharing the history via Bluetooth more easily, as Android
converts plain text to HTML there.
2023-12-09 22:01:51 +01:00
Markus Fisch bf7eb7efd5 Update tools version and gradle wrapper
And finally add the `compileOptions` block to make Gradle 8
work, because the Google guys are still unable to fix their bugs.
2023-12-09 16:29:48 +01:00
Markus Fisch d00d08bae3 Update to latest ZXingCpp 2023-12-09 15:13:45 +01:00
Markus Fisch 379256a7fe Share selected scans only from history
If there is a selection. Otherwise use the search filter as before.
2023-11-26 19:54:08 +01:00
Markus Fisch 180339269f Advance version number to 1.61.2 2023-11-22 19:16:16 +01:00
v72807647andGitHub 49af17cba8 Fix quick setting tile failed to launch on Android 14
SDK 34 deprecates startActivityAndCollapse(Intent), making it
throws an UnsupportedOperationException on Android 14 devices
instead of launching the app.

Added new function call for API 34, tested with Android 14 and
Android 9 devices.
2023-11-22 09:24:30 +01:00
Markus Fisch a03a187d2d Escape apostrophes in french translation
Apostrophe characters need to be escaped in XML.
2023-11-20 10:55:29 +01:00
solokotandGitHub 84806e70d7 Update Russian translation 2023-11-20 10:09:02 +01:00
NathanandGitHub 058de5a141 Update french locale 2023-11-18 12:28:07 +01:00
Kryštof ČernýandGitHub d330fbd195 Update CZ translation 2023-11-16 13:45:59 +01:00
Markus Fisch f5bc6f664f Fix Xiaomi MIUI lockscreen barcode shortcut 2023-11-15 22:07:50 +01:00
Markus Fisch 464279807c Advance version number to 1.61.1 2023-11-14 23:10:09 +01:00
Markus Fisch f7e5ef473a Add menu item to share scanned contents as a file
Because when sharing plain text via Bluetooth, Android explicitly
converts anything to HTML, which is not always what we want.

Details here:
https://stackoverflow.com/q/29907030

This feature is also useful to share binary contents more easily.
2023-11-13 20:26:31 +01:00
Markus Fisch ff733ef59e Fix opening recreated barcodes with binary content
Use the raw bytes not only for the recreated image but also
when opening the barcode in BarcodeFragment.
2023-11-10 21:16:05 +01:00
Markus Fisch 35b110a472 Fix key of general format restriction preference
Makes this setting have an effect again.

I somehow forgot to update the keys in the XMLs as well.
2023-11-08 20:41:32 +01:00
Markus Fisch 58eabf4754 Update to SDK 34 2023-11-01 18:12:57 +01:00
Markus Fisch 1f273a48a5 Catch IAE when parsing a WIFI configuration
Because some arguments may be out of range.

For example, SSIDs cannot be longer than 32 characters in some
versions of Android.

See: https://github.com/markusfisch/BinaryEye/issues/402
2023-10-29 13:53:58 +01:00
solokotandGitHub 785a64725d Update Russian translation 2023-10-24 08:56:32 +02:00
Markus Fisch 7a42e1de73 Recreate binary barcodes too
Since this is easily possible now.
2023-10-23 21:11:01 +02:00
7heMechandGitHub 9856ab7b6b Add Bulgarian translation and Brave search engine 2023-10-20 14:14:36 +02:00
Markus Fisch 746ff143d8 Advance version number to 1.61.0 2023-10-13 23:49:38 +02:00
Markus Fisch b0d4d94973 Add test for a VCARD with an explicit TYPE
And make the other tests fail if null was returned.
2023-10-13 21:11:22 +02:00
Markus Fisch c604ad5ef2 Fix type regex for VCARDs
The previous regex captured just the first letter of the type argument
because it was modified to be not greedy.
2023-10-13 21:08:01 +02:00
Markus Fisch 91c2b324ac Refactor pick_file to pick_image_file
And use `pick_file` to pick any file, not just an image.
2023-10-12 22:23:27 +02:00
Markus Fisch df59fd7446 Update Kotlin and coroutines library version 2023-10-12 22:15:19 +02:00
Markus Fisch 449fd1f26c Limit read binary data for encoding
And use just the first 4096 bytes, which is way too optimistic
as there is no barcode format available that can save that much
data.

This is just to avoid an OutOfMemoryError. Maybe it's better to
just show an error message for files that are too big?
2023-10-13 13:16:24 +02:00
Markus Fisch 72d839c6e2 Support encoding binary data from a file
So binary data can finally be re-encoded too.
2023-10-13 12:48:13 +02:00
Markus Fisch ab8c993a81 Add padding for spinners on encode fragment
Enlarges the touch area and makes the spinners easier to use.
2023-10-13 11:47:36 +02:00
Markus Fisch 5f5bd913d6 Move content EditText to the top
As this is probably the most important input field.
2023-10-13 11:47:36 +02:00
Markus Fisch 92fe4dd55f Reformat code with Android Studio
And apply the latest style changes.
2023-10-12 20:32:01 +02:00
Markus Fisch dab95176f0 Remember last selected margin for generation
More convenient.

Maybe we should store these settings for each barcode format.
2023-10-12 20:29:23 +02:00
Markus Fisch 650503ba70 Update camera library to latest version 2023-10-08 20:17:58 +02:00
solokotandGitHub 61078f214e Update Russian translation 2023-10-02 13:33:45 +02:00
Markus Fisch a4b639edfe Add margin parameter for encoding barcodes 2023-09-20 13:46:34 +02:00
Markus Fisch f37878dbb8 Replace toUpperCase() with uppercase()
Because toUpperCase(Locale) is deprecated.
2023-09-19 22:19:35 +02:00
Markus Fisch 10fde7c3d6 Add intent for MIUI fingerprint shortcut
So it's possible to start BinaryEye from the MIUI of Xiaomi devices.
2023-09-19 14:46:34 +02:00
Markus Fisch a098fcb48c Always save and use rawBytes from ZXingCpp
If available.

Because the text member may already be UTF-8 encoded, and this
cannot simply be converted back into a byte array for the hex dump.

So for ContentType.GS1, ContentType.ISO15434 and ContentType.TEXT
there could be inconsistencies between the displayed content and the
hex dump, because the latter was generated from the (already encoded)
content.
2023-09-15 19:08:29 +02:00
m_s_andMarkus Fisch f162977640 Update Brazilian Portuguese translation 2023-09-03 20:18:50 +02:00
Oleg KoretskyandGitHub 66cab58d7b Fix missing Copy button for binary QRs 2023-08-31 13:54:29 +02:00
John VenessandGitHub e99e2d7dcc Update English strings 2023-08-20 11:23:53 +02:00
Sergiy StuparandGitHub 9045a27a5d Update Ukrainian translation 2023-08-14 12:00:14 +02:00
Markus Fisch 9e1208c72b Update ZXing C++ wrapper 2023-08-13 19:09:32 +02:00
Markus Fisch 90410f232a Update Kotlin version 2023-08-13 18:09:29 +02:00
ddarfantasyandGitHub 6769fa7e37 Update zh-CN translation 2023-08-13 17:50:26 +02:00
Markus Fisch 72444f5081 Update ZXing C++ wrapper 2023-07-06 12:40:12 +02:00
Markus Fisch 9a3ca4ffa3 Update ZXing C++ wrapper 2023-05-20 14:24:34 +02:00
Markus Fisch f572923720 Add changelog for latest version for f-droid 2023-05-19 18:57:15 +02:00
Markus Fisch 4971f58f44 Advance version number to 1.60.3 2023-05-19 18:55:49 +02:00
m_s_andMarkus Fisch 0bf24080a8 Update Brazilian Portuguese translation 2023-05-19 18:49:51 +02:00
Markus Fisch 04a21f71ef Update Kotlin version 2023-05-11 20:37:39 +02:00
Unknownman820andGitHub e22a215863 Update Chinese translation 2023-05-04 12:10:51 +02:00
Markus Fisch 881829e7b2 Fix saving custom language
This preference needs to be committed so it is written to disk
immediately.

Otherwise the app will restart before this preference is presisted.
2023-05-03 20:48:40 +02:00
Kryštof ČernýandGitHub 2ad261f3b6 Update Czech localisation 2023-05-01 19:25:05 +02:00
Kryštof ČernýandGitHub e0fdda78b3 Fix library name in en-US/full_description.txt 2023-05-01 19:24:42 +02:00
solokotandGitHub 9c3710c56d Update Russian translation 2023-04-28 12:11:22 +02:00
bovirusandGitHub ddcab46d47 Update Italian language 2023-04-25 21:20:15 +02:00
Markus Fisch 076da26075 Advance version number to 1.60.2 2023-04-25 19:28:27 +02:00
Markus Fisch dbc37714f9 Remember checkbox state for unescaping input 2023-04-24 23:22:00 +02:00
Markus Fisch 50e57eaf25 Expand escape sequences in encoding input
So escape sequences can be used when encoding a barcode.
2023-04-24 23:14:58 +02:00
Markus Fisch 340f7f9d1d Remove lateinit for BT members
And use safe calls (?.) instead of catching the NullPointerException.

If something can be null, we should use the appropriate type for it
to make things explicit.
2023-04-19 21:13:05 +02:00
Unknownman820andGitHub a42bd793bb Update Taiwan translation 2023-04-04 08:56:49 +02:00
KamaleiZestriandGitHub 6e531f5395 Fix crash when not connected to a Bluetooth server 2023-03-29 12:06:20 +02:00
Markus Fisch 79b3fddcf8 Set BT results from return values
So the result variables can be immutable.

And use simpler terms for their names to make this easier to
read and understand.
2023-03-28 21:28:22 +02:00
Markus Fisch 09f3c8f193 Use "when" instead of "if/else"
More Kotlin idiomatic.

Also use full words instead of abbreviations for clarity.
2023-03-28 21:26:37 +02:00
Markus Fisch 5f82f216fd Advance version number to 1.60.1 2023-03-27 22:11:43 +02:00
Markus Fisch 8424e3ea31 Add Bluetooth permissions to PRIVACY
And explain why these are required.
2023-03-27 22:05:31 +02:00
Markus Fisch 15ce34aa92 Add legacy Bluetooth permissions for SDK < 12
According to the docs, BLUETOOTH and BLUETOOTH_ADMIN are required
for Android versions below SDK 31.

See:
https://developer.android.com/guide/topics/connectivity/bluetooth/permissions
2023-03-27 22:02:09 +02:00
Markus Fisch 8dfc1e37c0 Assign sendScanBluetooth properly
getBoolean() returns the current value.
2023-03-27 21:41:42 +02:00
Markus Fisch a0d3b7ba87 Catch SecurityExc when BT permission is missing
Make sure not to crash the app because the bluetooth permission is
missing. The app should gracefully handle this situation. It's
perfectly okay when the app doesn't have the permission to use
bluetooth. This is really a very niche use-case.
2023-03-27 19:04:39 +02:00
Markus Fisch a234dc3a9a Only ask for BT permission on activation
Doesn't make sense to ask for permssion when we deactivate bluetooth.

Also simplify this code a bit.
2023-03-27 19:04:39 +02:00
Markus Fisch a00f70e25b Check bluetooth permission before using API
Without this check, the app will crash as soon as the PreferencesFragment
is initialized when the user didn't already grant Bluetooth permission.

Always make sure to check the permissions.
2023-03-27 21:17:30 +02:00
Markus Fisch ef555c0153 Disable bluetooth sending by default
This is a pretty niche use-case and so it should be disabled by
default. Especially because it requires runtime permissions.
2023-03-27 21:14:37 +02:00
Markus Fisch 9fffb53f73 Remove stray character from Italian translation 2023-03-16 23:05:13 +01:00
bovirusandGitHub b82c6200c0 Update italian language 2023-03-16 23:03:16 +01:00
solokotandGitHub 5bb347c392 Update Russian translation 2023-03-16 18:34:30 +01:00
bovirusandGitHub f8033614fa Update italian language 2023-03-16 00:23:21 +01:00
Markus Fisch 46cc2fb1a1 Make showResult an extension of Activity
And remove all the "activity." prefixes.
2023-03-15 20:57:10 +01:00
Markus Fisch f2ac1e77bb Update gradle wrapper 2023-03-15 20:53:18 +01:00
Markus Fisch 89c84880c6 Update Kotlin version 2023-03-15 20:52:58 +01:00
Markus Fisch 352fe07850 Remove useless class wrapping for BluetoothSender
Since all members and properties are static so it's more idiomatic
to make them simple top-level functions.
2023-03-15 20:51:38 +01:00
Markus Fisch 00abe33314 Add new string resources to translations
Otherwise lint will fail.
2023-03-15 20:37:30 +01:00
Markus Fisch 142df25d7e Only check BLUETOOTH_CONNECT on Android S+
This symbol doesn't exist before Android S.
2023-03-15 20:31:01 +01:00
Markus Fisch 021d775835 Clean up new additions
Fix indent, apply auto format, remove commented out code, etc.
2023-03-15 20:29:44 +01:00
KamaleiZestriandGitHub 06df0c4d64 Support sending scans via bluetooth
Makes it possible to send scanned codes to a bluetooth device.

Should work with any bluetooth receiver that shares it's UUID
of "8a8478c9-2ca8-404b-a0de-101f34ab71ae".
2023-03-15 12:09:06 +01:00
Markus Fisch 32c70af812 Remove plural for one for Korean translation
Because lint tells me it's not required for Korean.
2023-03-01 21:42:10 +01:00
Markus Fisch 7cc1777e7a Add barcode_info plurals for polish translation
With the default strings, silly enough.

Otherwise Lint will throw an error (!) because these plurals
are missing.

Now, I doubt I will ever meet the person who thought it would
be wise to throw an error if a plurals tag is missing in a
translation, but I sure hope this person will learn to make
better decisions some day.
2023-03-01 21:36:45 +01:00
Anthony RyanandGitHub 23e0564594 Losslessly compress PNG images
Using Efficient-Compression-Tool we can reduce the size by 254.17KB (15.8405%)
without changing visual appearance.
2023-03-01 21:29:50 +01:00
Markus Fisch 0447384bea Update tools and gradle wrapper 2023-03-01 12:09:59 +01:00
Markus Fisch b4d66e28e2 Fix resolving EC level letter for recreation
While ZXing C++ reports the QR Code error correction level with four
letters (following the spec), it expects the level for encoding as an
integer in a range from 0 to 8.
2023-02-28 21:09:52 +01:00
Markus Fisch ee1c56d99f Update ZXing C++ wrapper 2023-02-23 20:03:04 +01:00
Markus Fisch 33ef78443e Advance version number to 1.59.0 2023-02-22 18:37:57 +01:00
Markus Fisch 9f1e34b06d Add setting error correction for AZTEC/PDF417 too
Because ZXing C++ supports setting an error correction level for
these formats too.
2023-02-20 20:04:01 +01:00
Markus Fisch 24766f55d4 Fix setting error correction level for QR Codes
ZXing C++ expects the error correction level to be in a range from
0 to 8, so the four possible values for QR codes need to be translated
into this range.
2023-02-19 18:59:11 +01:00
Markus Fisch 0ae7d080e3 Add missing changelogs for F-Droid 2023-02-09 22:22:05 +01:00
Markus Fisch 7d8c6016db Optimize monochrome launcher icon with avocado 2023-02-09 22:19:37 +01:00
KamaleiZestriandGitHub 3d923035e3 Follow beep settings for error beeps too 2023-02-06 10:18:52 +01:00
Markus Fisch a489f3aa22 Remove finishing string resource
And show the more generic `camera_error` message instead.

This message makes no sense anymore now that the app isn't
finished.
2023-02-05 20:30:55 +01:00
Markus Fisch 7dfd7a1303 Remove finishing activity if camera is unavailable
While the camera is the main feature of the camera acitvity,
there are other features that are still useful even if the
camera is not available.

So it's better not automatically finish the camera activity
but let the user decide.
2023-02-05 18:03:32 +01:00
Markus Fisch 9abc01fc4f Advance version number to 1.58.2 2023-01-31 12:36:32 +01:00
Markus Fisch d10a33c1b7 Check string is not empty before calling last()
Because `String.last()` will throw a NoSuchElementException when
the string is empty.
2023-01-31 10:51:18 +01:00
Markus Fisch 357b2b5d59 Advance version number to 1.58.1 2023-01-31 10:51:18 +01:00
Markus Fisch dfd64405da Fix design of monochrome launcher icon
Monochrome icons don't use any color, not even black or white.
2023-01-30 18:41:56 +01:00
Markus Fisch 045daf9deb Remove committing preferences
This made only sense when this app used RenderScript, which could
cause the app to crash before settings were saved.
2023-01-25 20:31:11 +01:00
Tom XinandGitHub 46b9ef516c Update zh-rCn translation 2023-01-21 20:52:19 +01:00
Markus Fisch 0265909fd7 Advance version number to 1.58.0 2023-01-19 20:28:25 +01:00
Markus Fisch ceef92bb7c Alternate binarizers to improve detection
By default, ZXing uses LOCAL_AVERAGE, but this does not work well
with inverted barcodes on low-contrast backgrounds.

Of course, it would be better to improve ZXing's HybridBinarizer
(which is used for LOCAL_AVERAGE), but this is not trivial.
2023-01-19 20:16:49 +01:00
Markus Fisch 46785bab01 Update ZXing C++ wrapper for DecodeHints
The latest version makes all native `DecodeHints` available
to the JVM.
2023-01-19 20:12:54 +01:00
Markus Fisch ccbb0f9e8a Fix terminating string tag in italian translation 2023-01-19 18:01:27 +01:00
bovirusandGitHub 833d8e93d3 Update italian language 2023-01-19 17:53:41 +01:00
Markus Fisch 6dff9992d8 Remove META parameter from README
Since the migration to ZXing-C++ this parameter doesn't exist anymore.
2023-01-16 20:20:44 +01:00
Markus Fisch 0b3dfe93cf Silently try openUrl() for OpenOrSearchAction
Avoid using `execShareIntent()` for OpenOrSearchAction because it
will toast a message when it fails.
2023-01-11 19:21:54 +01:00
Markus Fisch e79ad49d15 Remove converting charset for PDF417
This is handled by ZXing C++ now.

Converting the text again destroys it.
2023-01-08 17:35:57 +01:00
chreddyandGitHub 38f7ba1ec6 Add Danish translation 2023-01-08 17:35:43 +01:00
NPLandGitHub 4d268240fd Update Japanese translation 2023-01-03 09:22:56 +01:00
Markus Fisch 1131f749a9 Make database migrations extension functions
Simpler and more Kotlin idiomatic.
2023-01-02 12:12:43 +01:00
Markus Fisch 93a3cd2620 Migrate version number to version string
Because ZXing C++ did switch to a String to support versions
with letters.
2023-01-02 12:10:02 +01:00
Mystery0 MandGitHub a866a2eab3 Add fillAlpha for monochrome icon 2022-12-29 13:58:22 +01:00
Markus Fisch c5d6bfa4b2 Respect setting to ignore duplicates for bulk scan
Before this, the previously scanned code was _always_ ignored for
bulk scans to avoid scanning the same code twice. But this isn't
always desirable, and the setting to ignore duplicates should have
an effect to give the user control over this behaviour.
2022-12-21 18:24:00 +01:00
Markus Fisch 6a63bc4e06 Increase the duration of the vibration
Because 100ms is too short for some devices and the vibration
cannot be felt.

This should fix https://github.com/markusfisch/BinaryEye/issues/334
2022-12-21 18:17:03 +01:00
Markus Fisch 09ff334c4c Update link on how to write a good commit message 2022-12-21 16:09:56 +01:00
solokotandGitHub 45a2ac0647 Update Russian translation 2022-12-20 13:06:40 +01:00
Markus Fisch 425f3a7b80 Advance version number to 1.57.0 2022-12-19 20:45:36 +01:00
Markus Fisch f895b42cfe Allow removing individual network suggestions
From Android R on, because `WifiManager.getNetworkSuggestions()`
is not available on Q.
2022-12-19 20:29:04 +01:00
Markus Fisch ecd4c487e3 Update pt-br translation
Thanks to m_s_
2022-12-18 12:39:05 +01:00
Markus Fisch 750671c408 Remove carriage returns from string resource 2022-12-17 13:27:24 +01:00
Markus Fisch ead856221f Update Kotlin version 2022-12-16 10:19:28 +01:00
Markus Fisch 34f14d0ff0 Update to latest ZXing C++ version 2022-12-16 10:19:28 +01:00
motive0604andGitHub 1b21297d83 Add Korean language 2022-12-13 09:38:34 +01:00
solokotandGitHub 53cb6fe007 Update Russian translation 2022-12-11 22:44:17 +01:00
Markus Fisch f1daa19056 Add optional audio feedback for scanning
And also support a couple of signal noises.
2022-12-10 13:52:39 +01:00
Markus Fisch bd9da5889c Add a semicolon to WIFI strings if it is missing
Because many generators don't terminate the expression.
2022-12-07 12:21:22 +01:00
Markus Fisch d5949e557a Add a RequiresApi annotation
To make clear `addNetworkFromBuilder` is only ever called
on Android Q+.
2022-12-06 20:09:41 +01:00
Markus Fisch 51079e5208 Update tools version 2022-12-06 20:07:55 +01:00
Markus Fisch 09f3368bf0 Update ScalingImageView library 2022-12-06 20:06:22 +01:00
Markus Fisch 84e03e3cc8 Fix typo in german translation 2022-12-06 20:03:20 +01:00
Markus Fisch cff299771a Remove resource references from vector drawables
Resource references will not work correctly in images generated
for vector icons for API < 24.
2022-12-06 20:00:43 +01:00
Markus Fisch 7a327ab7c4 Fix transferring shared text to encode fragment 2022-12-03 21:37:18 +01:00
Markus Fisch 72b8924574 Fix parsing a WiFi network on Android Q
`WifiConnector.parse()` still returned a `WifiNetworkSuggestion`
on Android Q but `WifiConnector.addNetwork()` is now expecting a
`WifiNetworkSuggestion.Builder` :8

Anyway, this code wasn't very good to start with. Having the exact
same check in two different places with the exact same meaning is
bad. This bug is a good example of that ;)

So now, `WifiConnector.addNetwork()` checks what type of object it
got and does the right thing with it.

Thanks to @JacobKochems who found this bug and made me aware of it.
2022-12-01 19:23:17 +01:00
Markus Fisch 527b7a6a59 Use deprecated WiFi API on Android Q
Because WifiManager.getNetworkSuggestions() is only available from
Android R (API level 30) on. So added network suggestions cannot be
queried on Android Q (API level 29) which will be important to allow
removing them individually.
2022-11-30 21:07:05 +01:00
Markus Fisch e9267e4e5a Accept lower case for E, PH and T in WiFi configs
While the spec demands upper case here, it's probably best to
be liberal and accept lower case values as well.
2022-11-30 12:11:29 +01:00
Tom XinandGitHub ac18733368 Update Chinese translation 2022-11-22 12:17:05 +01:00
Markus Fisch 300c3bfd26 Advance version number to 1.56.3 2022-11-21 13:04:19 +01:00
Markus Fisch 6449d9962d Simplify RegEx to recognize URLs
Because the current expression leads to an infinite loop for some
inputs. For example, this string was reported to make the app crash:
com.taobao.arthas.boot.ProcessUtils.findJavaHome(ProcessUtils.java:222)

The problem seems to be somehow related to case insensitivity as the
expression works when insensitivity is removed. Also, the infinite
loop only occurs on an Android device - not in unit tests.

Anyway, the current expression was too complex anyway, and this
simpler one should be good enough.
2022-11-21 12:37:15 +01:00
Markus Fisch 12554e63e2 Remove deep linking intent filter
Was just added to make Lint happy but is no longer necessary.
2022-11-17 19:21:28 +01:00
Markus Fisch af60ee6edb Fix indent in chinese translation 2022-11-17 16:17:20 +01:00
grenagitandGitHub fe31fc6df2 Update french translation 2022-11-17 14:07:36 +01:00
Markus Fisch 6d4f3cafbf Update pt-br translation 2022-11-14 18:06:17 +01:00
Oğuz ErsenandGitHub fa23b8078c Update Turkish translation 2022-11-13 12:49:20 +01:00
jhih_yuandGitHub e3c0fe8857 Update zh_TW 2022-11-11 09:45:58 +01:00
Markus Fisch fa568686b6 Advance version number to 1.56.2 2022-11-09 20:34:08 +01:00
Markus Fisch 304357b43c Update to latest ZXing C++ wrapper
To fix generating text output of 1D barcodes.
2022-11-09 18:27:06 +01:00
Markus Fisch 1e76b14038 Advance version number to 1.56.1 2022-11-08 20:33:08 +01:00
Markus Fisch d0777350a2 Allow pinching to size in device pixels
To allow small codes to be small on screen.

This makes it easier to use a generated barcode in place of a small
printed barcode. There are scanners out there that expect a barcode
to be of a certain (small) size.
2022-11-08 20:28:41 +01:00
Markus Fisch c7c1e74240 Update to latest ZXing C++ wrapper
To fix handling of tab characters.
2022-11-08 20:27:23 +01:00
Markus Fisch c793e1399b Fix view/frame transformation matrix
The view offset needs to be translated first - not last, where the
matrix is no longer in view space.
2022-11-08 20:27:23 +01:00
zmniandGitHub c550a6ff51 Update indonesian translation 2022-11-08 20:27:00 +01:00
solokotandGitHub 05b0f719e0 Updated Russian translation 2022-11-07 18:22:34 +01:00
182 changed files with 4146 additions and 1401 deletions
+120
View File
@@ -1,5 +1,125 @@
# Change Log
## 1.63.1
* Include scan label in exports/shares
* Update Russian translation
## 1.63.0
* Edit scan label in decoding screen
* Add a button to add generated codes to the history
* Improve scanning from images
* Improve asking for camera permission
* Fix margin for recreated barcodes
* Limit duration of error beep to 1s
* Update Ukrainian translation
* Update Spanish translation
## 1.62.3
* Extend selection with single taps in history listing
* Update Brazilian Portuguese translation
## 1.62.2
* Include rMQR Code for updates
## 1.62.1
* Make rMQR Code selectable
## 1.62.0
* Add support for rMQR (rectangular Micro QR) Code
* Share history as a file
* Only share selected scans from history
## 1.61.2
* Fix quick settings tile for Android 14
* Fix Xiaomi MIUI lockscreen barcode shortcut
* Update French translation
* Update Czech translation
* Update Russian translation
## 1.61.1
* Share scanned contents as a file
* Recreate binary barcodes too
* Add Bulgarian translation and Brave search engine
* Update Russian translation
* Fix general format restriction from preferences
## 1.61.0
* Support encoding binary data
* Add margin parameter for encoding
* Add copying binary data as hex dump
* Fix parsing TYPE argument in VCARDs
* Update Russian translation
* Update Brazilian Portuguese translation
* Update Ukrainian translation
* Update Chinese translation
## 1.60.3
* Fix saving custom language
* Update Brazilian Portuguese translation
* Update Chinese translation
* Update Czech localisation
* Update Russian translation
* Update Italian language
## 1.60.2
* Expand escape sequences in encoding input
* Update Taiwan translation
## 1.60.1
* Support sending scans via bluetooth
* Fix resolving error correction level for recreation
* Improve asset image compression
* Update ZXing C++
* Update Italian translation
* Update Russian translation
## 1.59.0
* Add setting error correction for AZTEC/PDF417
* Fix setting error correction level for QR Codes
* Make error beeps follow beep settings too
* Don't close the app when cameras is unavailable
## 1.58.2
* Fix parsing WiFi network suggestions
## 1.58.1
* Update zh-rCn translation
* Fix monochrome launcher icon
## 1.58.0
* Improve detection of inverted barcodes
* Optionally allow duplicates in bulk scan
* Add Danish translation
* Update Italian translation
* Update Japanese translation
* Update Russian translation
* Fix charset decoding for PDF 417
* Fix monochrome icon
## 1.57.0
* Add optional audio feedback for scanning
* Allow removing individual WiFi network suggestions
* Recognize incomplete and malformed WiFi barcodes
* Fix using shared text for encoding
* Add Korean language
* Update Chinese translation
* Update Português/Brasil translation
* Update Russian translation
## 1.56.3
* Fix parsing URLs in barcodes
* Update French, Portuguese, Turkish and Taiwan translation
## 1.56.2
* Fix generating 1D barcode text output
## 1.56.1
* Allow pinching a barcode to its generated size
* Fix view/frame transformation matrix
* Fix handling of tab characters
* Update Indonesian translation
* Update Russian translation
## 1.56.0
* Add an action item to lock/unlock free rotation
* Support setting fore and background colors when creating barcodes
+1 -1
View File
@@ -17,4 +17,4 @@ Then write a [good commit message][commit_messages] to keep the history
meaningful and useful. One feature, one commit.
[android_best_practices]: https://developer.android.com/distribute/best-practices/develop/
[commit_messages]: https://juffalow.com/other/write-good-git-commit-message
[commit_messages]: https://git-scm.com/book/en/v2/Distributed-Git-Contributing-to-a-Project#_commit_guidelines
+5
View File
@@ -33,6 +33,11 @@ testurl:
-c android.intent.category.BROWSABLE \
-d 'http://markusfisch.de/BinaryEye?ret=http%3A%2F%2Fmarkusfisch.de%2F%3Fresult%3D{RESULT}'
testxiaomi:
adb shell am start -a android.intent.action.VIEW \
-c android.intent.category.BROWSABLE \
-d 'market://details?id=com.xiaomi.scanner'
install:
adb $(TARGET) install -r app/build/outputs/apk/debug/app-debug.apk
+18 -3
View File
@@ -12,11 +12,18 @@ hesitate to contact me.
Required to add a WiFi network from a barcode (usually a QR Code).
Before Android Q, this feature requires using
[WifiManager.getConfiguredNetworks][configuredNetworks]
[WifiManager.getConfiguredNetworks][getConfiguredNetworks]
which requires `ACCESS_FINE_LOCATION`.
On Android Q an better, this permission is not requested nor required.
### [BLUETOOTH][BLUETOOTH], [BLUETOOTH_ADMIN][BLUETOOTH_ADMIN] and [BLUETOOTH_CONNECT][BLUETOOTH_CONNECT]
Required to optionally forward scans to a Bluetooth device.
[BLUETOOTH][BLUETOOTH] and [BLUETOOTH_ADMIN][BLUETOOTH_ADMIN] are required
before Android S. [BLUETOOTH_CONNECT][BLUETOOTH_CONNECT] from Android S on.
### [CAMERA][CAMERA]
Required to read a barcode from the camera image.
@@ -46,15 +53,23 @@ which requires `CHANGE_WIFI_STATE`.
Required to add a WiFi network from a barcode (usually a QR Code).
Before Android Q, this feature requires using
[WifiManager.getConfiguredNetworks][configuredNetworks]
[WifiManager.getConfiguredNetworks][getConfiguredNetworks]
which requires `ACCESS_WIFI_STATE`.
From Android R, `ACCESS_WIFI_STATE` is required for
[WifiManager.getNetworkSuggestions][getNetworkSuggestions]
which is used to remove individual network suggestions.
[ACCESS_FINE_LOCATION]: https://developer.android.com/reference/android/Manifest.permission#ACCESS_FINE_LOCATION
[BLUETOOTH]: https://developer.android.com/reference/android/Manifest.permission#BLUETOOTH
[BLUETOOTH_ADMIN]: https://developer.android.com/reference/android/Manifest.permission#BLUETOOTH_ADMIN
[BLUETOOTH_CONNECT]: https://developer.android.com/reference/android/Manifest.permission#BLUETOOTH_CONNECT
[CAMERA]: https://developer.android.com/reference/android/Manifest.permission#CAMERA
[INTERNET]: https://developer.android.com/reference/android/Manifest.permission#INTERNET
[VIBRATE]: https://developer.android.com/reference/android/Manifest.permission#VIBRATE
[WRITE_EXTERNAL_STORAGE]: https://developer.android.com/reference/android/Manifest.permission#WRITE_EXTERNAL_STORAGE
[CHANGE_WIFI_STATE]: https://developer.android.com/reference/android/Manifest.permission#CHANGE_WIFI_STATE
[ACCESS_WIFI_STATE]: https://developer.android.com/reference/android/Manifest.permission#ACCESS_FINE_LOCATION
[configuredNetworks]: https://developer.android.com/reference/android/net/wifi/WifiManager#getConfiguredNetworks()
[getConfiguredNetworks]: https://developer.android.com/reference/android/net/wifi/WifiManager#getConfiguredNetworks()
[addNetworkSuggestions]: https://developer.android.com/reference/android/net/wifi/WifiManager#addNetworkSuggestions(java.util.List%3Candroid.net.wifi.WifiNetworkSuggestion%3E)
[getNetworkSuggestions]: https://developer.android.com/reference/android/net/wifi/WifiManager#getNetworkSuggestions()
+2 -1
View File
@@ -43,6 +43,7 @@ ZXing can read the following barcode formats:
* [PDF417][pdf417]
* [QR CODE][qr_code]
* [Micro QR Code][micro_qr_code]
* [rMQR Code][rmqr_code]
* [RSS 14][rss]
* [RSS EXPANDED][rss]
* [UPC A][upc_a]
@@ -84,7 +85,6 @@ Supported symbols are:
* `RESULT` - scanned content
* `RESULT_BYTES` - raw result as a hex string
* `FORMAT` - barcode format
* `META` - the meta data, if available
### SCAN Intent
@@ -151,6 +151,7 @@ class YourActivity : Activity() {
[pdf417]: https://en.wikipedia.org/wiki/PDF417
[qr_code]: https://en.wikipedia.org/wiki/QR_code
[micro_qr_code]: https://en.wikipedia.org/wiki/QR_code#Micro_QR_code
[rmqr_code]: https://www.qrcode.com/en/codes/rmqr.html
[rss]: https://en.wikipedia.org/wiki/GS1_DataBar
[upc_a]: https://en.wikipedia.org/wiki/Universal_Product_Code
[upc_e]: https://en.wikipedia.org/wiki/Universal_Product_Code#UPC-E
+24 -12
View File
@@ -2,15 +2,15 @@ apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
android {
namespace 'de.markusfisch.android.binaryeye'
compileSdkVersion sdk_version
namespace "de.markusfisch.android.binaryeye"
compileSdk sdk_version
defaultConfig {
minSdkVersion 9
targetSdkVersion sdk_version
minSdk 9
targetSdk sdk_version
versionCode 106
versionName '1.56.0'
versionCode 129
versionName '1.63.1'
}
signingConfigs {
@@ -53,18 +53,30 @@ android {
enableSplit = false
}
}
compileOptions {
// Required for Gradle 8.
sourceCompatibility = 17
targetCompatibility = 17
}
}
dependencies {
testImplementation 'junit:junit:4.13.2'
// Kotlin
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.1"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4"
// Support
implementation "com.android.support:appcompat-v7:$support_version"
implementation "com.android.support:design:$support_version"
implementation "com.android.support:preference-v7:$support_version"
implementation "com.android.support:preference-v14:$support_version"
implementation 'com.github.markusfisch:CameraView:1.9.1'
implementation 'com.github.markusfisch:ScalingImageView:1.4.0'
implementation 'com.github.markusfisch:zxing-cpp:v1.4.0.7'
// External
implementation 'com.github.markusfisch:CameraView:1.10.0'
implementation 'com.github.markusfisch:ScalingImageView:1.4.1'
implementation 'com.github.markusfisch:zxing-cpp:v2.2.0.1'
// Testing
testImplementation 'junit:junit:4.13.2'
}
-2
View File
@@ -3,5 +3,3 @@
-dontwarn android.support.v8.**
-keep class android.support.v7.widget.SearchView { *; }
-keep class de.markusfisch.android.zxingcpp.** { *; }
Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.2 KiB

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 8.2 KiB

+8 -6
View File
@@ -6,14 +6,18 @@
android:xlargeScreens="true"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"
android:maxSdkVersion="28"/>
<uses-permission android:name="android.permission.BLUETOOTH"
android:maxSdkVersion="30"/>
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"
android:maxSdkVersion="30"/>
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT"/>
<uses-permission android:name="android.permission.CAMERA"/>
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.VIBRATE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="28"/>
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE"/>
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"
android:maxSdkVersion="28"/>
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
<uses-feature android:name="android.hardware.camera"/>
<uses-feature android:name="android.hardware.camera.autofocus"/>
<uses-sdk tools:overrideLibrary="android.support.v14.preference"/>
@@ -68,10 +72,8 @@
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>
<data android:host="markusfisch.de"/>
<data android:pathPrefix="/BinaryEye"/>
<data android:scheme="http"/>
<data android:scheme="https"/>
<data android:host="details" android:scheme="market"
android:pathSuffix="id=com.xiaomi.scanner"/>
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW"/>
@@ -23,7 +23,7 @@ object OpenOrSearchAction : IAction {
url: String,
search: Boolean
) {
if (!context.openUrl(url) && search) {
if (!context.openUrl(url, silent = true) && search) {
openSearch(context, url)
}
}
@@ -1,6 +1,6 @@
package de.markusfisch.android.binaryeye.actions.vtype
import java.util.*
import java.util.Locale
object VTypeParser {
private const val BACKSLASH_R_LEGACY =
@@ -39,6 +39,6 @@ data class VTypeProperty(
} ?: this.info.firstOrNull()
companion object {
private val typeRegex = """^(?:TYPE=)(.+?)[:;]?.*$""".toRegex(RegexOption.IGNORE_CASE)
private val typeRegex = """^TYPE=([A-Z_]+)[:;]?.*$""".toRegex(RegexOption.IGNORE_CASE)
}
}
@@ -8,7 +8,7 @@ import android.provider.ContactsContract
import de.markusfisch.android.binaryeye.R
import de.markusfisch.android.binaryeye.actions.IntentAction
import de.markusfisch.android.binaryeye.actions.vtype.VTypeParser
import java.util.*
import java.util.Locale
object VCardAction : IntentAction() {
override val iconResId: Int
@@ -45,10 +45,13 @@ object VCardAction : IntentAction() {
val (extraPhoneType, extraPhone) = when (index) {
0 -> ContactsContract.Intents.Insert.PHONE_TYPE to
ContactsContract.Intents.Insert.PHONE
1 -> ContactsContract.Intents.Insert.SECONDARY_PHONE_TYPE to
ContactsContract.Intents.Insert.SECONDARY_PHONE
2 -> ContactsContract.Intents.Insert.TERTIARY_PHONE_TYPE to
ContactsContract.Intents.Insert.TERTIARY_PHONE_TYPE
else -> return@forEachIndexed
}
phoneProperty.firstTypeOrFirstInfo?.also {
@@ -65,10 +68,13 @@ object VCardAction : IntentAction() {
val (extraMailType, extraMail) = when (index) {
0 -> ContactsContract.Intents.Insert.EMAIL_TYPE to
ContactsContract.Intents.Insert.EMAIL
1 -> ContactsContract.Intents.Insert.SECONDARY_EMAIL_TYPE to
ContactsContract.Intents.Insert.SECONDARY_EMAIL
2 -> ContactsContract.Intents.Insert.TERTIARY_EMAIL_TYPE to
ContactsContract.Intents.Insert.TERTIARY_EMAIL
else -> return@forEachIndexed
}
mailProperty.firstTypeOrFirstInfo?.also {
@@ -184,7 +190,7 @@ object VCardAction : IntentAction() {
}
private val String.addressType: Int?
get() = when (this.toUpperCase(Locale.US)) {
get() = when (this.uppercase()) {
"HOME" -> ContactsContract.CommonDataKinds.StructuredPostal.TYPE_HOME
"WORK" -> ContactsContract.CommonDataKinds.StructuredPostal.TYPE_WORK
"OTHER" -> ContactsContract.CommonDataKinds.StructuredPostal.TYPE_OTHER
@@ -7,9 +7,7 @@ import de.markusfisch.android.binaryeye.content.openUrl
object WebAction : IAction {
private val colloquialRegex =
"""^(http[s]*://)*([a-z0-9]+[a-z0-9-]*[a-z0-9]+[.])+[a-z]{2,}([?#/]+[\w/=&;._-]*)*$""".toRegex(
RegexOption.IGNORE_CASE
)
"^(http[s]*://)*[A-Za-z0-9]{3,}\\.[A-Za-z]{2,}[^ \t\r\n]*$".toRegex()
override val iconResId: Int = R.drawable.ic_action_open
override val titleResId: Int = R.string.open_url
@@ -22,10 +22,9 @@ object WifiAction : IAction {
context: Context,
data: ByteArray
) = withContext(Dispatchers.IO) {
val wifiConfig = WifiConnector.parse(
String(data)
) ?: return@withContext
val message = WifiConnector.addNetwork(context, wifiConfig)
val message = WifiConnector.parse(String(data))?.let {
WifiConnector.addNetwork(context, it)
} ?: R.string.wifi_config_failed
withContext(Dispatchers.Main) {
context.toast(message)
}
@@ -10,12 +10,6 @@ import android.support.annotation.RequiresApi
import de.markusfisch.android.binaryeye.R
import java.util.*
@RequiresApi(Build.VERSION_CODES.Q)
fun removeNetworkSuggestions(context: Context): Int =
(context.applicationContext.getSystemService(
Context.WIFI_SERVICE
) as WifiManager).removeNetworkSuggestions(listOf())
/**
* Normal:
* WIFI:S:[network SSID];T:<WPA|WEP|nopass|>;P:[network password];H:<true|false|>;;
@@ -50,12 +44,21 @@ object WifiConnector {
passwordBlock?.apply {
invoke(parsedData.password)
}
return if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
// WifiConfiguration is deprecated in Android Q.
@Suppress("DEPRECATION")
WifiConfiguration().apply(parsedData)
} else {
WifiNetworkSuggestion.Builder().apply(parsedData)
return try {
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q) {
// WifiConfiguration is deprecated in Android Q but
// because it's only possible in R to query network
// suggestions, we still use the deprecated API on Q.
@Suppress("DEPRECATION")
WifiConfiguration().apply(parsedData)
} else {
WifiNetworkSuggestion.Builder().apply(parsedData)
}
} catch (e: IllegalArgumentException) {
// Some arguments may be out of range, e.g. SSIDs cannot
// be longer than 32 characters in some versions of Android.
// See: https://github.com/markusfisch/BinaryEye/issues/402
null
}
}
@@ -63,33 +66,23 @@ object WifiConnector {
val wifiManager = context.applicationContext.getSystemService(
Context.WIFI_SERVICE
) as WifiManager
return if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
// WifiConfiguration is deprecated in Android Q.
@Suppress("DEPRECATION")
val wifiConfig = config as WifiConfiguration
if (wifiManager.enableWifi() &&
wifiManager.removeOldNetwork(wifiConfig) &&
wifiManager.enableNewNetwork(wifiConfig)
) {
R.string.wifi_added
} else {
R.string.wifi_config_failed
}
// WifiConfiguration is deprecated in Android Q.
@Suppress("DEPRECATION")
return if ((config is WifiConfiguration &&
wifiManager.enableWifi() &&
wifiManager.removeOldNetwork(config) &&
wifiManager.enableNewNetwork(config)) ||
(Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q &&
config is WifiNetworkSuggestion.Builder &&
wifiManager.addNetworkFromBuilder(config))
) {
R.string.wifi_added
} else {
val suggestion = (config as WifiNetworkSuggestion.Builder).build()
val suggestions = listOf(suggestion)
// Remove previous conflicting network suggestion.
wifiManager.removeNetworkSuggestions(suggestions)
val result = wifiManager.addNetworkSuggestions(suggestions)
if (result == WifiManager.STATUS_NETWORK_SUGGESTIONS_SUCCESS) {
R.string.wifi_added
} else {
R.string.wifi_config_failed
}
R.string.wifi_config_failed
}
}
internal fun parseMap(string: String): Map<String, String>? {
internal fun parseMap(input: String): Map<String, String>? {
// Normally those codes should have the last semicolon, but many
// generators don't add it.
val wifiRegex = """^WIFI:((?:.+?:(?:[^\\;]|\\.)*;)+);?$""".toRegex()
@@ -97,7 +90,7 @@ object WifiConnector {
// and : because many QR Code creators don't escape properly.
val pairRegex = """(.+?):((?:[^\\;]|\\.)*);""".toRegex()
return wifiRegex.matchEntire(
string
input.trimAndTerminate()
)?.groupValues?.get(1)?.let { pairs ->
pairRegex.findAll(pairs).map { pair ->
pair.groupValues[1].uppercase(Locale.US) to pair.groupValues[2]
@@ -126,7 +119,7 @@ object WifiConnector {
requireSdk(Build.VERSION_CODES.JELLY_BEAN_MR2) {
WifiEnterpriseConfig.Eap.NONE
}
} else when (inputMap["E"]) {
} else when (inputMap["E"]?.uppercase()) {
"AKA" -> requireSdk(Build.VERSION_CODES.LOLLIPOP) { WifiEnterpriseConfig.Eap.AKA }
"AKA_PRIME" -> requireSdk(Build.VERSION_CODES.M) { WifiEnterpriseConfig.Eap.AKA_PRIME }
"NONE" -> requireSdk(Build.VERSION_CODES.JELLY_BEAN_MR2) { WifiEnterpriseConfig.Eap.NONE }
@@ -143,7 +136,7 @@ object WifiConnector {
requireSdk(Build.VERSION_CODES.JELLY_BEAN_MR2) {
WifiEnterpriseConfig.Phase2.NONE
}
} else when (inputMap["PH2"]) {
} else when (inputMap["PH2"]?.uppercase()) {
"AKA" -> requireSdk(Build.VERSION_CODES.O) { WifiEnterpriseConfig.Phase2.AKA }
"AKA_PRIME" -> requireSdk(Build.VERSION_CODES.O) { WifiEnterpriseConfig.Phase2.AKA_PRIME }
"GTC" -> requireSdk(Build.VERSION_CODES.JELLY_BEAN_MR2) { WifiEnterpriseConfig.Phase2.GTC }
@@ -179,10 +172,11 @@ object WifiConnector {
data: SimpleDataAccessor
): WifiNetworkSuggestion.Builder? {
try {
when (data.securityType) {
when (data.securityType.uppercase()) {
"WPA", "WPA2" -> {
data.password?.let { setWpa2Passphrase(it) }
}
"WPA2-EAP" -> {
setWpa2EnterpriseConfig(WifiEnterpriseConfig().apply {
identity = data.identity
@@ -192,9 +186,11 @@ object WifiConnector {
data.phase2Method?.let { phase2Method = it }
})
}
"WPA3" -> {
data.password?.let { setWpa3Passphrase(it) }
}
"WPA3-EAP" -> {
setWpa3EnterpriseConfig(WifiEnterpriseConfig().apply {
identity = data.identity
@@ -240,8 +236,8 @@ object WifiConnector {
fun WifiConfiguration.applySecurity(
data: SimpleDataAccessor
): WifiConfiguration? {
when (data.securityType) {
"", "nopass" -> allowedKeyManagement.set(WifiConfiguration.KeyMgmt.NONE)
when (data.securityType.uppercase()) {
"", "NOPASS" -> allowedKeyManagement.set(WifiConfiguration.KeyMgmt.NONE)
"WEP" -> @Suppress("DEPRECATION") /* WEP as insecure */ {
passwordWithQuotes?.let {
wepKeys[0] = it
@@ -254,6 +250,7 @@ object WifiConnector {
allowedGroupCiphers.set(WifiConfiguration.GroupCipher.WEP40)
allowedGroupCiphers.set(WifiConfiguration.GroupCipher.WEP104)
}
"WPA" -> {
passwordWithQuotes?.let {
preSharedKey = it
@@ -270,6 +267,7 @@ object WifiConnector {
allowedGroupCiphers.set(WifiConfiguration.GroupCipher.TKIP)
allowedGroupCiphers.set(WifiConfiguration.GroupCipher.CCMP)
}
"WPA2-EAP" -> requireSdk(Build.VERSION_CODES.JELLY_BEAN_MR2) {
passwordWithQuotes?.let {
preSharedKey = it
@@ -303,11 +301,32 @@ object WifiConnector {
}
}
@RequiresApi(Build.VERSION_CODES.Q)
private fun WifiManager.addNetworkFromBuilder(
builder: WifiNetworkSuggestion.Builder
): Boolean {
val suggestion = builder.build()
val suggestions = listOf(suggestion)
// Remove previous conflicting network suggestion.
removeNetworkSuggestions(suggestions)
val result = addNetworkSuggestions(suggestions)
return result == WifiManager.STATUS_NETWORK_SUGGESTIONS_SUCCESS
}
// Make sure the string ends with a semicolon.
private fun String.trimAndTerminate(): String {
var trimmed = trim()
if (trimmed.isNotEmpty() && trimmed.last() != ';') {
trimmed += ";"
}
return trimmed
}
// Keep possibility of wrongly unescaped \ by explicitly searching
// for special chars.
private val escapedRegex = """\\([\\;,":])""".toRegex()
private val String.unescape: String
get() = this.replace(escapedRegex) { escaped ->
get() = replace(escapedRegex) { escaped ->
escaped.groupValues[1]
}
@@ -8,11 +8,8 @@ import android.content.pm.PackageManager
import android.graphics.Matrix
import android.graphics.Rect
import android.hardware.Camera
import android.media.AudioManager
import android.media.ToneGenerator
import android.net.Uri
import android.os.Bundle
import android.os.Vibrator
import android.support.design.widget.FloatingActionButton
import android.support.v7.app.AppCompatActivity
import android.support.v7.widget.Toolbar
@@ -23,7 +20,13 @@ import android.view.View
import android.widget.SeekBar
import de.markusfisch.android.binaryeye.R
import de.markusfisch.android.binaryeye.adapter.prettifyFormatName
import de.markusfisch.android.binaryeye.app.*
import de.markusfisch.android.binaryeye.app.PERMISSION_CAMERA
import de.markusfisch.android.binaryeye.app.applyLocale
import de.markusfisch.android.binaryeye.app.db
import de.markusfisch.android.binaryeye.app.hasBluetoothPermission
import de.markusfisch.android.binaryeye.app.hasCameraPermission
import de.markusfisch.android.binaryeye.app.prefs
import de.markusfisch.android.binaryeye.bluetooth.sendBluetoothAsync
import de.markusfisch.android.binaryeye.content.copyToClipboard
import de.markusfisch.android.binaryeye.content.execShareIntent
import de.markusfisch.android.binaryeye.content.openUrl
@@ -33,20 +36,21 @@ import de.markusfisch.android.binaryeye.graphics.FrameMetrics
import de.markusfisch.android.binaryeye.graphics.mapPosition
import de.markusfisch.android.binaryeye.graphics.setFrameRoi
import de.markusfisch.android.binaryeye.graphics.setFrameToView
import de.markusfisch.android.binaryeye.media.releaseToneGenerators
import de.markusfisch.android.binaryeye.net.sendAsync
import de.markusfisch.android.binaryeye.net.urlEncode
import de.markusfisch.android.binaryeye.os.error
import de.markusfisch.android.binaryeye.os.getVibrator
import de.markusfisch.android.binaryeye.os.vibrate
import de.markusfisch.android.binaryeye.view.errorFeedback
import de.markusfisch.android.binaryeye.view.initSystemBars
import de.markusfisch.android.binaryeye.view.scanFeedback
import de.markusfisch.android.binaryeye.view.setPaddingFromWindowInsets
import de.markusfisch.android.binaryeye.widget.DetectorView
import de.markusfisch.android.binaryeye.widget.toast
import de.markusfisch.android.cameraview.widget.CameraView
import de.markusfisch.android.zxingcpp.ZxingCpp
import de.markusfisch.android.zxingcpp.ZxingCpp.Format
import de.markusfisch.android.zxingcpp.ZxingCpp.BarcodeFormat
import de.markusfisch.android.zxingcpp.ZxingCpp.Binarizer
import de.markusfisch.android.zxingcpp.ZxingCpp.ReaderOptions
import de.markusfisch.android.zxingcpp.ZxingCpp.Result
import java.nio.charset.Charset
import kotlin.math.max
import kotlin.math.min
import kotlin.math.roundToInt
@@ -55,13 +59,12 @@ class CameraActivity : AppCompatActivity() {
private val frameRoi = Rect()
private val matrix = Matrix()
private lateinit var vibrator: Vibrator
private lateinit var cameraView: CameraView
private lateinit var detectorView: DetectorView
private lateinit var zoomBar: SeekBar
private lateinit var flashFab: FloatingActionButton
private var formats = setOf<String>()
private var formatsToRead = setOf<BarcodeFormat>()
private var frameMetrics = FrameMetrics()
private var decoding = true
private var returnResult = false
@@ -72,6 +75,7 @@ class CameraActivity : AppCompatActivity() {
private var restrictFormat: String? = null
private var ignoreNext: String? = null
private var fallbackBuffer: IntArray? = null
private var requestCameraPermission = true
override fun onRequestPermissionsResult(
requestCode: Int,
@@ -82,8 +86,7 @@ class CameraActivity : AppCompatActivity() {
PERMISSION_CAMERA -> if (grantResults.isNotEmpty() &&
grantResults[0] != PackageManager.PERMISSION_GRANTED
) {
toast(R.string.no_camera_no_fun)
finish()
toast(R.string.camera_error)
}
}
}
@@ -118,8 +121,6 @@ class CameraActivity : AppCompatActivity() {
// custom locale.
setTitle(R.string.scan_code)
vibrator = getVibrator()
initSystemBars(this)
setSupportActionBar(findViewById(R.id.toolbar) as Toolbar)
@@ -144,6 +145,7 @@ class CameraActivity : AppCompatActivity() {
fallbackBuffer = null
saveZoom()
detectorView.saveCropHandlePos()
releaseToneGenerators()
}
override fun onResume() {
@@ -156,14 +158,18 @@ class CameraActivity : AppCompatActivity() {
ignoreNext = null
}
setReturnTarget(intent)
if (hasCameraPermission()) {
// Avoid asking multiple times when the user has denied access
// for this session. Otherwise ActivityCompat.requestPermissions()
// will trigger onResume() again and again.
if (hasCameraPermission(requestCameraPermission)) {
openCamera()
}
requestCameraPermission = false
}
private fun updateHints() {
val restriction = restrictFormat
formats = if (restriction != null) {
formatsToRead = if (restriction != null) {
title = getString(
R.string.scan_format,
prettifyFormatName(restriction)
@@ -172,7 +178,7 @@ class CameraActivity : AppCompatActivity() {
} else {
setTitle(R.string.scan_code)
prefs.barcodeFormats
}
}.toFormatSet()
}
private fun setReturnTarget(intent: Intent?) {
@@ -180,6 +186,7 @@ class CameraActivity : AppCompatActivity() {
intent?.action == "com.google.zxing.client.android.SCAN" -> {
returnResult = true
}
intent?.dataString?.isReturnUrl() == true -> {
finishAfterShowingResult = true
returnUrlTemplate = intent.data?.getQueryParameter("ret")
@@ -248,44 +255,52 @@ class CameraActivity : AppCompatActivity() {
createBarcode()
true
}
R.id.history -> {
startActivity(MainActivity.getHistoryIntent(this))
true
}
R.id.pick_file -> {
startActivityForResult(
Intent.createChooser(
Intent(Intent.ACTION_GET_CONTENT).apply {
type = "image/*"
},
getString(R.string.pick_file)
getString(R.string.pick_image_file)
),
PICK_FILE_RESULT_CODE
)
true
}
R.id.switch_camera -> {
switchCamera()
true
}
R.id.bulk_mode -> {
bulkMode = bulkMode xor true
item.isChecked = bulkMode
ignoreNext = null
true
}
R.id.restrict_format -> {
showRestrictionDialog()
true
}
R.id.preferences -> {
startActivity(MainActivity.getPreferencesIntent(this))
true
}
R.id.info -> {
openReadme()
true
}
else -> super.onOptionsItemSelected(item)
}
}
@@ -354,6 +369,7 @@ class CameraActivity : AppCompatActivity() {
progress = zoomBar.progress
return true
}
MotionEvent.ACTION_MOVE -> {
if (prefs.zoomBySwiping) {
v ?: return false
@@ -367,6 +383,7 @@ class CameraActivity : AppCompatActivity() {
return true
}
}
MotionEvent.ACTION_UP -> {
// Stop calling focusTo() as soon as it returns false
// to avoid throwing and catching future exceptions.
@@ -414,7 +431,6 @@ class CameraActivity : AppCompatActivity() {
override fun onCameraError() {
this@CameraActivity.toast(R.string.camera_error)
finish()
}
override fun onCameraReady(camera: Camera) {
@@ -426,22 +442,39 @@ class CameraActivity : AppCompatActivity() {
updateFrameRoiAndMappingMatrix()
ignoreNext = null
decoding = true
// These settings can't change while the camera is open.
val options = ReaderOptions(
tryHarder = prefs.tryHarder,
tryRotate = prefs.autoRotate,
tryInvert = true,
tryDownscale = true,
maxNumberOfSymbols = 1
)
var useLocalAverage = false
camera.setPreviewCallback { frameData, _ ->
if (decoding) {
useLocalAverage = useLocalAverage xor true
ZxingCpp.readByteArray(
frameData,
frameMetrics.width,
frameRoi.left, frameRoi.top,
frameRoi.width(), frameRoi.height(),
frameMetrics.orientation,
formats.joinToString(),
prefs.tryHarder,
prefs.autoRotate,
tryInvert = true,
tryDownscale = true
)?.let { result ->
options.apply {
// By default, ZXing uses LOCAL_AVERAGE, but
// this does not work well with inverted
// barcodes on low-contrast backgrounds.
binarizer = if (useLocalAverage) {
Binarizer.LOCAL_AVERAGE
} else {
Binarizer.GLOBAL_HISTOGRAM
}
formats = formatsToRead
}
)?.let { results ->
val result = results.first()
if (result.text != ignoreNext) {
postResult(result.redact())
postResult(result)
decoding = false
}
}
@@ -561,7 +594,7 @@ class CameraActivity : AppCompatActivity() {
detectorView.coordinates
)
)
vibrator.vibrate()
scanFeedback()
val returnUri = returnUrlTemplate?.let {
try {
completeUrl(it, result)
@@ -577,16 +610,13 @@ class CameraActivity : AppCompatActivity() {
setResult(Activity.RESULT_OK, getReturnIntent(result))
finish()
}
returnUri != null -> execShareIntent(
Intent(Intent.ACTION_VIEW, returnUri)
)
else -> {
showResult(
this@CameraActivity,
result,
vibrator,
bulkMode
)
showResult(result, bulkMode)
// If this app was invoked via a deep link but without
// a return URI, we probably don't want to return to
// the camera screen after scanning, but to the caller.
@@ -596,7 +626,9 @@ class CameraActivity : AppCompatActivity() {
}
}
if (bulkMode) {
ignoreNext = result.text
if (prefs.ignoreConsecutiveDuplicates) {
ignoreNext = result.text
}
if (prefs.showToastInBulkMode) {
toast(result.text)
}
@@ -617,42 +649,16 @@ class CameraActivity : AppCompatActivity() {
}
}
fun Result.redact() = if (
format == Format.PDF_417.name &&
text.isNotEmpty()
) {
Result(
format,
contentType,
String(
text.toByteArray(Charset.forName("ISO-8859-1")),
Charset.forName("UTF-8")
),
position,
orientation,
rawBytes,
ecLevel,
symbologyIdentifier,
sequenceSize,
sequenceIndex,
sequenceId,
readerInit,
lineCount,
versionNumber,
gtin
)
} else {
this
}
fun Set<String>.toFormatSet(): Set<BarcodeFormat> = map {
BarcodeFormat.valueOf(it)
}.toSet()
fun showResult(
activity: Activity,
fun Activity.showResult(
result: Result,
vibrator: Vibrator,
bulkMode: Boolean = false,
) {
if (prefs.copyImmediately) {
activity.copyToClipboard(result.text)
copyToClipboard(result.text)
}
val scan = result.toScan()
if (prefs.useHistory) {
@@ -660,7 +666,7 @@ fun showResult(
}
if (prefs.sendScanActive && prefs.sendScanUrl.isNotEmpty()) {
if (prefs.sendScanType == "4") {
activity.openUrl(
openUrl(
prefs.sendScanUrl + scan.content.urlEncode()
)
return
@@ -670,21 +676,42 @@ fun showResult(
prefs.sendScanType
) { code, body ->
if (code == null || code < 200 || code > 299) {
vibrator.error()
if (!activity.isSilent()) {
beepBeepBeep()
}
errorFeedback()
}
if (body != null && body.isNotEmpty()) {
activity.toast(body)
if (!body.isNullOrEmpty()) {
toast(body)
} else if (code == null || code > 299) {
activity.toast(R.string.background_request_failed)
toast(R.string.background_request_failed)
}
}
}
if (prefs.sendScanBluetooth &&
prefs.sendScanBluetoothHost.isNotEmpty() &&
hasBluetoothPermission()
) {
scan.sendBluetoothAsync(
prefs.sendScanBluetoothHost
) { connected, sent ->
toast(
when {
!connected -> {
errorFeedback()
R.string.bluetooth_connect_fail
}
!sent -> {
errorFeedback()
R.string.bluetooth_send_fail
}
else -> R.string.bluetooth_send_success
}
)
}
}
if (!bulkMode) {
activity.startActivity(
MainActivity.getDecodeIntent(activity, scan)
startActivity(
MainActivity.getDecodeIntent(this, scan)
)
}
}
@@ -709,25 +736,7 @@ private fun completeUrl(urlTemplate: String, result: Result) = Uri.parse(
urlTemplate
.replace("{RESULT}", result.text.urlEncode())
.replace("{RESULT_BYTES}", result.rawBytes.toHexString())
.replace(
"{FORMAT}", result.format.urlEncode()
)
.replace("{FORMAT}", result.format.name.urlEncode())
// And support {CODE} from the old ZXing app, too.
.replace("{CODE}", result.text.urlEncode())
)
private fun Context.isSilent(): Boolean {
val am = getSystemService(Context.AUDIO_SERVICE) as AudioManager
return when (am.ringerMode) {
AudioManager.RINGER_MODE_SILENT,
AudioManager.RINGER_MODE_VIBRATE -> true
else -> false
}
}
private fun beepBeepBeep() {
ToneGenerator(AudioManager.STREAM_ALARM, 100).startTone(
ToneGenerator.TONE_CDMA_CONFIRM,
1000
)
}
@@ -9,7 +9,12 @@ import android.support.v4.app.Fragment
import android.support.v7.app.AppCompatActivity
import android.support.v7.widget.Toolbar
import de.markusfisch.android.binaryeye.R
import de.markusfisch.android.binaryeye.app.*
import de.markusfisch.android.binaryeye.app.PERMISSION_LOCATION
import de.markusfisch.android.binaryeye.app.PERMISSION_WRITE
import de.markusfisch.android.binaryeye.app.applyLocale
import de.markusfisch.android.binaryeye.app.permissionGrantedCallback
import de.markusfisch.android.binaryeye.app.prefs
import de.markusfisch.android.binaryeye.app.setFragment
import de.markusfisch.android.binaryeye.database.Scan
import de.markusfisch.android.binaryeye.fragment.DecodeFragment
import de.markusfisch.android.binaryeye.fragment.EncodeFragment
@@ -80,10 +85,14 @@ class MainActivity : AppCompatActivity() {
return when {
intent.hasExtra(PREFERENCES) -> PreferencesFragment()
intent.hasExtra(HISTORY) -> HistoryFragment()
intent.hasExtra(ENCODE) -> EncodeFragment.newInstance()
intent.hasExtra(DECODED) -> DecodeFragment.newInstance(
intent.getParcelableExtra(DECODED)!!
intent.hasExtra(ENCODE) -> EncodeFragment.newInstance(
intent.getStringExtra(ENCODE)
)
intent.hasExtra(DECODED) -> intent.getScanExtra(DECODED)?.let {
DecodeFragment.newInstance(it)
} ?: DecodeFragment()
else -> PreferencesFragment()
}
}
@@ -128,3 +137,12 @@ class MainActivity : AppCompatActivity() {
}
}
}
private fun Intent.getScanExtra(name: String): Scan? = if (
Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU
) {
@Suppress("DEPRECATION")
getParcelableExtra(name)
} else {
getParcelableExtra(name, Scan::class.java)
}
@@ -7,9 +7,8 @@ import android.graphics.Matrix
import android.graphics.Rect
import android.graphics.RectF
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.Parcelable
import android.os.Vibrator
import android.support.v7.app.AppCompatActivity
import android.support.v7.widget.Toolbar
import android.view.Menu
@@ -21,18 +20,24 @@ import de.markusfisch.android.binaryeye.graphics.crop
import de.markusfisch.android.binaryeye.graphics.fixTransparency
import de.markusfisch.android.binaryeye.graphics.loadImageUri
import de.markusfisch.android.binaryeye.graphics.mapPosition
import de.markusfisch.android.binaryeye.os.getVibrator
import de.markusfisch.android.binaryeye.os.vibrate
import de.markusfisch.android.binaryeye.media.releaseToneGenerators
import de.markusfisch.android.binaryeye.view.colorSystemAndToolBars
import de.markusfisch.android.binaryeye.view.initSystemBars
import de.markusfisch.android.binaryeye.view.recordToolbarHeight
import de.markusfisch.android.binaryeye.view.scanFeedback
import de.markusfisch.android.binaryeye.view.setPaddingFromWindowInsets
import de.markusfisch.android.binaryeye.widget.CropImageView
import de.markusfisch.android.binaryeye.widget.DetectorView
import de.markusfisch.android.binaryeye.widget.toast
import de.markusfisch.android.zxingcpp.ZxingCpp
import de.markusfisch.android.zxingcpp.ZxingCpp.Binarizer
import de.markusfisch.android.zxingcpp.ZxingCpp.ReaderOptions
import de.markusfisch.android.zxingcpp.ZxingCpp.Result
import kotlinx.coroutines.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlin.math.max
import kotlin.math.min
import kotlin.math.roundToInt
@@ -41,8 +46,15 @@ class PickActivity : AppCompatActivity() {
private val matrix = Matrix()
private val parentJob = Job()
private val scope = CoroutineScope(Dispatchers.IO + parentJob)
private val options = ReaderOptions(
tryHarder = true,
tryRotate = true,
tryInvert = true,
tryDownscale = true,
maxNumberOfSymbols = 1,
formats = prefs.barcodeFormats.toFormatSet()
)
private lateinit var vibrator: Vibrator
private lateinit var cropImageView: CropImageView
private lateinit var detectorView: DetectorView
private lateinit var freeRotationItem: MenuItem
@@ -62,8 +74,6 @@ class PickActivity : AppCompatActivity() {
// locale.
setTitle(R.string.pick_code_to_scan)
vibrator = getVibrator()
initSystemBars(this)
val toolbar = findViewById(R.id.toolbar) as Toolbar
recordToolbarHeight(toolbar)
@@ -115,52 +125,47 @@ class PickActivity : AppCompatActivity() {
}
private fun scanWithinBounds(bitmap: Bitmap) {
val viewRoi = if (detectorView.roi.width() < 1) {
cropImageView.getBoundsRect()
} else {
detectorView.roi
}
val mappedRect = cropImageView.mappedRect
val cropped = bitmap.crop(
getNormalizedRoi(mappedRect, viewRoi),
cropImageView.imageRotation,
cropImageView.pivotX,
cropImageView.pivotY
) ?: return
val croppedInView = Rect(
max(viewRoi.left, mappedRect.left.roundToInt()),
max(viewRoi.top, mappedRect.top.roundToInt()),
min(viewRoi.right, mappedRect.right.roundToInt()),
min(viewRoi.bottom, mappedRect.bottom.roundToInt())
val viewRoi = Rect(
if (detectorView.roi.width() < 1) {
cropImageView.getBoundsRect()
} else {
detectorView.roi
}
)
matrix.apply {
setScale(
croppedInView.width().toFloat() / cropped.width,
croppedInView.height().toFloat() / cropped.height
)
postTranslate(
croppedInView.left.toFloat(),
croppedInView.top.toFloat()
)
}
val mappedRect = RectF(cropImageView.mappedRect)
val imageRotation = cropImageView.imageRotation
val pivotX = cropImageView.pivotX
val pivotY = cropImageView.pivotY
scope.launch {
ZxingCpp.readBitmap(
cropped,
0, 0,
cropped.width, cropped.height,
0,
prefs.barcodeFormats.joinToString(),
tryHarder = true,
tryRotate = true,
tryInvert = true,
tryDownscale = true
)?.let {
val cropped = bitmap.crop(
getNormalizedRoi(mappedRect, viewRoi),
imageRotation,
pivotX,
pivotY
) ?: return@launch
val croppedInView = Rect(
max(viewRoi.left, mappedRect.left.roundToInt()),
max(viewRoi.top, mappedRect.top.roundToInt()),
min(viewRoi.right, mappedRect.right.roundToInt()),
min(viewRoi.bottom, mappedRect.bottom.roundToInt())
)
matrix.apply {
setScale(
croppedInView.width().toFloat() / cropped.width,
croppedInView.height().toFloat() / cropped.height
)
postTranslate(
croppedInView.left.toFloat(),
croppedInView.top.toFloat()
)
}
cropped.decode()?.first()?.let {
withContext(Dispatchers.Main) {
if (isFinishing) {
return@withContext
}
result = it
vibrator.vibrate()
scanFeedback()
detectorView.update(
matrix.mapPosition(
it.position,
@@ -172,10 +177,31 @@ class PickActivity : AppCompatActivity() {
}
}
// By default, ZXing uses LOCAL_AVERAGE, but this does not work
// well with inverted barcodes on low-contrast backgrounds.
private fun Bitmap.decode() = ZxingCpp.readBitmap(
this,
0, 0,
width, height,
0,
options.apply {
binarizer = Binarizer.LOCAL_AVERAGE
}
) ?: ZxingCpp.readBitmap(
this,
0, 0,
width, height,
0,
options.apply {
binarizer = Binarizer.GLOBAL_HISTOGRAM
}
)
override fun onDestroy() {
super.onDestroy()
detectorView.saveCropHandlePos()
parentJob.cancel()
releaseToneGenerators()
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
@@ -192,12 +218,14 @@ class PickActivity : AppCompatActivity() {
rotateClockwise()
true
}
R.id.toggle_free -> {
prefs.freeRotation = prefs.freeRotation xor true
cropImageView.freeRotation = prefs.freeRotation
freeRotationItem.updateFreeRotationIcon()
true
}
else -> super.onOptionsItemSelected(item)
}
}
@@ -217,9 +245,7 @@ class PickActivity : AppCompatActivity() {
}
private fun loadSentImage(intent: Intent): Bitmap? {
val uri = intent.getParcelableExtra<Parcelable>(
Intent.EXTRA_STREAM
) as? Uri ?: return null
val uri = intent.getUriExtra() ?: return null
return contentResolver.loadImageUri(uri)
}
@@ -231,7 +257,7 @@ class PickActivity : AppCompatActivity() {
private fun showResult() {
val r = result
if (r != null) {
showResult(this, r.redact(), vibrator)
showResult(r)
finish()
} else {
applicationContext.toast(R.string.no_barcode_found)
@@ -249,3 +275,12 @@ private fun getNormalizedRoi(imageRect: RectF, roi: Rect): RectF {
(roi.bottom - imageRect.top) / h
)
}
private fun Intent.getUriExtra(): Uri? = if (
Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU
) {
@Suppress("DEPRECATION")
getParcelableExtra(Intent.EXTRA_STREAM)
} else {
getParcelableExtra(Intent.EXTRA_STREAM, Uri::class.java)
}
@@ -13,7 +13,7 @@ import de.markusfisch.android.binaryeye.database.Database
import java.text.DateFormat
import java.text.ParseException
import java.text.SimpleDateFormat
import java.util.*
import java.util.Locale
class ScansAdapter(context: Context, cursor: Cursor) :
CursorAdapter(context, cursor, false) {
@@ -99,9 +99,11 @@ class ScansAdapter(context: Context, cursor: Cursor) :
icon = R.drawable.ic_label
name
}
content?.isEmpty() == true -> context.getString(
content.isNullOrEmpty() -> context.getString(
R.string.binary_data
)
else -> content
}
contentView.setCompoundDrawablesWithIntrinsicBounds(
@@ -2,7 +2,7 @@ package de.markusfisch.android.binaryeye.app
import android.content.Context
import android.os.Build
import java.util.*
import java.util.Locale
fun Context.applyLocale(localeName: String) {
if (localeName.isEmpty()) {
@@ -3,15 +3,16 @@ package de.markusfisch.android.binaryeye.app
import android.Manifest
import android.app.Activity
import android.content.pm.PackageManager
import android.os.Build
import android.support.v4.app.ActivityCompat
import android.support.v4.content.ContextCompat
var permissionGrantedCallback: (() -> Any)? = null
const val PERMISSION_CAMERA = 1
fun Activity.hasCameraPermission() = hasPermission(
fun Activity.hasCameraPermission(request: Boolean = true) = hasPermission(
Manifest.permission.CAMERA,
PERMISSION_CAMERA
if (request) PERMISSION_CAMERA else 0
)
const val PERMISSION_WRITE = 2
@@ -32,6 +33,18 @@ fun Activity.hasLocationPermission(callback: () -> Any): Boolean {
)
}
const val PERMISSION_BLUETOOTH = 4
fun Activity.hasBluetoothPermission() = if (
Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
) {
hasPermission(
Manifest.permission.BLUETOOTH_CONNECT,
PERMISSION_BLUETOOTH
)
} else {
false
}
private fun Activity.hasPermission(
permission: String,
requestCode: Int
@@ -40,11 +53,13 @@ private fun Activity.hasPermission(
permission
) != PackageManager.PERMISSION_GRANTED
) {
ActivityCompat.requestPermissions(
this,
arrayOf(permission),
requestCode
)
if (requestCode > 0) {
ActivityCompat.requestPermissions(
this,
arrayOf(permission),
requestCode
)
}
false
} else {
true
@@ -0,0 +1,116 @@
package de.markusfisch.android.binaryeye.bluetooth
import android.bluetooth.BluetoothAdapter
import android.bluetooth.BluetoothDevice
import android.bluetooth.BluetoothSocket
import android.support.v7.preference.ListPreference
import de.markusfisch.android.binaryeye.database.Scan
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.IOException
import java.io.OutputStreamWriter
import java.util.UUID
fun Scan.sendBluetoothAsync(
host: String,
callback: (Boolean, Boolean) -> Unit
) {
CoroutineScope(Dispatchers.IO).launch(Dispatchers.IO) {
val connected = if (isConnected) {
true
} else {
connect(host)
}
val sent = if (connected) {
send(content)
} else {
false
}
withContext(Dispatchers.Main) {
callback(connected, sent)
}
}
}
fun setBluetoothHosts(listPref: ListPreference) {
val devices = try {
BluetoothAdapter.getDefaultAdapter().bondedDevices
} catch (e: SecurityException) {
// Do nothing, either the user has denied Bluetooth access
// or the permission was removed by the system. We're catching
// the exception to keep the app from crashing.
null
} ?: return
listPref.entries = devices.map {
it.name
}.toTypedArray()
listPref.entryValues = devices.map {
it.address
}.toTypedArray()
listPref.callChangeListener(listPref.value)
}
private var socket: BluetoothSocket? = null
private var writer: OutputStreamWriter? = null
private val blue = BluetoothAdapter.getDefaultAdapter()
private val uuid = UUID.fromString(
"8a8478c9-2ca8-404b-a0de-101f34ab71ae"
)
private var isConnected = false
private fun connect(deviceName: String): Boolean = try {
val device = findByName(deviceName) ?: throw RuntimeException(
"Bluetooth device not found"
)
socket = device.createRfcommSocketToServiceRecord(uuid)
socket?.connect()
writer = socket?.outputStream?.writer()
isConnected = true
true
} catch (e: Exception) {
close()
false
}
private fun send(message: String): Boolean = try {
writer?.apply {
write(message)
write("\n")
flush()
}
true
} catch (e: Exception) {
close()
false
}
private fun close() {
try {
writer?.close()
} catch (e: IOException) {
// Catch exception if writer wasn't initialized.
}
try {
socket?.close()
} catch (e: IOException) {
// Nothing we can do about this but keep
// the app from crashing.
}
writer = null
socket = null
isConnected = false
}
private fun findByName(findableName: String): BluetoothDevice? {
val deviceList = blue.bondedDevices
for (device in deviceList) {
if (device.address == findableName) {
return device
}
}
return null
}
@@ -10,6 +10,8 @@ import de.markusfisch.android.binaryeye.BuildConfig
import de.markusfisch.android.binaryeye.R
import de.markusfisch.android.binaryeye.widget.toast
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
fun Context.execShareIntent(intent: Intent): Boolean = if (
!startIntent(intent)
@@ -35,11 +37,14 @@ fun Context.startIntent(intent: Intent): Boolean = try {
false
}
fun Context.openUrl(url: String): Boolean = openUri(url.parseAndNormalizeUri())
fun Context.openUri(uri: Uri): Boolean = execShareIntent(
Intent(Intent.ACTION_VIEW, uri)
)
fun Context.openUrl(url: String, silent: Boolean = false): Boolean {
val intent = Intent(Intent.ACTION_VIEW, url.parseAndNormalizeUri())
return if (silent) {
startIntent(intent)
} else {
execShareIntent(intent)
}
}
fun String.parseAndNormalizeUri(): Uri = Uri.parse(this).let {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
@@ -56,6 +61,44 @@ fun Context.shareText(text: String, mimeType: String = "text/plain") {
})
}
private var lastShareFile: File? = null
fun wipeLastShareFile() {
lastShareFile?.let {
it.delete()
lastShareFile = null
}
}
fun <T> Context.shareAsFile(content: T, fileName: String) {
val (bytes, mimeType) = when (content) {
is ByteArray -> Pair(
content,
"text/plain"
)
is String -> Pair(
content.toByteArray(),
"application/octet-stream"
)
else -> throw IllegalArgumentException(
"Unsupported content text for shareAsFile()"
)
}
wipeLastShareFile()
val file = File(externalCacheDir, fileName)
try {
FileOutputStream(file).use {
it.write(bytes)
}
} catch (e: IOException) {
toast(R.string.error_saving_file)
return
}
lastShareFile = file
shareFile(file, mimeType)
}
fun Context.shareFile(file: File, mimeType: String) {
getUriForFile(file)?.let {
shareUri(it, mimeType)
@@ -32,9 +32,10 @@ private fun exportCsv(
val columns = arrayOf(
Database.SCANS_DATETIME,
Database.SCANS_FORMAT,
Database.SCANS_NAME,
Database.SCANS_CONTENT,
Database.SCANS_ERROR_CORRECTION_LEVEL,
Database.SCANS_VERSION_NUMBER,
Database.SCANS_VERSION,
Database.SCANS_SEQUENCE_SIZE,
Database.SCANS_SEQUENCE_INDEX,
Database.SCANS_SEQUENCE_ID,
@@ -6,6 +6,7 @@ import android.database.Cursor
import android.database.sqlite.SQLiteDatabase
import android.database.sqlite.SQLiteOpenHelper
import de.markusfisch.android.binaryeye.app.prefs
import de.markusfisch.android.zxingcpp.ZxingCpp.BarcodeFormat
class Database {
private lateinit var db: SQLiteDatabase
@@ -36,7 +37,7 @@ class Database {
$SCANS_RAW,
$SCANS_FORMAT,
$SCANS_ERROR_CORRECTION_LEVEL,
$SCANS_VERSION_NUMBER,
$SCANS_VERSION,
$SCANS_SEQUENCE_SIZE,
$SCANS_SEQUENCE_INDEX,
$SCANS_SEQUENCE_ID,
@@ -69,6 +70,29 @@ class Database {
null
}
fun getScansDetailed(ids: LongArray): Cursor? = db.rawQuery(
"""SELECT
$SCANS_ID,
$SCANS_DATETIME,
$SCANS_NAME,
$SCANS_CONTENT,
$SCANS_RAW,
$SCANS_FORMAT,
$SCANS_ERROR_CORRECTION_LEVEL,
$SCANS_VERSION,
$SCANS_SEQUENCE_SIZE,
$SCANS_SEQUENCE_INDEX,
$SCANS_SEQUENCE_ID,
$SCANS_GTIN_COUNTRY,
$SCANS_GTIN_ADD_ON,
$SCANS_GTIN_PRICE,
$SCANS_GTIN_ISSUE_NUMBER
FROM $SCANS
WHERE $SCANS_ID IN (?)
ORDER BY $SCANS_DATETIME DESC
""".trimMargin(), arrayOf(ids.joinToString(","))
)
fun getScan(id: Long): Scan? = db.rawQuery(
"""SELECT
$SCANS_ID,
@@ -78,7 +102,7 @@ class Database {
$SCANS_RAW,
$SCANS_FORMAT,
$SCANS_ERROR_CORRECTION_LEVEL,
$SCANS_VERSION_NUMBER,
$SCANS_VERSION,
$SCANS_SEQUENCE_SIZE,
$SCANS_SEQUENCE_INDEX,
$SCANS_SEQUENCE_ID,
@@ -94,9 +118,9 @@ class Database {
Scan(
it.getString(SCANS_CONTENT),
it.getBlob(SCANS_RAW),
it.getString(SCANS_FORMAT),
BarcodeFormat.valueOf(it.getString(SCANS_FORMAT)),
it.getString(SCANS_ERROR_CORRECTION_LEVEL),
it.getInt(SCANS_VERSION_NUMBER),
it.getString(SCANS_VERSION),
it.getInt(SCANS_SEQUENCE_SIZE),
it.getInt(SCANS_SEQUENCE_INDEX),
it.getString(SCANS_SEQUENCE_ID),
@@ -105,7 +129,8 @@ class Database {
it.getString(SCANS_GTIN_PRICE),
it.getString(SCANS_GTIN_ISSUE_NUMBER),
it.getString(SCANS_DATETIME),
it.getLong(SCANS_ID)
it.getLong(SCANS_ID),
it.getString(SCANS_NAME),
)
} else {
null
@@ -117,18 +142,15 @@ class Database {
null,
ContentValues().apply {
put(SCANS_DATETIME, scan.dateTime)
val isRaw = scan.raw != null
if (isRaw) {
put(SCANS_CONTENT, "")
put(SCANS_CONTENT, scan.content)
if (scan.raw != null) {
put(SCANS_RAW, scan.raw)
} else {
put(SCANS_CONTENT, scan.content)
}
put(SCANS_FORMAT, scan.format)
put(SCANS_FORMAT, scan.format.name)
scan.errorCorrectionLevel?.let {
put(SCANS_ERROR_CORRECTION_LEVEL, it)
}
put(SCANS_VERSION_NUMBER, scan.versionNumber)
put(SCANS_VERSION, scan.version)
put(SCANS_SEQUENCE_SIZE, scan.sequenceSize)
put(SCANS_SEQUENCE_INDEX, scan.sequenceIndex)
put(SCANS_SEQUENCE_ID, scan.sequenceId)
@@ -139,7 +161,7 @@ class Database {
if (prefs.ignoreConsecutiveDuplicates) {
val id = getIdOfLastScan(
get(SCANS_CONTENT) as String,
if (isRaw) get(SCANS_RAW) as ByteArray else null,
get(SCANS_RAW) as ByteArray?,
scan.format
)
if (id > 0L) {
@@ -152,7 +174,7 @@ class Database {
private fun getIdOfLastScan(
content: String,
raw: ByteArray?,
format: String
format: BarcodeFormat
): Long = db.rawQuery(
"""SELECT
$SCANS_ID,
@@ -169,7 +191,7 @@ class Database {
it.getString(SCANS_CONTENT) == content &&
(raw == null || it.getBlob(SCANS_RAW)
?.contentEquals(raw) == true) &&
it.getString(SCANS_FORMAT) == format
it.getString(SCANS_FORMAT) == format.name
) {
it.getLong(SCANS_ID)
} else {
@@ -192,9 +214,9 @@ class Database {
}
private class OpenHelper(context: Context) :
SQLiteOpenHelper(context, FILE_NAME, null, 5) {
SQLiteOpenHelper(context, FILE_NAME, null, 6) {
override fun onCreate(db: SQLiteDatabase) {
createScans(db)
db.createScans()
}
override fun onUpgrade(
@@ -203,16 +225,19 @@ class Database {
newVersion: Int
) {
if (oldVersion < 2) {
addRawColumn(db)
db.addRawColumn()
}
if (oldVersion < 3) {
addMetaDataColumns(db)
db.addMetaDataColumns()
}
if (oldVersion < 4) {
addNameColumn(db)
db.addNameColumn()
}
if (oldVersion < 5) {
migrateToZxingCpp(db)
db.migrateToZxingCpp()
}
if (oldVersion < 6) {
db.migrateToVersionString()
}
}
}
@@ -228,6 +253,7 @@ class Database {
const val SCANS_FORMAT = "format"
const val SCANS_ERROR_CORRECTION_LEVEL = "error_correction_level"
const val SCANS_VERSION_NUMBER = "version_number"
const val SCANS_VERSION = "version"
const val SCANS_ISSUE_NUMBER = "issue_number"
const val SCANS_ORIENTATION = "orientation"
const val SCANS_OTHER_META_DATA = "other_meta_data"
@@ -243,9 +269,9 @@ class Database {
const val SCANS_GTIN_PRICE = "gtin_price"
const val SCANS_GTIN_ISSUE_NUMBER = "gtin_issue_number"
private fun createScans(db: SQLiteDatabase) {
db.execSQL("DROP TABLE IF EXISTS $SCANS".trimMargin())
db.execSQL(
private fun SQLiteDatabase.createScans() {
execSQL("DROP TABLE IF EXISTS $SCANS".trimMargin())
execSQL(
"""CREATE TABLE $SCANS (
$SCANS_ID INTEGER PRIMARY KEY AUTOINCREMENT,
$SCANS_DATETIME DATETIME NOT NULL,
@@ -254,7 +280,7 @@ class Database {
$SCANS_RAW BLOB,
$SCANS_FORMAT TEXT NOT NULL,
$SCANS_ERROR_CORRECTION_LEVEL TEXT,
$SCANS_VERSION_NUMBER INTEGER,
$SCANS_VERSION TEXT,
$SCANS_SEQUENCE_SIZE INTEGER,
$SCANS_SEQUENCE_INDEX INTEGER,
$SCANS_SEQUENCE_ID TEXT,
@@ -266,44 +292,45 @@ class Database {
)
}
private fun addRawColumn(db: SQLiteDatabase) {
db.execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_RAW BLOB".trimMargin())
private fun SQLiteDatabase.addRawColumn() {
execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_RAW BLOB".trimMargin())
}
private fun addMetaDataColumns(db: SQLiteDatabase) {
db.apply {
execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_ERROR_CORRECTION_LEVEL TEXT".trimMargin())
execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_ISSUE_NUMBER INT".trimMargin())
execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_ORIENTATION INT".trimMargin())
execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_OTHER_META_DATA TEXT".trimMargin())
execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_PDF417_EXTRA_METADATA TEXT".trimMargin())
execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_POSSIBLE_COUNTRY TEXT".trimMargin())
execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_SUGGESTED_PRICE TEXT".trimMargin())
execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_UPC_EAN_EXTENSION TEXT".trimMargin())
}
private fun SQLiteDatabase.addMetaDataColumns() {
execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_ERROR_CORRECTION_LEVEL TEXT".trimMargin())
execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_ISSUE_NUMBER INT".trimMargin())
execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_ORIENTATION INT".trimMargin())
execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_OTHER_META_DATA TEXT".trimMargin())
execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_PDF417_EXTRA_METADATA TEXT".trimMargin())
execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_POSSIBLE_COUNTRY TEXT".trimMargin())
execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_SUGGESTED_PRICE TEXT".trimMargin())
execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_UPC_EAN_EXTENSION TEXT".trimMargin())
}
private fun addNameColumn(db: SQLiteDatabase) {
db.execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_NAME TEXT".trimMargin())
private fun SQLiteDatabase.addNameColumn() {
execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_NAME TEXT".trimMargin())
}
private fun migrateToZxingCpp(db: SQLiteDatabase) {
db.apply {
execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_VERSION_NUMBER INTEGER".trimMargin())
execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_SEQUENCE_SIZE INTEGER".trimMargin())
execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_SEQUENCE_INDEX INTEGER".trimMargin())
execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_SEQUENCE_ID TEXT".trimMargin())
execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_GTIN_COUNTRY TEXT".trimMargin())
execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_GTIN_ADD_ON TEXT".trimMargin())
execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_GTIN_PRICE TEXT".trimMargin())
execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_GTIN_ISSUE_NUMBER TEXT".trimMargin())
execSQL("UPDATE $SCANS SET $SCANS_SEQUENCE_SIZE = -1")
execSQL("UPDATE $SCANS SET $SCANS_SEQUENCE_INDEX = -1")
execSQL("UPDATE $SCANS SET $SCANS_GTIN_COUNTRY = $SCANS_POSSIBLE_COUNTRY")
execSQL("UPDATE $SCANS SET $SCANS_GTIN_ADD_ON = $SCANS_UPC_EAN_EXTENSION")
execSQL("UPDATE $SCANS SET $SCANS_GTIN_PRICE = $SCANS_SUGGESTED_PRICE")
execSQL("UPDATE $SCANS SET $SCANS_GTIN_ISSUE_NUMBER = $SCANS_ISSUE_NUMBER")
}
private fun SQLiteDatabase.migrateToZxingCpp() {
execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_VERSION_NUMBER INTEGER".trimMargin())
execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_SEQUENCE_SIZE INTEGER".trimMargin())
execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_SEQUENCE_INDEX INTEGER".trimMargin())
execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_SEQUENCE_ID TEXT".trimMargin())
execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_GTIN_COUNTRY TEXT".trimMargin())
execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_GTIN_ADD_ON TEXT".trimMargin())
execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_GTIN_PRICE TEXT".trimMargin())
execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_GTIN_ISSUE_NUMBER TEXT".trimMargin())
execSQL("UPDATE $SCANS SET $SCANS_SEQUENCE_SIZE = -1")
execSQL("UPDATE $SCANS SET $SCANS_SEQUENCE_INDEX = -1")
execSQL("UPDATE $SCANS SET $SCANS_GTIN_COUNTRY = $SCANS_POSSIBLE_COUNTRY")
execSQL("UPDATE $SCANS SET $SCANS_GTIN_ADD_ON = $SCANS_UPC_EAN_EXTENSION")
execSQL("UPDATE $SCANS SET $SCANS_GTIN_PRICE = $SCANS_SUGGESTED_PRICE")
execSQL("UPDATE $SCANS SET $SCANS_GTIN_ISSUE_NUMBER = $SCANS_ISSUE_NUMBER")
}
private fun SQLiteDatabase.migrateToVersionString() {
execSQL("ALTER TABLE $SCANS ADD $SCANS_VERSION TEXT")
execSQL("UPDATE $SCANS SET $SCANS_VERSION = $SCANS_VERSION_NUMBER")
}
}
}
@@ -24,9 +24,10 @@ fun Cursor.exportJson(): String? {
val columns = arrayOf(
Database.SCANS_DATETIME,
Database.SCANS_FORMAT,
Database.SCANS_NAME,
Database.SCANS_CONTENT,
Database.SCANS_ERROR_CORRECTION_LEVEL,
Database.SCANS_VERSION_NUMBER,
Database.SCANS_VERSION,
Database.SCANS_SEQUENCE_SIZE,
Database.SCANS_SEQUENCE_INDEX,
Database.SCANS_SEQUENCE_ID,
@@ -6,16 +6,16 @@ import android.os.Parcel
import android.os.Parcelable
import android.text.format.DateFormat
import de.markusfisch.android.zxingcpp.ZxingCpp
import de.markusfisch.android.zxingcpp.ZxingCpp.BarcodeFormat
import de.markusfisch.android.zxingcpp.ZxingCpp.ContentType
import de.markusfisch.android.zxingcpp.ZxingCpp.Format
import de.markusfisch.android.zxingcpp.ZxingCpp.Result
data class Scan(
val content: String,
val raw: ByteArray?,
val format: String,
val format: BarcodeFormat,
val errorCorrectionLevel: String? = null,
val versionNumber: Int = 0,
val version: String? = null,
val sequenceSize: Int = -1,
val sequenceIndex: Int = -1,
val sequenceId: String = "",
@@ -24,7 +24,8 @@ data class Scan(
val price: String? = null,
val issueNumber: String? = null,
val dateTime: String = getDateTime(),
var id: Long = 0L
var id: Long = 0L,
val label: String? = null
) : Parcelable {
// Needs to be overwritten manually, as ByteArray is an array and
// this isn't handled well by Kotlin.
@@ -45,7 +46,7 @@ data class Scan(
(raw != null && other.raw != null && raw.contentEquals(other.raw))) &&
format == other.format &&
errorCorrectionLevel == other.errorCorrectionLevel &&
versionNumber == other.versionNumber &&
version == other.version &&
sequenceSize == other.sequenceSize &&
sequenceIndex == other.sequenceIndex &&
sequenceId == other.sequenceId &&
@@ -64,7 +65,7 @@ data class Scan(
result = 31 * result + (raw?.contentHashCode() ?: 0)
result = 31 * result + format.hashCode()
result = 31 * result + (errorCorrectionLevel?.hashCode() ?: 0)
result = 31 * result + versionNumber
result = 31 * result + version.hashCode()
result = 31 * result + sequenceSize
result = 31 * result + sequenceIndex
result = 31 * result + sequenceId.hashCode()
@@ -78,9 +79,9 @@ data class Scan(
private constructor(parcel: Parcel) : this(
content = parcel.readString() ?: "",
raw = parcel.readSizedByteArray(),
format = parcel.readString() ?: "",
format = BarcodeFormat.valueOf(parcel.readString() ?: ""),
errorCorrectionLevel = parcel.readString(),
versionNumber = parcel.readInt(),
version = parcel.readString(),
sequenceSize = parcel.readInt(),
sequenceIndex = parcel.readInt(),
sequenceId = parcel.readString() ?: "",
@@ -89,16 +90,17 @@ data class Scan(
price = parcel.readString(),
issueNumber = parcel.readString(),
dateTime = parcel.readString() ?: "",
id = parcel.readLong()
id = parcel.readLong(),
label = parcel.readString()
)
override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.apply {
writeString(content)
writeSizedByteArray(raw)
writeString(format)
writeString(format.name)
writeString(errorCorrectionLevel)
writeInt(versionNumber)
writeString(version)
writeInt(sequenceSize)
writeInt(sequenceIndex)
writeString(sequenceId)
@@ -108,6 +110,7 @@ data class Scan(
writeString(issueNumber)
writeString(dateTime)
writeLong(id)
writeString(label)
}
}
@@ -123,26 +126,18 @@ data class Scan(
}
fun Result.toScan(): Scan {
val content: String
val raw: ByteArray?
when (contentType) {
ContentType.GS1,
ContentType.ISO15434,
ContentType.TEXT -> {
content = text
raw = null
}
else -> {
content = ""
raw = rawBytes
}
}
return Scan(
content,
raw,
when (contentType) {
ContentType.GS1,
ContentType.ISO15434,
ContentType.TEXT -> text
else -> ""
},
rawBytes,
format,
ecLevel,
versionNumber,
version,
sequenceSize,
sequenceIndex,
sequenceId,
@@ -154,12 +149,12 @@ fun Result.toScan(): Scan {
}
data class Recreation(
val format: Format,
val format: BarcodeFormat,
val ecLevel: Int,
val size: Int,
val margin: Int
) {
fun encode(content: String): Bitmap? = try {
fun <T> encode(content: T): Bitmap? = try {
ZxingCpp.encodeAsBitmap(
content, format, size, size, margin, ecLevel
)
@@ -171,22 +166,18 @@ data class Recreation(
fun Scan.toRecreation(
size: Int = 128,
margin: Int = -1
) = if (content.isEmpty()) {
null
} else {
Recreation(
Format.valueOf(format),
when (errorCorrectionLevel) {
"L" -> 0
"M" -> 1
"Q" -> 2
"H" -> 3
else -> -1
},
size,
margin
)
}
) = Recreation(
format,
when (errorCorrectionLevel) {
"L" -> 0
"M" -> 4
"Q" -> 6
"H" -> 8
else -> -1
},
size,
margin
)
private fun getDateTime(
time: Long = System.currentTimeMillis()
@@ -10,10 +10,12 @@ import android.support.v4.app.Fragment
import android.view.*
import android.widget.EditText
import de.markusfisch.android.binaryeye.R
import de.markusfisch.android.binaryeye.app.db
import de.markusfisch.android.binaryeye.app.hasWritePermission
import de.markusfisch.android.binaryeye.content.copyToClipboard
import de.markusfisch.android.binaryeye.content.shareFile
import de.markusfisch.android.binaryeye.content.shareText
import de.markusfisch.android.binaryeye.database.toScan
import de.markusfisch.android.binaryeye.graphics.COLOR_BLACK
import de.markusfisch.android.binaryeye.graphics.COLOR_WHITE
import de.markusfisch.android.binaryeye.io.addSuffixIfNotGiven
@@ -24,13 +26,14 @@ import de.markusfisch.android.binaryeye.view.setPaddingFromWindowInsets
import de.markusfisch.android.binaryeye.widget.ConfinedScalingImageView
import de.markusfisch.android.binaryeye.widget.toast
import de.markusfisch.android.zxingcpp.ZxingCpp
import de.markusfisch.android.zxingcpp.ZxingCpp.Format
import de.markusfisch.android.zxingcpp.ZxingCpp.BarcodeFormat
import kotlinx.coroutines.*
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import java.io.OutputStream
import java.util.*
import kotlin.math.min
class BarcodeFragment : Fragment() {
private enum class FileType {
@@ -40,7 +43,8 @@ class BarcodeFragment : Fragment() {
private val parentJob = Job()
private val scope = CoroutineScope(Dispatchers.IO + parentJob)
private lateinit var barcode: Barcode
private lateinit var barcode: Barcode<*>
private lateinit var addToHistoryItem: MenuItem
override fun onCreate(state: Bundle?) {
super.onCreate(state)
@@ -70,7 +74,7 @@ class BarcodeFragment : Fragment() {
bitmap = barcode.bitmap()
} catch (e: Exception) {
var message = e.message
if (message == null || message.isEmpty()) {
if (message.isNullOrEmpty()) {
message = getString(R.string.error_encoding_barcode)
}
message?.let {
@@ -86,7 +90,10 @@ class BarcodeFragment : Fragment() {
imageView.setImageBitmap(bitmap)
imageView.post {
// Make sure to invoke this after ScalingImageView.onLayout().
imageView.minWidth /= 2f
imageView.minWidth = min(
imageView.minWidth / 2f,
barcode.size.toFloat()
)
}
view.findViewById<View>(R.id.share).setOnClickListener {
@@ -106,17 +113,18 @@ class BarcodeFragment : Fragment() {
}
private fun Bundle.toBarcode() = Barcode(
getString(CONTENT) ?: throw IllegalArgumentException(
getString(CONTENT_TEXT) ?: getByteArray(CONTENT_RAW) ?: throw IllegalArgumentException(
"content cannot be null"
),
Format.valueOf(
BarcodeFormat.valueOf(
getString(FORMAT) ?: throw IllegalArgumentException(
"format cannot be null"
)
),
getInt(SIZE),
getInt(MARGIN),
getInt(EC_LEVEL),
Colors.values()[getInt(COLORS)]
Colors.entries[getInt(COLORS)]
)
override fun onDestroyView() {
@@ -126,10 +134,18 @@ class BarcodeFragment : Fragment() {
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.fragment_barcode, menu)
addToHistoryItem = menu.findItem(R.id.add_to_history)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.add_to_history -> {
readAndAddToHistory(barcode.bitmap(), barcode.format)
addToHistoryItem.isVisible = false
context.toast(R.string.added_to_history)
true
}
R.id.copy_to_clipboard -> {
context.apply {
copyToClipboard(barcode.text())
@@ -137,12 +153,14 @@ class BarcodeFragment : Fragment() {
}
true
}
R.id.export_to_file -> {
context.pickFileType(R.string.export_as) {
askForFileNameAndSave(it)
}
true
}
else -> super.onOptionsItemSelected(item)
}
}
@@ -151,7 +169,7 @@ class BarcodeFragment : Fragment() {
title: Int,
action: (FileType) -> Unit
) {
val fileTypes = FileType.values()
val fileTypes = FileType.entries.toTypedArray()
AlertDialog.Builder(this)
.setTitle(title)
.setItems(fileTypes.map { it.name }.toTypedArray()) { _, which ->
@@ -186,12 +204,14 @@ class BarcodeFragment : Fragment() {
) {
barcode.bitmap().saveAsPng(it)
}
FileType.SVG -> saveAs(
addSuffixIfNotGiven(fileName, ".svg"),
MIME_SVG
) { outputStream ->
outputStream.write(barcode.svg().toByteArray())
}
FileType.TXT -> saveAs(
addSuffixIfNotGiven(fileName, ".txt"),
MIME_TXT
@@ -252,26 +272,44 @@ class BarcodeFragment : Fragment() {
}
companion object {
private const val CONTENT = "content"
private const val CONTENT_TEXT = "content_text"
private const val CONTENT_RAW = "content_raw"
private const val FORMAT = "format"
private const val SIZE = "size"
private const val MARGIN = "margin"
private const val EC_LEVEL = "ec_level"
private const val COLORS = "colors"
private const val MIME_PNG = "image/png"
private const val MIME_SVG = "image/svg+xmg"
private const val MIME_TXT = "text/plain"
fun newInstance(
content: String,
format: Format,
fun <T> newInstance(
content: T,
format: BarcodeFormat,
size: Int,
margin: Int,
ecLevel: Int = -1,
colors: Int = 0
): Fragment {
val args = Bundle()
args.putString(CONTENT, content)
when (content) {
is String -> {
args.putString(CONTENT_TEXT, content)
}
is ByteArray -> {
args.putByteArray(CONTENT_RAW, content)
}
else -> {
throw IllegalArgumentException(
"content must be a String of a ByteArray"
)
}
}
args.putString(FORMAT, format.name)
args.putInt(SIZE, size)
args.putInt(MARGIN, margin)
args.putInt(EC_LEVEL, ecLevel)
args.putInt(COLORS, colors)
val fragment = BarcodeFragment()
@@ -281,17 +319,18 @@ class BarcodeFragment : Fragment() {
}
}
private data class Barcode(
val content: String,
val format: Format,
private data class Barcode<T>(
val content: T,
val format: BarcodeFormat,
val size: Int,
val margin: Int,
val ecLevel: Int,
val colors: Colors
) {
private var _bitmap: Bitmap? = null
fun bitmap(): Bitmap {
val b = _bitmap ?: ZxingCpp.encodeAsBitmap(
content, format, size, size, -1, ecLevel,
content, format, size, size, margin, ecLevel,
setColor = colors.foregroundColor(),
unsetColor = colors.backgroundColor()
)
@@ -302,7 +341,7 @@ private data class Barcode(
private var _svg: String? = null
fun svg(): String {
val s = _svg ?: ZxingCpp.encodeAsSvg(
content, format, -1, ecLevel
content, format, margin, ecLevel
)
_svg = s
return s
@@ -311,7 +350,7 @@ private data class Barcode(
private var _text: String? = null
fun text(): String {
val t = _text ?: ZxingCpp.encodeAsText(
content, format, -1, ecLevel,
content, format, margin, ecLevel,
inverted = colors == Colors.BLACK_ON_WHITE
)
_text = t
@@ -328,6 +367,7 @@ private enum class Colors {
fun foregroundColor(): Int = when (this) {
BLACK_ON_WHITE,
BLACK_ON_TRANSPARENT -> COLOR_BLACK
WHITE_ON_BLACK,
WHITE_ON_TRANSPARENT -> COLOR_WHITE
}
@@ -344,5 +384,31 @@ private fun Bitmap.saveAsPng(outputStream: OutputStream, quality: Int = 90) =
compress(Bitmap.CompressFormat.PNG, quality, outputStream)
private val fileNameCharacters = "[^A-Za-z0-9]".toRegex()
private fun encodeFileName(name: String): String =
fileNameCharacters.replace(name, "_").take(16).trim('_').lowercase(Locale.getDefault())
private fun encodeFileName(name: String): String = fileNameCharacters
.replace(name, "_")
.take(16)
.trim('_')
.lowercase(Locale.getDefault())
private fun readAndAddToHistory(
bitmap: Bitmap,
format: BarcodeFormat
) {
ZxingCpp.readBitmap(
bitmap,
0, 0,
bitmap.width, bitmap.height,
0,
ZxingCpp.ReaderOptions(
tryHarder = true,
tryRotate = true,
tryInvert = true,
tryDownscale = true,
maxNumberOfSymbols = 1,
formats = setOf(format)
)
)?.first {
db.insertScan(it.toScan())
true
}
}
@@ -8,8 +8,17 @@ import android.text.Editable
import android.text.Html
import android.text.TextWatcher
import android.text.method.LinkMovementMethod
import android.view.*
import android.widget.*
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.widget.EditText
import android.widget.ImageView
import android.widget.TableLayout
import android.widget.TableRow
import android.widget.TextView
import de.markusfisch.android.binaryeye.R
import de.markusfisch.android.binaryeye.actions.ActionRegistry
import de.markusfisch.android.binaryeye.actions.IAction
@@ -17,10 +26,16 @@ import de.markusfisch.android.binaryeye.actions.wifi.WifiAction
import de.markusfisch.android.binaryeye.actions.wifi.WifiConnector
import de.markusfisch.android.binaryeye.activity.MainActivity
import de.markusfisch.android.binaryeye.adapter.prettifyFormatName
import de.markusfisch.android.binaryeye.app.*
import de.markusfisch.android.binaryeye.app.addFragment
import de.markusfisch.android.binaryeye.app.db
import de.markusfisch.android.binaryeye.app.hasLocationPermission
import de.markusfisch.android.binaryeye.app.hasWritePermission
import de.markusfisch.android.binaryeye.app.prefs
import de.markusfisch.android.binaryeye.content.copyToClipboard
import de.markusfisch.android.binaryeye.content.shareAsFile
import de.markusfisch.android.binaryeye.content.shareText
import de.markusfisch.android.binaryeye.content.toHexString
import de.markusfisch.android.binaryeye.content.wipeLastShareFile
import de.markusfisch.android.binaryeye.database.Recreation
import de.markusfisch.android.binaryeye.database.Scan
import de.markusfisch.android.binaryeye.database.toRecreation
@@ -29,7 +44,11 @@ import de.markusfisch.android.binaryeye.io.toSaveResult
import de.markusfisch.android.binaryeye.io.writeExternalFile
import de.markusfisch.android.binaryeye.view.setPaddingFromWindowInsets
import de.markusfisch.android.binaryeye.widget.toast
import kotlinx.coroutines.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlin.math.roundToInt
class DecodeFragment : Fragment() {
@@ -40,6 +59,7 @@ class DecodeFragment : Fragment() {
private lateinit var hexView: TextView
private lateinit var stampView: TextView
private lateinit var recreationView: ImageView
private lateinit var labelView: EditText
private lateinit var fab: FloatingActionButton
private lateinit var format: String
@@ -56,6 +76,7 @@ class DecodeFragment : Fragment() {
private var originalBytes: ByteArray = ByteArray(0)
private var id = 0L
private var recreation: Recreation? = null
private var label: String? = null
override fun onCreate(state: Bundle?) {
super.onCreate(state)
@@ -86,9 +107,9 @@ class DecodeFragment : Fragment() {
} ?: throw IllegalArgumentException("DecodeFragment needs a Scan")
val originalContent = scan.content
isBinary = scan.raw != null
isBinary = scan.content.isEmpty()
originalBytes = scan.raw ?: originalContent.toByteArray()
format = scan.format
format = scan.format.name
id = scan.id
contentView = view.findViewById(R.id.content)
@@ -98,6 +119,7 @@ class DecodeFragment : Fragment() {
hexView = view.findViewById(R.id.hex)
stampView = view.findViewById(R.id.stamp)
recreationView = view.findViewById(R.id.recreation)
labelView = view.findViewById(R.id.label)
fab = view.findViewById(R.id.open)
initContentAndFab(originalContent)
@@ -112,6 +134,14 @@ class DecodeFragment : Fragment() {
(200f * dp).roundToInt()
)
}
if (id > 0) {
scan.label?.let {
labelView.setText(it)
label = it
}
} else {
labelView.visibility = View.GONE
}
updateViewsAndFab(originalContent, originalBytes)
@@ -121,9 +151,21 @@ class DecodeFragment : Fragment() {
return view
}
override fun onPause() {
super.onPause()
if (id > 0) {
val newLabel = labelView.text.toString().trim()
if (newLabel != (label ?: "")) {
db.renameScan(id, newLabel)
label = newLabel
}
}
}
override fun onDestroy() {
super.onDestroy()
parentJob.cancel()
wipeLastShareFile()
}
private fun initContentAndFab(originalContent: String) {
@@ -204,13 +246,15 @@ class DecodeFragment : Fragment() {
}
recreationView.showIf(prefs.showRecreation) { v ->
val r = recreation ?: return@showIf
v.setImageBitmap(r.encode(text))
val content = if (isBinary) bytes else text
v.setImageBitmap(r.encode(content))
v.setOnClickListener {
fragmentManager?.addFragment(
BarcodeFragment.newInstance(
text,
content,
r.format,
r.size,
r.margin,
r.ecLevel
)
)
@@ -253,15 +297,15 @@ class DecodeFragment : Fragment() {
R.string.gtin_price to scan.price,
R.string.gtin_issue_number to scan.issueNumber,
)
if (scan.versionNumber > 0) {
val versionString = if (scan.format == "QR_CODE") {
if (!scan.version.isNullOrBlank()) {
val versionString = if (scan.format.name == "QR_CODE") {
getString(
R.string.qr_version_and_modules,
scan.versionNumber,
scan.versionNumber * 4 + 17
scan.version,
(scan.version.toIntOrNull() ?: 0) * 4 + 17
)
} else {
scan.versionNumber.toString()
scan.version
}
items.putAll(
linkedMapOf(
@@ -299,10 +343,6 @@ class DecodeFragment : Fragment() {
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.fragment_decode, menu)
if (isBinary) {
menu.findItem(R.id.copy_to_clipboard).isVisible = false
menu.findItem(R.id.create).isVisible = false
}
if (id > 0L) {
menu.findItem(R.id.remove).isVisible = true
}
@@ -318,11 +358,13 @@ class DecodeFragment : Fragment() {
maybeBackOrFinish()
true
}
R.id.copy_to_clipboard -> {
copyToClipboard(textOrHex())
maybeBackOrFinish()
true
}
R.id.share -> {
context?.apply {
shareText(textOrHex())
@@ -330,17 +372,36 @@ class DecodeFragment : Fragment() {
}
true
}
R.id.share_file -> {
context?.apply {
val (data, name) = if (isBinary) {
Pair(originalBytes, "barcode_content.bin")
} else {
Pair(content, "barcode_content.txt")
}
shareAsFile(data, name)
maybeBackOrFinish()
}
true
}
R.id.create -> {
fragmentManager?.addFragment(
EncodeFragment.newInstance(content, format)
EncodeFragment.newInstance(
if (isBinary) originalBytes else content,
format
)
)
true
}
R.id.remove -> {
db.removeScan(id)
backOrFinish()
true
}
else -> super.onOptionsItemSelected(item)
}
}
@@ -1,20 +1,36 @@
package de.markusfisch.android.binaryeye.fragment
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.support.v4.app.Fragment
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.widget.*
import android.widget.AdapterView
import android.widget.ArrayAdapter
import android.widget.CheckBox
import android.widget.EditText
import android.widget.SeekBar
import android.widget.Spinner
import android.widget.TextView
import de.markusfisch.android.binaryeye.R
import de.markusfisch.android.binaryeye.adapter.prettifyFormatName
import de.markusfisch.android.binaryeye.app.addFragment
import de.markusfisch.android.binaryeye.app.prefs
import de.markusfisch.android.binaryeye.text.unescape
import de.markusfisch.android.binaryeye.view.hideSoftKeyboard
import de.markusfisch.android.binaryeye.view.setPaddingFromWindowInsets
import de.markusfisch.android.binaryeye.widget.toast
import de.markusfisch.android.zxingcpp.ZxingCpp.Format
import de.markusfisch.android.zxingcpp.ZxingCpp.BarcodeFormat
import java.io.InputStream
import kotlin.math.max
import kotlin.math.min
class EncodeFragment : Fragment() {
private lateinit var formatView: Spinner
@@ -24,22 +40,57 @@ class EncodeFragment : Fragment() {
private lateinit var colorsSpinner: Spinner
private lateinit var sizeView: TextView
private lateinit var sizeBarView: SeekBar
private lateinit var marginLayout: View
private lateinit var marginView: TextView
private lateinit var marginBarView: SeekBar
private lateinit var contentView: EditText
private lateinit var unescapeCheckBox: CheckBox
private val writers = arrayListOf(
Format.AZTEC,
Format.CODABAR,
Format.CODE_39,
Format.CODE_128,
Format.DATA_MATRIX,
Format.EAN_8,
Format.EAN_13,
Format.ITF,
Format.PDF_417,
Format.QR_CODE,
Format.UPC_A
private val formats = arrayListOf(
BarcodeFormat.AZTEC,
BarcodeFormat.CODABAR,
BarcodeFormat.CODE_39,
BarcodeFormat.CODE_128,
BarcodeFormat.DATA_MATRIX,
BarcodeFormat.EAN_8,
BarcodeFormat.EAN_13,
BarcodeFormat.ITF,
BarcodeFormat.PDF_417,
BarcodeFormat.QR_CODE,
BarcodeFormat.UPC_A
)
private var minMargin = 0
private var bytes: ByteArray? = null
override fun onActivityResult(
requestCode: Int,
resultCode: Int,
resultData: Intent?
) {
when (requestCode) {
PICK_FILE_RESULT_CODE -> {
if (resultCode == Activity.RESULT_OK &&
resultData != null &&
resultData.data != null
) {
val ac = activity ?: return
ac.hideSoftKeyboard(contentView)
val uri = resultData.data ?: return
bytes = ac.contentResolver?.openInputStream(uri)?.use {
it.readBytesMax(4096)
} ?: return
setEncodeByteArray()
}
}
}
}
override fun onCreate(state: Bundle?) {
super.onCreate(state)
setHasOptionsMenu(true)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
@@ -58,7 +109,7 @@ class EncodeFragment : Fragment() {
val formatAdapter = ArrayAdapter(
ac,
android.R.layout.simple_spinner_item,
writers.map { prettifyFormatName(it.name) }
formats.map { prettifyFormatName(it.name) }
)
formatAdapter.setDropDownViewResource(
android.R.layout.simple_spinner_dropdown_item
@@ -71,16 +122,35 @@ class EncodeFragment : Fragment() {
position: Int,
id: Long
) {
val ecVisibility = if (
writers[position] == Format.QR_CODE
) View.VISIBLE else View.GONE
ecLabel.visibility = ecVisibility
ecSpinner.visibility = ecVisibility
val colorsVisibility = if (
writers[position].canBeInverted()
) View.VISIBLE else View.GONE
colorsLabel.visibility = colorsVisibility
colorsSpinner.visibility = colorsVisibility
val format = formats[position]
val arrayId = when (format) {
BarcodeFormat.AZTEC -> R.array.aztec_error_correction_levels
BarcodeFormat.QR_CODE -> R.array.qr_error_correction_levels
BarcodeFormat.PDF_417 -> R.array.pdf417_error_correction_levels
else -> 0
}
if (arrayId > 0) {
ecSpinner.setEntries(arrayId)
val idx = format.unpackEcLevel(
prefs.indexOfLastSelectedEcLevel
)
if (idx < ecSpinner.adapter.count) {
ecSpinner.setSelection(idx)
}
true
} else {
false
}.setVisibilityFor(ecLabel, ecSpinner)
format.canBeInverted().setVisibilityFor(
colorsLabel, colorsSpinner
)
when (format) {
BarcodeFormat.AZTEC -> setMarginBar(4, 4)
BarcodeFormat.DATA_MATRIX -> setMarginBar(1, 1)
BarcodeFormat.QR_CODE -> setMarginBar(4, 4)
BarcodeFormat.PDF_417 -> setMarginBar(0, 30)
else -> false
}.setVisibilityFor(marginLayout)
}
override fun onNothingSelected(parentView: AdapterView<*>?) {}
@@ -88,6 +158,22 @@ class EncodeFragment : Fragment() {
ecLabel = view.findViewById(R.id.error_correction_label)
ecSpinner = view.findViewById(R.id.error_correction_level)
ecSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(
parentView: AdapterView<*>?,
selectedItemView: View?,
position: Int,
id: Long
) {
val format = formats[formatView.selectedItemPosition]
prefs.indexOfLastSelectedEcLevel = format.packEcLevel(
prefs.indexOfLastSelectedEcLevel,
position
)
}
override fun onNothingSelected(parentView: AdapterView<*>?) {}
}
colorsLabel = view.findViewById(R.id.colors_label)
colorsSpinner = view.findViewById(R.id.colors)
@@ -96,17 +182,28 @@ class EncodeFragment : Fragment() {
sizeBarView = view.findViewById(R.id.size_bar)
initSizeBar()
marginLayout = view.findViewById(R.id.margin)
marginView = view.findViewById(R.id.margin_display)
marginBarView = view.findViewById(R.id.margin_bar)
initMarginBar()
contentView = view.findViewById(R.id.content)
unescapeCheckBox = view.findViewById(R.id.unescape)
unescapeCheckBox.isChecked = prefs.expandEscapeSequences
val args = arguments
args?.getString(CONTENT)?.let {
args?.getString(CONTENT_TEXT)?.let {
contentView.setText(it)
}
args?.getByteArray(CONTENT_RAW)?.let {
bytes = it
setEncodeByteArray()
}
val barcodeFormat = args?.getString(FORMAT)
if (barcodeFormat != null) {
formatView.setSelection(
writers.indexOf(barcodeFormat.toFormat())
formats.indexOf(barcodeFormat.toFormat())
)
} else if (state == null) {
formatView.post {
@@ -124,26 +221,72 @@ class EncodeFragment : Fragment() {
return view
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.fragment_encode, menu)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.pick_file -> {
startActivityForResult(
Intent.createChooser(
Intent(Intent.ACTION_GET_CONTENT).apply {
type = "*/*"
},
getString(R.string.pick_file)
),
PICK_FILE_RESULT_CODE
)
true
}
else -> super.onOptionsItemSelected(item)
}
}
override fun onPause() {
super.onPause()
prefs.indexOfLastSelectedFormat = formatView.selectedItemPosition
prefs.expandEscapeSequences = unescapeCheckBox.isChecked
prefs.lastMargin = marginBarView.progress
}
private fun Context.encode() {
val content = contentView.text.toString()
if (content.isEmpty()) {
toast(R.string.error_no_content)
return
}
hideSoftKeyboard(contentView)
val writer = writers[formatView.selectedItemPosition]
val content = if (bytes != null) {
bytes
} else {
var text = contentView.text.toString()
if (unescapeCheckBox.isChecked) {
try {
text = text.unescape()
} catch (e: IllegalArgumentException) {
toast(e.message ?: "Invalid escape sequence")
return
}
}
if (text.isEmpty()) {
toast(R.string.error_no_content)
return
}
text
}
val format = formats[formatView.selectedItemPosition]
fragmentManager?.addFragment(
BarcodeFragment.newInstance(
content,
writer,
format,
getSize(sizeBarView.progress),
ecSpinner.selectedItemPosition,
if (writer.canBeInverted()) {
when (format) {
BarcodeFormat.AZTEC,
BarcodeFormat.DATA_MATRIX,
BarcodeFormat.QR_CODE,
BarcodeFormat.PDF_417 -> marginBarView.progress
else -> -1
},
format.getErrorCorrectionLevel(ecSpinner.selectedItemPosition),
if (format.canBeInverted()) {
colorsSpinner.selectedItemPosition
} else 0
)
@@ -170,21 +313,77 @@ class EncodeFragment : Fragment() {
private fun updateSize(power: Int) {
val size = getSize(power)
sizeView.text = getString(R.string.width_by_height, size, size)
sizeView.text = getString(R.string.size_width_by_height, size, size)
}
private fun getSize(power: Int) = 128 * (power + 1)
private fun initMarginBar() {
updateMargin(marginBarView.progress)
marginBarView.setOnSeekBarChangeListener(
object : SeekBar.OnSeekBarChangeListener {
override fun onProgressChanged(
seekBar: SeekBar,
progressValue: Int,
fromUser: Boolean
) {
if (progressValue >= minMargin) {
updateMargin(progressValue)
}
}
override fun onStartTrackingTouch(seekBar: SeekBar) {}
override fun onStopTrackingTouch(seekBar: SeekBar) {}
})
}
private fun updateMargin(margin: Int) {
marginView.text = getString(R.string.margin_size, margin)
}
private fun setMarginBar(minimum: Int, value: Int): Boolean {
minMargin = minimum
marginBarView.apply {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
min = minimum
}
progress = max(
minimum, if (prefs.lastMargin > -1) {
prefs.lastMargin
} else {
value
}
)
}
return true
}
private fun setEncodeByteArray() {
contentView.text = null
contentView.hint = getString(R.string.binary_data)
contentView.isEnabled = false
unescapeCheckBox.isEnabled = false
}
companion object {
private const val CONTENT = "content"
private const val CONTENT_TEXT = "content_text"
private const val CONTENT_RAW = "content_raw"
private const val FORMAT = "format"
private const val PICK_FILE_RESULT_CODE = 1
fun newInstance(
content: String? = null,
fun <T> newInstance(
content: T? = null,
format: String? = null
): Fragment {
val args = Bundle()
content?.let { args.putString(CONTENT, content) }
content?.let {
when (content) {
is String -> args.putString(CONTENT_TEXT, content)
is ByteArray -> args.putByteArray(CONTENT_RAW, content)
else -> throw IllegalArgumentException(
"content must be a String or a ByteArray"
)
}
}
format?.let { args.putString(FORMAT, it) }
val fragment = EncodeFragment()
fragment.arguments = args
@@ -193,15 +392,73 @@ class EncodeFragment : Fragment() {
}
}
private fun Format.canBeInverted() = when (this) {
Format.AZTEC,
Format.DATA_MATRIX,
Format.QR_CODE -> true
private fun Boolean.setVisibilityFor(vararg views: View) {
val visibility = if (this) View.VISIBLE else View.GONE
for (view in views) {
view.visibility = visibility
}
}
private fun BarcodeFormat.packEcLevel(packed: Int, level: Int): Int {
val s = ecLevelShift()
return (level shl s) or (packed and (15 shl s).inv())
}
private fun BarcodeFormat.unpackEcLevel(packed: Int) =
(packed shr ecLevelShift()) and 15
private fun BarcodeFormat.ecLevelShift() = when (this) {
BarcodeFormat.AZTEC -> 0
BarcodeFormat.QR_CODE -> 4
BarcodeFormat.PDF_417 -> 8
else -> throw IllegalArgumentException("$this does not have error levels")
}
private fun BarcodeFormat.canBeInverted() = when (this) {
BarcodeFormat.AZTEC,
BarcodeFormat.DATA_MATRIX,
BarcodeFormat.QR_CODE -> true
else -> false
}
private fun String.toFormat(default: Format = Format.QR_CODE): Format = try {
Format.valueOf(this)
private fun String.toFormat(default: BarcodeFormat = BarcodeFormat.QR_CODE): BarcodeFormat = try {
BarcodeFormat.valueOf(this)
} catch (_: IllegalArgumentException) {
default
}
private fun BarcodeFormat.getErrorCorrectionLevel(position: Int) = when (this) {
BarcodeFormat.AZTEC -> position
BarcodeFormat.QR_CODE -> (position + 1) * 2
BarcodeFormat.PDF_417 -> position
else -> 0
}.coerceIn(0, 8)
private fun Spinner.setEntries(resId: Int) = ArrayAdapter.createFromResource(
this.context,
resId,
android.R.layout.simple_spinner_item
).also { aa ->
aa.setDropDownViewResource(
android.R.layout.simple_spinner_dropdown_item
)
adapter = aa
}
private fun getSize(power: Int) = 128 * (power + 1)
private fun InputStream.readBytesMax(max: Int): ByteArray {
var offset = 0
var remaining = min(available(), max)
val result = ByteArray(remaining)
while (remaining > 0) {
val read = read(result, offset, remaining)
if (read < 0) {
break
}
remaining -= read
offset += read
}
return result
}
@@ -22,7 +22,9 @@ import de.markusfisch.android.binaryeye.R
import de.markusfisch.android.binaryeye.adapter.ScansAdapter
import de.markusfisch.android.binaryeye.app.*
import de.markusfisch.android.binaryeye.content.copyToClipboard
import de.markusfisch.android.binaryeye.content.shareAsFile
import de.markusfisch.android.binaryeye.content.shareText
import de.markusfisch.android.binaryeye.content.wipeLastShareFile
import de.markusfisch.android.binaryeye.database.*
import de.markusfisch.android.binaryeye.io.askForFileName
import de.markusfisch.android.binaryeye.io.toSaveResult
@@ -79,7 +81,8 @@ class HistoryFragment : Fragment() {
closeActionMode()
true
}
R.id.edit_scan -> {
R.id.edit_label -> {
scansAdapter?.forSelection { id, position ->
ac.askForName(
id,
@@ -90,6 +93,7 @@ class HistoryFragment : Fragment() {
closeActionMode()
true
}
R.id.remove_scan -> {
scansAdapter?.getSelectedIds()?.let {
if (it.isNotEmpty()) {
@@ -99,6 +103,7 @@ class HistoryFragment : Fragment() {
closeActionMode()
true
}
else -> false
}
}
@@ -114,6 +119,7 @@ class HistoryFragment : Fragment() {
private var filter: String? = null
private var clearListMenuItem: MenuItem? = null
private var exportHistoryMenuItem: MenuItem? = null
private var shareAsFileMenuItem: MenuItem? = null
override fun onCreate(state: Bundle?) {
super.onCreate(state)
@@ -140,8 +146,12 @@ class HistoryFragment : Fragment() {
initHistorySwitch(useHistorySwitch)
listView = view.findViewById(R.id.scans)
listView.setOnItemClickListener { _, _, _, id ->
showScan(id)
listView.setOnItemClickListener { _, v, position, id ->
if (actionMode != null) {
scansAdapter?.select(v, id, position)
} else {
showScan(id)
}
}
listView.setOnItemLongClickListener { _, v, position, id ->
scansAdapter?.select(v, id, position)
@@ -156,7 +166,7 @@ class HistoryFragment : Fragment() {
fab = view.findViewById(R.id.share)
fab.setOnClickListener { v ->
v.context.pickListSeparatorAndShare()
v.context.pickListSeparatorAndShare(false)
}
progressView = view.findViewById(R.id.progress_view)
@@ -173,6 +183,7 @@ class HistoryFragment : Fragment() {
super.onDestroy()
scansAdapter?.changeCursor(null)
parentJob.cancel()
wipeLastShareFile()
}
override fun onPause() {
@@ -182,10 +193,19 @@ class HistoryFragment : Fragment() {
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.fragment_history, menu)
initSearchView(menu.findItem(R.id.search))
menu.setGroupVisible(R.id.scans_available, scansAdapter?.count != 0)
clearListMenuItem = menu.findItem(R.id.clear)
exportHistoryMenuItem = menu.findItem(R.id.export_history)
val searchMenuItem = menu.findItem(R.id.search)
initSearchView(searchMenuItem)
val visible = scansAdapter?.count != 0
searchMenuItem?.isVisible = visible
exportHistoryMenuItem = menu.findItem(R.id.export_history)?.apply {
isVisible = visible
}
shareAsFileMenuItem = menu.findItem(R.id.share_as_file)?.apply {
isVisible = visible
}
clearListMenuItem = menu.findItem(R.id.clear)?.apply {
isVisible = visible
}
}
private fun initSearchView(item: MenuItem?) {
@@ -221,10 +241,17 @@ class HistoryFragment : Fragment() {
context.askToRemoveScans()
true
}
R.id.export_history -> {
askToExportToFile()
true
}
R.id.share_as_file -> {
context.pickListSeparatorAndShare(true)
true
}
else -> super.onOptionsItemSelected(item)
}
}
@@ -278,6 +305,7 @@ class HistoryFragment : Fragment() {
private fun enableMenuItems(enabled: Boolean) {
clearListMenuItem?.isEnabled = enabled
exportHistoryMenuItem?.isEnabled = enabled
shareAsFileMenuItem?.isEnabled = enabled
}
private fun closeActionMode() {
@@ -407,32 +435,44 @@ class HistoryFragment : Fragment() {
}
}
private fun Context.pickListSeparatorAndShare() {
private fun Context.pickListSeparatorAndShare(asFile: Boolean) {
val separators = resources.getStringArray(
R.array.list_separators_values
)
AlertDialog.Builder(this)
.setTitle(R.string.pick_list_separator)
.setItems(R.array.list_separators_names) { _, which ->
shareScans(separators[which])
shareScans(separators[which], asFile)
}
.show()
}
private fun shareScans(format: String) = scope.launch {
private fun shareScans(format: String, asFile: Boolean) = scope.launch {
progressView.useVisibility {
var text: String? = null
db.getScansDetailed(filter)?.use { cursor ->
val selectedIds = scansAdapter?.getSelectedIds()
if (selectedIds?.isNotEmpty() == true) {
db.getScansDetailed(selectedIds.toLongArray())
} else {
db.getScansDetailed(filter)
}?.use { cursor ->
val details = format.split(":")
text = when (details[0]) {
"text" -> cursor.exportText(details[1])
"csv" -> cursor.exportCsv(details[1])
else -> cursor.exportJson()
when (details[0]) {
"text" -> Pair(cursor.exportText(details[1]), "txt")
"csv" -> Pair(cursor.exportCsv(details[1]), "csv")
else -> Pair(cursor.exportJson(), "json")
}
}
text?.let {
withContext(Dispatchers.Main) {
context.shareText(it)
}?.let { (text, ext) ->
text?.let {
withContext(Dispatchers.Main) {
if (asFile) {
context.shareAsFile(
it,
String.format("scans.%s", ext)
)
} else {
context.shareText(it)
}
}
}
}
}
@@ -0,0 +1,92 @@
package de.markusfisch.android.binaryeye.fragment
import android.content.Context
import android.net.wifi.WifiManager
import android.net.wifi.WifiNetworkSuggestion
import android.os.Build
import android.os.Bundle
import android.support.annotation.RequiresApi
import android.support.v4.app.Fragment
import android.view.*
import android.widget.ArrayAdapter
import android.widget.ListView
import de.markusfisch.android.binaryeye.R
import de.markusfisch.android.binaryeye.view.setPaddingFromWindowInsets
import de.markusfisch.android.binaryeye.view.systemBarListViewScrollListener
import de.markusfisch.android.binaryeye.widget.toast
class NetworkSuggestionsFragment : Fragment() {
@RequiresApi(Build.VERSION_CODES.R)
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
state: Bundle?
): View? {
val ac = activity ?: return null
ac.setTitle(R.string.network_suggestions)
val view = inflater.inflate(
R.layout.fragment_network_suggestions,
container,
false
)
val wm = ac.applicationContext.getSystemService(
Context.WIFI_SERVICE
) as WifiManager
val suggestionArrayAdapter = ArrayAdapter(
ac,
android.R.layout.simple_list_item_checked,
wm.networkSuggestions.map {
Suggestion(
it.ssid ?: it.toString(),
it
)
}
)
val listView = view.findViewById<ListView>(R.id.suggestions)
listView.emptyView = view.findViewById(R.id.no_suggestions)
listView.adapter = suggestionArrayAdapter
listView.setOnScrollListener(systemBarListViewScrollListener)
(view.findViewById(R.id.inset_layout) as View).setPaddingFromWindowInsets()
listView.setPaddingFromWindowInsets()
view.findViewById<View>(R.id.remove).setOnClickListener {
val removeList = ArrayList<WifiNetworkSuggestion>()
val removeFromAdapter = ArrayList<Suggestion>()
val checked = listView.checkedItemPositions
val size = checked.size()
for (i in 0 until size) {
if (checked.valueAt(i)) {
val pos = checked.keyAt(i)
listView.setItemChecked(pos, false)
val suggestion = listView.getItemAtPosition(
pos
) as Suggestion
removeList.add(suggestion.suggestion)
removeFromAdapter.add(suggestion)
}
}
if (removeList.isNotEmpty()) {
removeFromAdapter.forEach {
suggestionArrayAdapter.remove(it)
}
wm.removeNetworkSuggestions(removeList)
} else {
ac.toast(
R.string.clear_network_suggestions_nothing_to_remove
)
}
}
return view
}
}
private data class Suggestion(
val label: String,
val suggestion: WifiNetworkSuggestion
) {
override fun toString() = label
}
@@ -16,9 +16,12 @@ import android.support.v7.preference.Preference
import android.support.v7.preference.PreferenceFragmentCompat
import android.support.v7.preference.PreferenceGroup
import de.markusfisch.android.binaryeye.R
import de.markusfisch.android.binaryeye.actions.wifi.removeNetworkSuggestions
import de.markusfisch.android.binaryeye.activity.SplashActivity
import de.markusfisch.android.binaryeye.app.addFragment
import de.markusfisch.android.binaryeye.app.hasBluetoothPermission
import de.markusfisch.android.binaryeye.app.prefs
import de.markusfisch.android.binaryeye.bluetooth.setBluetoothHosts
import de.markusfisch.android.binaryeye.media.beepConfirm
import de.markusfisch.android.binaryeye.preference.UrlPreference
import de.markusfisch.android.binaryeye.view.setPaddingFromWindowInsets
import de.markusfisch.android.binaryeye.view.systemBarRecyclerViewScrollListener
@@ -27,33 +30,70 @@ import de.markusfisch.android.binaryeye.widget.toast
class PreferencesFragment : PreferenceFragmentCompat() {
private val changeListener = object : OnSharedPreferenceChangeListener {
override fun onSharedPreferenceChanged(
sharedPreferences: SharedPreferences,
key: String
sharedPreferences: SharedPreferences?,
key: String?
) {
key ?: return
val preference = findPreference(key) ?: return
prefs.update()
if (preference.key == "custom_locale") {
activity?.restartApp()
} else {
setSummary(preference)
when (preference.key) {
"custom_locale" -> activity?.restartApp()
"beep_tone_name" -> {
beepConfirm()
setSummary(preference)
}
"send_scan_bluetooth" -> {
if (prefs.sendScanBluetooth &&
activity?.hasBluetoothPermission() == false
) {
prefs.sendScanBluetooth = false
}
setSummary(preference)
}
else -> setSummary(preference)
}
}
}
override fun onCreatePreferences(state: Bundle?, rootKey: String?) {
addPreferencesFromResource(R.xml.preferences)
activity?.setTitle(R.string.preferences)
setBluetoothResources()
wireClearNetworkPreferences()
}
private fun setBluetoothResources() {
if (prefs.sendScanBluetooth &&
activity?.hasBluetoothPermission() == true
) {
setBluetoothHosts(
findPreference("send_scan_bluetooth_host") as ListPreference
)
}
}
private fun wireClearNetworkPreferences() {
val pref = findPreference("clear_network_suggestions") ?: return
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
pref.isVisible = false
} else {
pref.setOnPreferenceClickListener {
context.askToClearNetworkSuggestions()
true
findPreference("clear_network_suggestions").apply {
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.Q) {
// From R+ we can query past network suggestions and
// make them editable.
setOnPreferenceClickListener {
fragmentManager.addFragment(NetworkSuggestionsFragment())
true
}
} else if (Build.VERSION.SDK_INT == Build.VERSION_CODES.Q) {
// On Q, we can only clear *all* suggestions.
// Note that previous versions of this app allowed
// adding network suggestions on Q as well, so we
// need to keep this option.
setOnPreferenceClickListener {
context.askToClearNetworkSuggestions()
true
}
} else {
// There are no network suggestions below Q.
isVisible = false
}
}
}
@@ -72,8 +112,8 @@ class PreferencesFragment : PreferenceFragmentCompat() {
@RequiresApi(Build.VERSION_CODES.Q)
private fun Context.clearNetworkSuggestions() {
toast(
if (
removeNetworkSuggestions(this) == WifiManager.STATUS_NETWORK_SUGGESTIONS_SUCCESS
if (removeAllNetworkSuggestions() ==
WifiManager.STATUS_NETWORK_SUGGESTIONS_SUCCESS
) {
R.string.clear_network_suggestions_success
} else {
@@ -84,6 +124,7 @@ class PreferencesFragment : PreferenceFragmentCompat() {
override fun onResume() {
super.onResume()
activity?.setTitle(R.string.preferences)
listView.setPaddingFromWindowInsets()
listView.removeOnScrollListener(systemBarRecyclerViewScrollListener)
listView.addOnScrollListener(systemBarRecyclerViewScrollListener)
@@ -105,6 +146,12 @@ class PreferencesFragment : PreferenceFragmentCompat() {
setTargetFragment(this@PreferencesFragment, 0)
show(fm, null)
}
} else if (preference.key == "send_scan_bluetooth_host") {
val ac = activity ?: return
if (ac.hasBluetoothPermission()) {
setBluetoothHosts(preference as ListPreference)
}
super.onDisplayPreferenceDialog(preference)
} else {
super.onDisplayPreferenceDialog(preference)
}
@@ -122,9 +169,11 @@ class PreferencesFragment : PreferenceFragmentCompat() {
is UrlPreference -> {
preference.setSummary(preference.getUrl())
}
is ListPreference -> {
preference.setSummary(preference.entry)
}
is MultiSelectListPreference -> {
preference.setSummary(
preference.values.joinToString(", ") {
@@ -132,6 +181,7 @@ class PreferencesFragment : PreferenceFragmentCompat() {
}
)
}
is PreferenceGroup -> {
setSummaries(preference)
}
@@ -152,3 +202,9 @@ private fun Activity.restartApp() {
// Restart to begin with an unmodified Locale to follow system settings.
Runtime.getRuntime().exit(0)
}
@RequiresApi(Build.VERSION_CODES.Q)
private fun Context.removeAllNetworkSuggestions(): Int =
(applicationContext.getSystemService(
Context.WIFI_SERVICE
) as WifiManager).removeNetworkSuggestions(listOf())
@@ -9,6 +9,7 @@ import de.markusfisch.android.binaryeye.app.prefs
import de.markusfisch.android.binaryeye.database.Scan
import de.markusfisch.android.binaryeye.net.sendAsync
import de.markusfisch.android.binaryeye.preference.UrlPreference
import de.markusfisch.android.zxingcpp.ZxingCpp.BarcodeFormat
class UrlDialogFragment : PreferenceDialogFragmentCompat() {
private var urlView: TextView? = null
@@ -50,7 +51,7 @@ class UrlDialogFragment : PreferenceDialogFragmentCompat() {
Scan(
"test",
null,
"none"
BarcodeFormat.NONE
).sendAsync(url, prefs.sendScanType) { code, body ->
textView.text = when {
code != null -> "$code"
@@ -55,17 +55,17 @@ fun Bitmap.crop(
pivotX: Float,
pivotY: Float
) = try {
val erected = erect(rotation, pivotX, pivotY)
val w = erected.width
val h = erected.height
val b = rotate(rotation, pivotX, pivotY)
val w = b.width
val h = b.height
val x = max(0, (rect.left * w).roundToInt())
val y = max(0, (rect.top * h).roundToInt())
Bitmap.createBitmap(
erected,
b,
x,
y,
min(w - x, (rect.right * w).roundToInt() - x),
min(h - y, (rect.bottom * h).roundToInt() - y)
min(w, (rect.right * w).roundToInt()) - x,
min(h, (rect.bottom * h).roundToInt()) - y
)
} catch (e: OutOfMemoryError) {
null
@@ -73,13 +73,11 @@ fun Bitmap.crop(
null
}
private fun Bitmap.erect(
private fun Bitmap.rotate(
rotation: Float,
pivotX: Float,
pivotY: Float
): Bitmap = if (
rotation % 360f != 0f
) {
): Bitmap = if (rotation % 360f != 0f) {
Bitmap.createBitmap(
this,
0,
@@ -19,10 +19,10 @@ fun Rect.setFrameRoi(
) {
Matrix().apply {
// Map ROI from view coordinates to frame coordinates.
setScale(1f / viewRect.width(), 1f / viewRect.height())
setTranslate(-viewRect.left.toFloat(), -viewRect.top.toFloat())
postScale(1f / viewRect.width(), 1f / viewRect.height())
postRotate(-frameMetrics.orientation.toFloat(), .5f, .5f)
postScale(frameMetrics.width.toFloat(), frameMetrics.height.toFloat())
postTranslate(-viewRect.left.toFloat(), -viewRect.top.toFloat())
val frameRoiF = RectF()
val viewRoiF = RectF(
viewRoi.left.toFloat(),
@@ -54,6 +54,7 @@ fun Matrix.setFrameToView(
uprightWidth = frameMetrics.height
uprightHeight = frameMetrics.width
}
else -> {
uprightWidth = frameMetrics.width
uprightHeight = frameMetrics.height
@@ -61,7 +61,6 @@ private fun Context.openExternalOutputStream(
fileName: String,
mimeType: String
): OutputStream = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
@Suppress("DEPRECATION")
val file = File(
Environment.getExternalStoragePublicDirectory(
Environment.DIRECTORY_DOWNLOADS
@@ -0,0 +1,33 @@
package de.markusfisch.android.binaryeye.media
import android.media.AudioManager
import android.media.ToneGenerator
import de.markusfisch.android.binaryeye.app.prefs
private var confirmToneGenerator: ToneGenerator? = null
private var errorToneGenerator: ToneGenerator? = null
fun beepConfirm() {
val tg = confirmToneGenerator ?: ToneGenerator(
AudioManager.STREAM_NOTIFICATION,
ToneGenerator.MAX_VOLUME
)
confirmToneGenerator = tg
tg.startTone(prefs.beepTone())
}
fun beepError() {
val tg = errorToneGenerator ?: ToneGenerator(
AudioManager.STREAM_ALARM,
ToneGenerator.MAX_VOLUME
)
errorToneGenerator = tg
tg.startTone(ToneGenerator.TONE_SUP_ERROR, 1000)
}
fun releaseToneGenerators() {
confirmToneGenerator?.release()
confirmToneGenerator = null
errorToneGenerator?.release()
errorToneGenerator = null
}
@@ -33,6 +33,7 @@ private fun Scan.send(url: String, type: String): Response {
"1" -> request(
url + "?" + asUrlArguments()
)
"2" -> request(url) { con ->
con.requestMethod = "POST"
con.setRequestProperty("Content-Type", "application/x-www-form-urlencoded")
@@ -41,6 +42,7 @@ private fun Scan.send(url: String, type: String): Response {
close()
}
}
"3" -> request(url) { con ->
con.requestMethod = "POST"
con.setRequestProperty("Content-Type", "application/json")
@@ -49,6 +51,7 @@ private fun Scan.send(url: String, type: String): Response {
close()
}
}
else -> request(
url + content.urlEncode()
)
@@ -66,9 +69,9 @@ private fun Scan.asJson() = JSONObject().apply {
private fun Scan.getMap(): Map<String, String> = mapOf(
"content" to content,
"raw" to raw?.toHexString(),
"format" to format,
"format" to format.name,
"errorCorrectionLevel" to errorCorrectionLevel,
"versionNumber" to versionNumber.toString(),
"version" to version,
"sequenceSize" to sequenceSize.toString(),
"sequenceIndex" to sequenceIndex.toString(),
"sequenceId" to sequenceId,
@@ -4,9 +4,8 @@ import android.content.Context
import android.os.Build
import android.os.VibrationEffect
import android.os.Vibrator
import de.markusfisch.android.binaryeye.app.prefs
private const val DURATION_IN_MS = 100L
private const val DURATION_IN_MS = 250L
private val beforeO = Build.VERSION.SDK_INT < Build.VERSION_CODES.O
private val errorPatternTimings = longArrayOf(
@@ -38,9 +37,7 @@ fun Context.getVibrator(): Vibrator = getSystemService(
) as Vibrator
fun Vibrator.vibrate() {
if (!prefs.vibrate) {
return
} else if (beforeO) {
if (beforeO) {
@Suppress("DEPRECATION")
vibrate(DURATION_IN_MS)
} else {
@@ -54,9 +51,7 @@ fun Vibrator.vibrate() {
}
fun Vibrator.error() {
if (!prefs.vibrate) {
return
} else if (beforeO) {
if (beforeO) {
@Suppress("DEPRECATION")
vibrate(errorPatternTimings, -1)
} else {
@@ -1,33 +1,36 @@
package de.markusfisch.android.binaryeye.preference
import android.annotation.SuppressLint
import android.content.Context
import android.content.SharedPreferences
import android.media.ToneGenerator
import android.os.Build
import android.preference.PreferenceManager
import android.support.annotation.RequiresApi
import de.markusfisch.android.zxingcpp.ZxingCpp.Format
import de.markusfisch.android.zxingcpp.ZxingCpp.BarcodeFormat
class Preferences {
lateinit var preferences: SharedPreferences
var barcodeFormats = setOf(
Format.AZTEC.name,
Format.CODABAR.name,
Format.CODE_39.name,
Format.CODE_93.name,
Format.CODE_128.name,
Format.DATA_BAR.name,
Format.DATA_BAR_EXPANDED.name,
Format.DATA_MATRIX.name,
Format.EAN_8.name,
Format.EAN_13.name,
Format.ITF.name,
Format.MAXICODE.name,
Format.PDF_417.name,
Format.QR_CODE.name,
Format.MICRO_QR_CODE.name,
Format.UPC_A.name,
Format.UPC_E.name,
BarcodeFormat.AZTEC.name,
BarcodeFormat.CODABAR.name,
BarcodeFormat.CODE_39.name,
BarcodeFormat.CODE_93.name,
BarcodeFormat.CODE_128.name,
BarcodeFormat.DATA_BAR.name,
BarcodeFormat.DATA_BAR_EXPANDED.name,
BarcodeFormat.DATA_MATRIX.name,
BarcodeFormat.EAN_8.name,
BarcodeFormat.EAN_13.name,
BarcodeFormat.ITF.name,
BarcodeFormat.MAXICODE.name,
BarcodeFormat.PDF_417.name,
BarcodeFormat.QR_CODE.name,
BarcodeFormat.MICRO_QR_CODE.name,
BarcodeFormat.RMQR_CODE.name,
BarcodeFormat.UPC_A.name,
BarcodeFormat.UPC_E.name,
)
@RequiresApi(Build.VERSION_CODES.HONEYCOMB)
set(value) {
@@ -61,37 +64,27 @@ class Preferences {
}
var autoRotate = true
set(value) {
// Immediately save this setting before it shouldn't change
// on the fly while scanning.
commit(AUTO_ROTATE, value)
apply(AUTO_ROTATE, value)
field = value
}
var tryHarder = false
set(value) {
// Immediately save this setting because it's only ever read
// before the camera is opened.
commit(TRY_HARDER, value)
apply(TRY_HARDER, value)
field = value
}
var bulkMode = false
set(value) {
// Immediately save this setting because it's only ever read
// before the camera is opened.
commit(BULK_MODE, value)
apply(BULK_MODE, value)
field = value
}
var bulkModeDelay = "500"
set(value) {
// Immediately save this setting because it's only ever read
// before the camera is opened.
commit(BULK_MODE_DELAY, value)
apply(BULK_MODE_DELAY, value)
field = value
}
var showToastInBulkMode = true
set(value) {
// Immediately save this setting because it's only ever read
// before the camera is opened.
commit(SHOW_TOAST_IN_BULK_MODE, value)
apply(SHOW_TOAST_IN_BULK_MODE, value)
field = value
}
var vibrate = true
@@ -99,6 +92,16 @@ class Preferences {
apply(VIBRATE, value)
field = value
}
var beep = false
set(value) {
apply(BEEP, value)
field = value
}
var beepToneName = "tone_prop_beep"
set(value) {
apply(BEEP_TONE_NAME, value)
field = value
}
var useHistory = false
set(value) {
apply(USE_HISTORY, value)
@@ -149,7 +152,7 @@ class Preferences {
apply(OPEN_WITH_URL, value)
field = value
}
var sendScanActive = true
var sendScanActive = false
set(value) {
apply(SEND_SCAN_ACTIVE, value)
field = value
@@ -164,9 +167,21 @@ class Preferences {
apply(SEND_SCAN_TYPE, value)
field = value
}
var sendScanBluetooth = false
set(value) {
apply(SEND_SCAN_BLUETOOTH, value)
field = value
}
var sendScanBluetoothHost: String = ""
set(value) {
apply(SEND_SCAN_BLUETOOTH_HOST, value)
field = value
}
var customLocale: String = ""
set(value) {
apply(CUSTOM_LOCALE, value)
// Make sure this setting is written immediately because
// the app is about to restart.
commit(CUSTOM_LOCALE, value)
field = value
}
var indexOfLastSelectedFormat: Int = 0
@@ -174,11 +189,26 @@ class Preferences {
apply(INDEX_OF_LAST_SELECTED_FORMAT, value)
field = value
}
var freeRotation = true
var indexOfLastSelectedEcLevel: Int = 0
set(value) {
apply(INDEX_OF_LAST_SELECTED_EC_LEVEL, value)
field = value
}
var lastMargin: Int = -1
set(value) {
apply(LAST_MARGIN, value)
field = value
}
var freeRotation = false
set(value) {
apply(FREE_ROTATION, value)
field = value
}
var expandEscapeSequences = true
set(value) {
apply(EXPAND_ESCAPE_SEQUENCES, value)
field = value
}
fun init(context: Context) {
preferences = PreferenceManager.getDefaultSharedPreferences(context)
@@ -188,7 +218,16 @@ class Preferences {
fun update() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
preferences.getStringSet(BARCODE_FORMATS, barcodeFormats)?.let {
barcodeFormats = it
// Add rMQR Code for existing installations.
val addRMQRCode = "rmqr_added"
barcodeFormats = if (!preferences.getBoolean(addRMQRCode, false)) {
preferences.edit().putBoolean(addRMQRCode, true).apply()
it.toMutableSet().apply {
add(BarcodeFormat.RMQR_CODE.name)
}
} else {
it
}
}
}
cropHandleX = preferences.getInt(CROP_HANDLE_X, cropHandleX)
@@ -214,6 +253,10 @@ class Preferences {
showToastInBulkMode
)
vibrate = preferences.getBoolean(VIBRATE, vibrate)
beep = preferences.getBoolean(BEEP, beep)
preferences.getString(BEEP_TONE_NAME, beepToneName)?.also {
beepToneName = it
}
useHistory = preferences.getBoolean(USE_HISTORY, useHistory)
ignoreConsecutiveDuplicates = preferences.getBoolean(
IGNORE_CONSECUTIVE_DUPLICATES,
@@ -250,6 +293,16 @@ class Preferences {
preferences.getString(SEND_SCAN_TYPE, sendScanType)?.also {
sendScanType = it
}
sendScanBluetooth = preferences.getBoolean(
SEND_SCAN_BLUETOOTH,
sendScanBluetooth
)
preferences.getString(
SEND_SCAN_BLUETOOTH_HOST,
sendScanBluetoothHost
)?.also {
sendScanBluetoothHost = it
}
preferences.getString(CUSTOM_LOCALE, customLocale)?.also {
customLocale = it
}
@@ -257,7 +310,28 @@ class Preferences {
INDEX_OF_LAST_SELECTED_FORMAT,
indexOfLastSelectedFormat
)
indexOfLastSelectedEcLevel = preferences.getInt(
INDEX_OF_LAST_SELECTED_EC_LEVEL,
indexOfLastSelectedEcLevel
)
lastMargin = preferences.getInt(
LAST_MARGIN,
lastMargin
)
freeRotation = preferences.getBoolean(FREE_ROTATION, freeRotation)
expandEscapeSequences = preferences.getBoolean(
EXPAND_ESCAPE_SEQUENCES,
expandEscapeSequences
)
}
fun beepTone() = when (beepToneName) {
"tone_cdma_confirm" -> ToneGenerator.TONE_CDMA_CONFIRM
"tone_sup_radio_ack" -> ToneGenerator.TONE_SUP_RADIO_ACK
"tone_prop_ack" -> ToneGenerator.TONE_PROP_ACK
"tone_prop_beep" -> ToneGenerator.TONE_PROP_BEEP
"tone_prop_beep2" -> ToneGenerator.TONE_PROP_BEEP2
else -> ToneGenerator.TONE_PROP_BEEP
}
private fun put(label: String, value: Boolean) =
@@ -266,14 +340,6 @@ class Preferences {
private fun put(label: String, value: String) =
preferences.edit().putString(label, value)
private fun commit(label: String, value: Boolean) {
put(label, value).commit()
}
private fun commit(label: String, value: String) {
put(label, value).commit()
}
private fun apply(label: String, value: Boolean) {
put(label, value).apply()
}
@@ -286,6 +352,11 @@ class Preferences {
preferences.edit().putInt(label, value).apply()
}
@SuppressLint("ApplySharedPref")
private fun commit(label: String, value: String) {
preferences.edit().putString(label, value).commit()
}
@RequiresApi(Build.VERSION_CODES.HONEYCOMB)
private fun apply(label: String, value: Set<String>) {
preferences.edit().putStringSet(label, value).apply()
@@ -304,6 +375,8 @@ class Preferences {
private const val BULK_MODE_DELAY = "bulk_mode_delay"
private const val SHOW_TOAST_IN_BULK_MODE = "show_toast_in_bulk_mode"
private const val VIBRATE = "vibrate"
private const val BEEP = "beep"
private const val BEEP_TONE_NAME = "beep_tone_name"
private const val USE_HISTORY = "use_history"
private const val IGNORE_CONSECUTIVE_DUPLICATES = "ignore_consecutive_duplicates"
private const val OPEN_IMMEDIATELY = "open_immediately"
@@ -317,8 +390,13 @@ class Preferences {
private const val SEND_SCAN_ACTIVE = "send_scan_active"
private const val SEND_SCAN_URL = "send_scan_url"
private const val SEND_SCAN_TYPE = "send_scan_type"
private const val SEND_SCAN_BLUETOOTH = "send_scan_bluetooth"
private const val SEND_SCAN_BLUETOOTH_HOST = "send_scan_bluetooth_host"
private const val CUSTOM_LOCALE = "custom_locale"
private const val INDEX_OF_LAST_SELECTED_FORMAT = "index_of_last_selected_format"
private const val INDEX_OF_LAST_SELECTED_EC_LEVEL = "index_of_last_selected_ec_level"
private const val LAST_MARGIN = "last_margin"
private const val FREE_ROTATION = "free_rotation"
private const val EXPAND_ESCAPE_SEQUENCES = "expand_escape_sequences"
}
}
@@ -1,5 +1,7 @@
package de.markusfisch.android.binaryeye.service
import android.annotation.SuppressLint
import android.app.PendingIntent
import android.content.Intent
import android.os.Build
import android.service.quicksettings.TileService
@@ -12,6 +14,21 @@ class ScanTileService : TileService() {
super.onClick()
val intent = Intent(applicationContext, CameraActivity::class.java)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
startActivityAndCollapse(intent)
// TileService#startActivityAndCollapse(Intent) is only ever run
// below UpsideDownCake, but Lint doesn't understand this yet.
@SuppressLint("StartActivityAndCollapseDeprecated")
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
@Suppress("DEPRECATION")
startActivityAndCollapse(intent)
} else {
startActivityAndCollapse(
PendingIntent.getActivity(
this,
0,
intent,
PendingIntent.FLAG_IMMUTABLE
)
)
}
}
}
@@ -0,0 +1,76 @@
package de.markusfisch.android.binaryeye.text
/**
* Copyright (c) 2000, Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// This code is based on Google's own implementation:
// http://www.java2s.com/example/android-utility-method/string-escape/javascriptunescape-string-s-dc359.html
fun String.unescape(): String {
val len = length
var i = 0
fun String.expand() = when (val ec = get(i++)) {
'n' -> '\n'
'r' -> '\r'
't' -> '\t'
'b' -> '\b'
'\\', '\"', '\'', '<', '>', '|' -> ec
'0', '1', '2', '3', '4', '5', '6', '7' -> {
val limit = if (ec < '4') 3 else 2
var l = 1
--i // Back to index of first digit.
while (l < limit &&
i + l < len &&
get(i + l) in '0'..'7'
) {
++l
}
val from = i
i += l
substring(from, i).toInt(8).toChar()
}
'x', 'u' -> {
val l = if (ec == 'u') 4 else 2
val hexCode = try {
substring(i, i + l)
} catch (_: IndexOutOfBoundsException) {
throw IllegalArgumentException(
"Invalid unicode sequence [${substring(i)}] at index $i"
)
}
val unicodeValue = try {
hexCode.toInt(16)
} catch (_: NumberFormatException) {
throw IllegalArgumentException(
"Invalid unicode sequence [$hexCode] at index $i"
)
}
i += l
unicodeValue.toChar()
}
else -> throw IllegalArgumentException(
"Unknown escape code [$ec] at index $i"
)
}
val sb = StringBuilder()
while (i < len) {
val ch = get(i++)
sb.append(if (ch == '\\' && i < len) expand() else ch)
}
return sb.toString()
}
@@ -0,0 +1,38 @@
package de.markusfisch.android.binaryeye.view
import android.content.Context
import android.media.AudioManager
import de.markusfisch.android.binaryeye.app.prefs
import de.markusfisch.android.binaryeye.media.beepConfirm
import de.markusfisch.android.binaryeye.media.beepError
import de.markusfisch.android.binaryeye.os.error
import de.markusfisch.android.binaryeye.os.getVibrator
import de.markusfisch.android.binaryeye.os.vibrate
fun Context.scanFeedback() {
if (prefs.vibrate) {
getVibrator().vibrate()
}
if (prefs.beep && !isSilent()) {
beepConfirm()
}
}
fun Context.errorFeedback() {
if (prefs.vibrate) {
getVibrator().error()
}
if (prefs.beep && !isSilent()) {
beepError()
}
}
private fun Context.isSilent(): Boolean {
val am = getSystemService(Context.AUDIO_SERVICE) as AudioManager
return when (am.ringerMode) {
AudioManager.RINGER_MODE_SILENT,
AudioManager.RINGER_MODE_VIBRATE -> true
else -> false
}
}
@@ -162,6 +162,7 @@ class DetectorView : View {
false
}
}
MotionEvent.ACTION_MOVE -> {
if (handleGrabbed) {
handlePos.set(x, y)
@@ -176,6 +177,7 @@ class DetectorView : View {
false
}
}
MotionEvent.ACTION_CANCEL -> {
if (handleGrabbed) {
snap(x, y)
@@ -183,6 +185,7 @@ class DetectorView : View {
}
false
}
MotionEvent.ACTION_UP -> {
if (handleGrabbed) {
if (!handleActive) {
@@ -199,6 +202,7 @@ class DetectorView : View {
}
false
}
else -> super.onTouchEvent(event)
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.9 KiB

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 731 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 899 B

After

Width:  |  Height:  |  Size: 583 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 755 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 899 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 698 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.3 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.8 KiB

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 16 KiB

@@ -0,0 +1,3 @@
<vector android:height="24dp" android:tint="#FFFFFF" android:viewportHeight="24.0" android:viewportWidth="24.0" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M14.54,18.49L14.54,16.51L17.51,16.51L17.51,13.54L19.49,13.54L19.49,16.51L22.46,16.51L22.46,18.49L19.508,18.508L19.49,21.46L17.51,21.46L17.492,18.508L14.54,18.49ZM13.35,14.75L11,12.4L11,7L13,7L13,11.6L14.4,13L13.35,14.75ZM3,10L3,4L5,4L5,6.35C5.85,5.283 6.888,4.458 8.113,3.875C9.338,3.292 10.633,3 12,3C14.5,3 16.625,3.875 18.375,5.625C20.125,7.375 21,9.5 21,12L19,12C19,10.05 18.321,8.396 16.963,7.037C15.604,5.679 13.95,5 12,5C10.85,5 9.775,5.267 8.775,5.8C7.775,6.333 6.933,7.067 6.25,8L9,8L9,10L3,10ZM12,21C9.7,21 7.696,20.237 5.988,18.712C4.279,17.187 3.3,15.283 3.05,13L5.1,13C5.333,14.733 6.104,16.167 7.413,17.3C8.721,18.433 10.25,19 12,19C12.183,19 12.354,18.996 12.513,18.987C12.671,18.979 12.833,18.958 13,18.925L13,20.95C12.833,20.967 12.671,20.979 12.513,20.987C12.354,20.996 12.183,21 12,21Z"/>
</vector>
@@ -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">
<path android:fillColor="@android:color/white" 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"/>
<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">
<path android:fillColor="@android:color/white" 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"/>
<path android:fillColor="#ffffff" android:pathData="M12 17c1.1 0 2-0.9 2-2s-0.9-2-2-2-2 0.9-2 2 0.9 2 2 2zm6-9h-1V6c0-2.76-2.24-5-5-5S7 3.24 7 6h1.9c0-1.71 1.39-3.1 3.1-3.1 1.71 0 3.1 1.39 3.1 3.1v2H6c-1.1 0-2 0.9-2 2v10c0 1.1 0.9 2 2 2h12c1.1 0 2-0.9 2-2V10c0-1.1-0.9-2-2-2zm0 12H6V10h12v10z"/>
</vector>
@@ -1,5 +1,4 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="90dp" android:height="90dp" android:viewportWidth="90" android:viewportHeight="90">
<path android:fillColor="#FF000000" android:pathData="M27 45a18 18 0 1 1 36 0 18 18 0 1 1-36 0" android:strokeWidth="4"/>
<path android:pathData="M31.738 45a13.262 13.262 0 1 1 26.524 0 13.262 13.262 0 1 1-26.524 0" android:strokeWidth="4" android:fillColor="#ffffff"/>
<path android:fillColor="#FF000000" android:pathData="M58.262 45c0 7.324-5.938 13.262-13.262 13.262-7.324 0-13.262-5.938-13.262-13.262 0-7.324 5.938-13.262 13.262-13.262 7.324 0 13.262 5.938 13.262 13.262zM45 27c-9.941 0-18 8.059-18 18s8.059 18 18 18 18-8.059 18-18-8.059-18-18-18z" android:strokeWidth="4"/>
<path android:pathData="M54.41 45c0 2.303-0.827 4.412-2.201 6.048V38.952c1.374 1.636 2.201 3.745 2.201 6.048zM45 54.41c-0.27 0-0.537-0.012-0.801-0.034V35.624C44.463 35.602 44.73 35.59 45 35.59s0.537 0.012 0.801 0.034v18.752C45.537 54.398 45.27 54.41 45 54.41zM35.59 45c0-2.303 0.827-4.412 2.201-6.048v12.096C36.417 49.412 35.59 47.303 35.59 45zm13.415 8.517c-0.511 0.241-1.046 0.437-1.602 0.583V35.9c0.556 0.146 1.091 0.342 1.602 0.583v17.034zM42.597 54.1c-1.173-0.309-2.257-0.839-3.204-1.542V37.442c0.947-0.703 2.031-1.233 3.204-1.542v18.2z" android:strokeWidth="4" android:fillColor="#000000"/>
</vector>
@@ -13,6 +13,17 @@
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<EditText
android:id="@+id/label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:gravity="start|top"
android:hint="@string/label"
android:imeOptions="flagNoExtractUi"
android:importantForAutofill="no"
android:inputType="text"
tools:ignore="UnusedAttribute"/>
<EditText
android:id="@+id/content"
android:layout_width="match_parent"
+49 -31
View File
@@ -17,11 +17,32 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:text="@string/format"/>
<Spinner
android:id="@+id/format"
android:text="@string/content"/>
<EditText
tools:targetApi="o"
android:id="@+id/content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="start|top"
android:typeface="monospace"
android:imeOptions="flagNoExtractUi"
android:inputType="textMultiLine"
android:hint="@string/input_content_here"
android:importantForAutofill="no"/>
<CheckBox
android:id="@+id/unescape"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:text="@string/unescape"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:text="@string/format"/>
<Spinner
style="@style/Spinner"
android:id="@+id/format"
android:layout_marginBottom="16dp"/>
<TextView
android:id="@+id/error_correction_label"
@@ -30,11 +51,9 @@
android:layout_marginBottom="8dp"
android:text="@string/error_correction_level"/>
<Spinner
style="@style/Spinner"
android:id="@+id/error_correction_level"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:entries="@array/error_correction_levels"/>
android:layout_marginBottom="16dp"/>
<TextView
android:id="@+id/colors_label"
android:layout_width="match_parent"
@@ -42,47 +61,46 @@
android:layout_marginBottom="8dp"
android:text="@string/colors"/>
<Spinner
style="@style/Spinner"
android:id="@+id/colors"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:entries="@array/colors"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:text="@string/size"/>
<TextView
android:id="@+id/size_display"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:gravity="center"/>
android:layout_marginBottom="8dp"/>
<SeekBar
style="@style/Widget.AppCompat.SeekBar.Discrete"
android:id="@+id/size_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:paddingTop="16dp"
android:paddingBottom="16dp"
android:progress="4"
android:max="7"/>
<TextView
<LinearLayout
android:id="@+id/margin"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:text="@string/content"/>
<EditText
tools:targetApi="o"
android:id="@+id/content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:gravity="start|top"
android:typeface="monospace"
android:imeOptions="flagNoExtractUi"
android:inputType="textMultiLine"
android:hint="@string/input_content_here"
android:importantForAutofill="no"/>
android:orientation="vertical"
android:visibility="gone">
<TextView
android:id="@+id/margin_display"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"/>
<SeekBar
style="@style/Widget.AppCompat.SeekBar.Discrete"
android:id="@+id/margin_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:paddingTop="16dp"
android:paddingBottom="16dp"
android:max="32"/>
</LinearLayout>
</LinearLayout>
</de.markusfisch.android.binaryeye.widget.ConfinedScrollView>
<android.support.design.widget.CoordinatorLayout
@@ -0,0 +1,32 @@
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ListView
android:id="@+id/suggestions"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:choiceMode="multipleChoice"
android:clipToPadding="false"
android:divider="@color/separator"
android:dividerHeight="1px"/>
<android.support.design.widget.CoordinatorLayout
android:id="@+id/inset_layout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:visibility="gone"
android:id="@+id/no_suggestions"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center"
android:padding="32dp"
android:text="@string/clear_network_suggestions_nothing_to_remove"/>
<android.support.design.widget.FloatingActionButton
style="@style/FAB"
android:id="@+id/remove"
android:contentDescription="@string/remove_suggestion"
android:src="@drawable/ic_action_remove"/>
</android.support.design.widget.CoordinatorLayout>
</FrameLayout>
+1 -1
View File
@@ -13,7 +13,7 @@
material:showAsAction="ifRoom"/>
<item
android:id="@+id/pick_file"
android:title="@string/pick_file"
android:title="@string/pick_image_file"
android:icon="@drawable/ic_action_pick_file"
material:showAsAction="ifRoom"/>
<item
@@ -1,6 +1,11 @@
<menu
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:material="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/add_to_history"
android:title="@string/add_to_history"
android:icon="@drawable/ic_action_history_add"
material:showAsAction="ifRoom"/>
<item
android:id="@+id/copy_to_clipboard"
android:title="@string/copy_to_clipboard"
@@ -23,6 +23,11 @@
android:title="@string/share"
android:icon="@drawable/ic_action_share"
material:showAsAction="ifRoom"/>
<item
android:id="@+id/share_file"
android:title="@string/share_file"
android:icon="@drawable/ic_action_share"
material:showAsAction="ifRoom"/>
<item
android:id="@+id/create"
android:title="@string/compose_barcode"
@@ -0,0 +1,9 @@
<menu
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:material="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/pick_file"
android:title="@string/pick_file"
android:icon="@drawable/ic_action_pick_file"
material:showAsAction="ifRoom"/>
</menu>
+21 -19
View File
@@ -1,23 +1,25 @@
<menu
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:material="http://schemas.android.com/apk/res-auto">
<group android:id="@+id/scans_available"
android:visible="false">
<item
android:id="@+id/search"
android:icon="@drawable/ic_action_search"
android:title="@string/search_scan"
material:actionViewClass="android.support.v7.widget.SearchView"
material:showAsAction="collapseActionView|always"/>
<item
android:id="@+id/export_history"
android:icon="@drawable/ic_action_export"
android:title="@string/export_to_file"
material:showAsAction="ifRoom"/>
<item
android:id="@+id/clear"
android:icon="@drawable/ic_action_remove"
android:title="@string/clear_history"
material:showAsAction="ifRoom"/>
</group>
<item
android:id="@+id/search"
android:icon="@drawable/ic_action_search"
android:title="@string/search_scan"
material:actionViewClass="android.support.v7.widget.SearchView"
material:showAsAction="collapseActionView|always"/>
<item
android:id="@+id/export_history"
android:icon="@drawable/ic_action_export"
android:title="@string/export_to_file"
material:showAsAction="ifRoom"/>
<item
android:id="@+id/share_as_file"
android:icon="@drawable/ic_action_share"
android:title="@string/share_file"
material:showAsAction="ifRoom"/>
<item
android:id="@+id/clear"
android:icon="@drawable/ic_action_remove"
android:title="@string/clear_history"
material:showAsAction="ifRoom"/>
</menu>
@@ -7,8 +7,8 @@
android:icon="@drawable/ic_action_copy"
material:showAsAction="ifRoom"/>
<item
android:id="@+id/edit_scan"
android:title="@string/edit_scan"
android:id="@+id/edit_label"
android:title="@string/edit_label"
android:icon="@drawable/ic_action_edit"
material:showAsAction="ifRoom"/>
<item
Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.9 KiB

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.3 KiB

After

Width:  |  Height:  |  Size: 6.9 KiB

+200
View File
@@ -0,0 +1,200 @@
<resources>
<string name="camera_error">Няма достъп до камерата, моля, опитайте отново</string>
<string name="scan_code">Сканирайте код</string>
<string name="compose_barcode">Създаване на баркод</string>
<string name="decode_barcode">Декодиране на баркод</string>
<string name="content">Съдържание</string>
<string name="binary_data">(бинарни данни)</string>
<plurals name="barcode_info">
<item quantity="one">%2$d символ, %1$s</item>
<item quantity="other">%2$d символа, %1$s</item>
</plurals>
<string name="error_correction_level">Ниво на коригиране на грешки</string>
<string name="error_correction_level_l" formatted="false">Ниско (~7&#37; корекция)</string>
<string name="error_correction_level_m" formatted="false">Средно (~15&#37; корекция)</string>
<string name="error_correction_level_q" formatted="false">Четвъртично (~25&#37; корекция)</string>
<string name="error_correction_level_h" formatted="false">Високо (~30&#37; корекция)</string>
<string name="label">Label</string>
<string name="colors">Преден и заден план</string>
<string name="colors_black_on_white">Черно на бяло</string>
<string name="colors_white_on_black">Бяло на черно</string>
<string name="colors_black_on_transparent">Черно на прозрачно</string>
<string name="colors_white_on_transparent">Бяло на прозрачно</string>
<string name="sequence_size">Размер на последователността</string>
<string name="sequence_index">Индекс на последователността</string>
<string name="sequence_id">ID на последователността</string>
<string name="gtin_issue_number">Номер на изданието</string>
<string name="gtin_country">Възможна страна на производство</string>
<string name="gtin_price">Предположена цена</string>
<string name="gtin_add_on">UPC EAN разширение</string>
<string name="barcode_version_number">Версия</string>
<string name="qr_version_and_modules">%1$s (%2$d модули)</string>
<string name="toggle_flash">Превключване на светкавицата</string>
<string name="error_flash">Грешка при превключването на светкавицата</string>
<string name="share">Сподели</string>
<string name="share_file">Share as file</string>
<string name="share_as">Сподели като?</string>
<string name="copy_to_clipboard">Копиране в клипборда</string>
<string name="copied_to_clipboard">Копирано в клипборда</string>
<string name="copy_password">Копиране на паролата в клипборда</string>
<string name="copied_password_to_clipboard">Паролата е копирана в клипборда</string>
<string name="open_url">Отворете URL(Линк)</string>
<string name="cannot_resolve_action">Няма приложение което може да отвори това</string>
<string name="pick_search_engine">Изберете търсачка</string>
<string name="format">Формат</string>
<string name="size_width_by_height">Размер в пиксели: %1$d×%2$d</string>
<string name="margin_size">Марж в пиксели: %1$d</string>
<string name="input_content_here">Въведете съдържание тук</string>
<string name="unescape">Разширете последователности за извеждане</string>
<string name="encode">КОДИРАНЕ</string>
<string name="error_no_content">Липсва съдържание</string>
<string name="error_encoding_barcode">Баркодът не може да бъде генериран</string>
<string name="view_barcode">Вижте баркода</string>
<string name="info">Информация</string>
<string name="switch_camera">Превключване на камерата</string>
<string name="bulk_mode">Сканирайте непрекъснато</string>
<string name="bulk_mode_summary">Винаги започвай непрекъснато сканиране.</string>
<string name="bulk_mode_delay">Забавяне между непрекъснатите сканирания</string>
<string name="restrict_format">Ограничаване на формат</string>
<string name="remove_restriction">Премахване на ограничението</string>
<string name="scan_format">Сканиране %s</string>
<string name="quarter_second">Червърт секунда</string>
<string name="half_second">Половин секунда</string>
<string name="a_second">Една секунда</string>
<string name="two_seconds">Две секунди</string>
<string name="five_seconds">Пет секунди</string>
<string name="history">История</string>
<string name="preferences">Настройки</string>
<string name="scan_category">Сканиране</string>
<string name="content_category">Съдържание</string>
<string name="locale_category">Език</string>
<string name="custom_locale">Персонализиран език</string>
<string name="follow_system_settings">Следване на системните настройки</string>
<string name="show_crop_handle">Показване на ограничителя за изрязване</string>
<string name="show_crop_handle_summary">Ограничете сканирането до персонализирана област.</string>
<string name="barcode_formats">Баркодови формати</string>
<string name="zoom_by_swiping">Плъзване нагоре/надолу, за да увеличите</string>
<string name="zoom_by_swiping_summary">Плъзнете нагоре или надолу в екрана на камерата, за да контролирате приближаването на камерата.</string>
<string name="auto_rotate">Разпознаване на вертикални 1D баркодове</string>
<string name="auto_rotate_summary">Автоматично завъртете рамката на камерата, за разпознаване на вертикални 1D баркодове. В зависимост от устройството това може да повлияе на скоростта.</string>
<string name="try_harder">Оптимизиране на четеца за точност</string>
<string name="try_harder_summary">Активирайте тази опция за много трудни за четене кодове. Ще повлияе на скоростта.</string>
<string name="show_toast_in_bulk_mode">Показване на данни в непрекъснат режим</string>
<string name="show_toast_in_bulk_mode_summary">Кратко показване на сканираните данни при непрекъснато сканиране.</string>
<string name="vibrate">Вибриране при сканиране</string>
<string name="vibrate_summary">Активирайте това, ако искате устройството ви да вибрира при разпознаване на код.</string>
<string name="beep">Звуков сигнал при сканиране</string>
<string name="beep_summary">Активирайте това, ако искате вашето устройство да издава звуков сигнал при разпознаване на код.</string>
<string name="beep_tone">Звуков сигнал</string>
<string name="tone_cdma_confirm">CDMA потвърждение</string>
<string name="tone_sup_confirm">Потвърждение на надзорния разговор</string>
<string name="tone_sup_radio_ack">Потвърждение на контролния радиопът на повикване</string>
<string name="tone_prop_ack">Положително признание</string>
<string name="tone_prop_beep">Общ звуков сигнал</string>
<string name="tone_prop_beep2">Общ двоен звуков сигнал</string>
<string name="use_history">Хронология на сканирането</string>
<string name="use_history_summary">Запазва разпознатите кодове на вашето устройство.</string>
<string name="ignore_consecutive_duplicates">Игнориране на дубликатите</string>
<string name="ignore_consecutive_duplicates_summary">Не запазва последователни дубликати в хронологията на сканирането.</string>
<string name="open_immediately">Отваряй внезапно</string>
<string name="open_immediately_summary">Пропуска проверката и внезапно отваря съдържанието. Може да отвори вредно съдържание, ако е активирано.</string>
<string name="copy_immediately">Поставяй в клипборда</string>
<string name="copy_immediately_summary">Автоматично копира сканираното съдържание в клипборда. Моля, имайте предвид, че други приложения може да подслушват вашия клипборд във фонов режим.</string>
<string name="show_meta_data">Показване на метаданни</string>
<string name="show_meta_data_summary">Показване на допълнителни данни за сканирания баркод.</string>
<string name="show_hex_dump">Показване на шестнадесетичен дъмп</string>
<string name="show_hex_dump_summary">Показване на шестнадесетичен дъмп на сканираното съдържание.</string>
<string name="show_recreation">Показване на пресъздаден баркод</string>
<string name="show_recreation_summary">Създай и покажи баркод отново от прочетеното съдържание, ако е възможно.</string>
<string name="close_automatically">Върщане се след Копиране/Споделяне</string>
<string name="close_automatically_summary">Автоматично връщане към екрана за сканиране след копиране или споделяне на прочетеното съдържание.</string>
<string name="default_search_engine">Отворяй неизвестни данни в</string>
<string name="always_ask">Винаги питай</string>
<string name="open_with_url">Отворяне на неизвестни данни с URL</string>
<string name="clear_network_suggestions">Премахнете мрежовите предложения</string>
<string name="clear_network_suggestions_summary">Премахване на мрежовите предложения, които преди са били добавени с това приложение.</string>
<string name="really_remove_all_networks">Наистина ли искате да премахнете всички мрежи?</string>
<string name="clear_network_suggestions_success">Предложенията за интернет мрежи са изчистени</string>
<string name="clear_network_suggestions_nothing_to_remove">Нищо за премахване</string>
<string name="send_category">Препращане</string>
<string name="send_scan_active">Препращане на сканиранията</string>
<string name="send_scan_active_summary">Включване на препращането на сканиранията към даден URL адрес.</string>
<string name="send_scan_url">URL(Линк) за препращане</string>
<string name="send_scan_type">Тип заявка</string>
<string name="send_type_get_add_content">GET и просто добавяне на съдържанието</string>
<string name="send_type_get_query_string">GET с пълен низ на заявка</string>
<string name="send_type_post_form">POST application/x-www-form-urlencoded</string>
<string name="send_type_post_json">POST application/json</string>
<string name="send_type_external_browser">Отваряне във външен браузър</string>
<string name="test_url">Тествайте URL(Линка)</string>
<string name="send_scan_bluetooth">Препращане на сканиранията с Bluetooth</string>
<string name="send_scan_bluetooth_summary">Активиране на препращане на сканирания към даден Bluetooth хост.</string>
<string name="send_scan_bluetooth_host">Хост</string>
<string name="no_bluetooth_hosts_paired">Няма сдвоени хостове</string>
<string name="bluetooth_connect_fail">Неуспешно свързване с Bluetooth устройство.</string>
<string name="bluetooth_send_fail">Неуспешно изпращане до Bluetooth устройство.</string>
<string name="bluetooth_send_success">Успешно Bluetooth изпращане.</string>
<string name="really_remove_scan">Наистина ли искате да премахнете това сканиране?</string>
<string name="really_remove_all_scans">Наистина ли искате да премахнете всички сканирания?</string>
<string name="really_remove_selected_scans">Наистина ли искате да премахнете избраните сканирания?</string>
<string name="clear_history">Изчистване на историята</string>
<string name="copy_scan">Сопиране на сканирането</string>
<string name="edit_label">Редактиране на етикет</string>
<string name="remove_scan">Премахване на сканиране</string>
<string name="enter_name">Въведете име, за да опишете сканирането</string>
<string name="enter_name_hint">Име на сканирането</string>
<string name="no_barcode_found">Няма намерен баркод</string>
<string name="pick_list_separator">Как бъдат разделени елементите от списъка?</string>
<string name="separator_line_break">Нова линия</string>
<string name="separator_ruler">Лента</string>
<string name="save_as_file_name">Запазване в папка за изтегляния?</string>
<string name="file_name">Име на файл</string>
<string name="error_saving_file">Неуспешно запазване на Файлът</string>
<string name="error_file_exists">Файлът вече съществува</string>
<string name="connect_to_wifi">Свържи се към Wi-Fi</string>
<string name="wifi_config_failed">Неуспешно конфигуриране на Wi-Fi</string>
<string name="wifi_added">Wi-Fi успешно добавен</string>
<string name="sms_send">Изпрати SMS</string>
<string name="sms_error">Неуспешно изпращане на SMS</string>
<string name="tel_dial">Набери номер</string>
<string name="tel_error">Неуспешно набиране на номера</string>
<string name="mail_send">Изпрати писмо</string>
<string name="mail_error">Неуспешно изпращане на имейл</string>
<string name="vcard_add">Добавяне към контактите</string>
<string name="vcard_failed">Неуспешно добавяне към контактите</string>
<string name="vevent_add">Добавяне към календара</string>
<string name="vevent_failed">Неуспешно добавяне към календара</string>
<string name="otpauth_add">Добави 2FA</string>
<string name="search_web">Търси в мрежата</string>
<string name="search_scan">Търсещо сканиране</string>
<string name="add_to_history">Add to history</string>
<string name="added_to_history">Added to history</string>
<string name="export_to_file">Извеждане в файл</string>
<string name="export_as">Извеждане като?</string>
<string name="export_csv_comma">CSV файл със запетаи</string>
<string name="export_csv_semicolon">CSV файл с точки и запетаи</string>
<string name="export_json">JSON файл</string>
<string name="export_database">SQLite база данни</string>
<string name="saved_in_downloads">Запазено в изтеглянията</string>
<string name="rotate_image_cw">Завъртете изображението по часовниковата стрелка</string>
<string name="toggle_free_rotation">Превключване на свободното въртене</string>
<string name="pick_file">Изберете файл</string>
<string name="pick_image_file">Изберете снимка</string>
<string name="pick_code_to_scan">Изберете код за сканиране</string>
<string name="shortcut_decode">Сканирай баркод</string>
<string name="shortcut_encode">Създай баркод</string>
<string name="shortcut_preferences">Настройки</string>
<string name="background_request_failed">Неуспешна заявка в фонов режим</string>
<string name="entry_type">Тип</string>
<string name="wifi_network">Wi-Fi мрежа</string>
<string name="wifi_ssid">Име</string>
<string name="wifi_type">Тип удостоверяване</string>
<string name="wifi_password">Парола</string>
<string name="wifi_hidden">Скрита интернет мрежа</string>
<string name="wifi_eap">EAP метод</string>
<string name="wifi_anonymous_identity">Анонимна идентичност</string>
<string name="wifi_identity">Идентичност</string>
<string name="wifi_phase2">Фаза 2 метод</string>
<string name="network_suggestions">Предложения за интернет мрежа</string>
<string name="remove_suggestion">Премахване на предложението</string>
</resources>

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