Compare commits

...
211 Commits
Author SHA1 Message Date
Markus Fisch f160cf0953 Advance version number to 1.43.0 2021-06-01 22:04:17 +02:00
Markus Fisch ef5b5aa34a Add a preference to remove network suggestions
Because as it turns out, WiFi network suggestions cannot be removed
by the user. Which is pretty silly in my opinion. But anyway, now a
user can remove all the networks added by Binary Eye.
2021-06-01 21:57:20 +02:00
GazandGitHub 536ee5bc3d Update Traditional Chinese Translation 2021-05-25 12:03:51 +02:00
solokotandGitHub 0becada572 Update Russian language 2021-05-25 11:55:34 +02:00
Markus Fisch 0c773d6f05 Update changelogs for F-Droid 2021-05-24 22:19:23 +02:00
Markus Fisch f2df364840 Advance version number to 1.42.0 2021-05-24 22:18:26 +02:00
Markus Fisch 5740b43568 Add a setting to pick a default search engine
To open unknown data with.
2021-05-24 22:12:13 +02:00
GazandGitHub b384b99b01 Update zh-TW(Traditional Chinese) Translation 2021-05-23 12:21:27 +02:00
Heimen StoffelsandGitHub 6affa4ce25 Update Dutch translation 2021-05-18 18:40:05 +02:00
Markus Fisch 455d329fff Remove resolveActivity() for opening content
Because on Android 11+ resolveActivity() can no longer be used
to query if there's an app that can handle the intent for privacy
reasons.
2021-05-07 22:15:38 +02:00
Markus Fisch 792be87283 Advance version number to 1.41.1 2021-05-06 23:01:49 +02:00
Markus Fisch 381e10bddc Ignore exceptions in MultiFormatReader.decode()
Usually it's bad practice to blindly catch *all* exceptions
because this can hide problems we want to know about.

But since ZXing has some errors (like all software) and I don't
want this app to break in the hands of my users, which won't
see the stack trace anyway, it's better to just catch all
exceptions that may happen in MultiFormatReader.decode().
2021-05-06 17:19:49 +02:00
Markus Fisch 220d1b9d4c Remove useless context argument
The argument is never used except for calling the function again.
A leftover from an earlier implementation.
2021-05-06 17:10:00 +02:00
Markus Fisch 020cd2c192 Update tools and gradle version
And migrate from jcenter() back to mavenCentral().
2021-05-05 20:50:50 +02:00
OymateandGitHub e8766a0fd1 Update Bengali translation 2021-05-02 13:31:28 +02:00
yzqzssandGitHub 69c6d1324e Update Simplified Chinese (Zh-rCN) 2021-05-01 19:50:13 +02:00
Markus Fisch 2dd0c48225 Format comments like sentences
Makes reading multiline comments easier.
2021-04-27 19:59:21 +02:00
Markus Fisch faa536274b Update build tools 2021-04-27 19:58:31 +02:00
Markus Fisch 0a944f9fb8 Add latest changelog for F-Droid 2021-04-13 19:47:59 +02:00
Markus Fisch a31982646a Advance version number to 1.41.0 2021-04-10 14:19:54 +02:00
Markus Fisch 4320ee8fcc Crop by default for new installations
To increase performance on bad devices.
2021-04-10 14:13:29 +02:00
Markus Fisch d890bf28fa Fix creating Locale from resource format
Locale() requires the country part as secondary argument and
can't use the resource format "lang-country" directly.
2021-04-09 10:23:45 +02:00
Markus Fisch 6347bd568c Fix XMl of Bengali language 2021-04-07 12:15:03 +02:00
OymateandGitHub 9de11e2ae8 Add Bangla translation 2021-04-07 11:12:15 +02:00
Ryota HasegawaandGitHub 57c381e310 Add missing Japanese locale 2021-03-25 09:28:20 +01:00
Balázs ÚrandGitHub a11650eaa5 Update Hungarian language pack
* Update Hungarian translation
* Add Hungarian app description
2021-03-24 09:34:14 +01:00
solokotandGitHub e85b006e40 Update Russian app description 2021-03-20 19:59:29 +01:00
OymateandGitHub a6c25be316 Add translation for Bangla
* Create title.txt
* Create summary.txt
* Create short_description.txt
* Create full_description.txt
2021-03-20 17:44:48 +01:00
fossddandGitHub 2b011325a6 Add summary from other languages 2021-03-18 09:37:24 +01:00
Markus Fisch fb96d5ecf8 Rename fastlane locale directories
On request of F-Droid.
2021-03-17 20:28:34 +01:00
Markus Fisch 897d61a646 Truncate all toasts with strings from the outside
Ellipsize texts in Toasts after 128 characters to not clutter the UI
with too long texts.

There's at least one case where too long toast texts caused crashes:
https://github.com/markusfisch/BinaryEye/issues/166
2021-03-13 16:08:32 +01:00
Markus Fisch ac67db6491 Advance version number to 1.40.0 2021-03-09 20:09:19 +01:00
solokotandGitHub 84bc7dad30 Update Russian language 2021-03-09 20:03:32 +01:00
Markus Fisch b21af65b68 Only reset bulk mode if it's always enabled
Otherwise the current state would be lost and rotating the device
with bulk mode on would reset it.
2021-03-08 20:18:35 +01:00
Markus Fisch a7adec7177 Share hex dump if content is binary
Instead of just sharing "(binary data)" which is quite useless.
2021-03-08 20:13:25 +01:00
Markus Fisch fc6fcfa743 Only show the first 50 characters in data Toast
More is just cluttering the UI.
2021-03-08 19:58:05 +01:00
Markus Fisch 47f95fc67a Add a setting to show/hide Toast with scanned data
When scanning continuously.

If we are scanning many codes at once, this might be too
cumbersome.
2021-03-08 19:50:38 +01:00
solokotandGitHub deccc9eb36 Update Russian language 2021-03-08 19:27:02 +01:00
Markus Fisch 6505312b44 Add latest changelog for F-Droid 2021-03-02 20:07:26 +01:00
Markus Fisch cb82fc3252 Advance version number to 1.39.0 2021-03-02 20:06:15 +01:00
Markus Fisch 636ac2386a Add a setting to always start in bulk mode 2021-03-02 19:51:41 +01:00
Markus Fisch 49d32ff95c Remove trying to resolve activities on API 30+.
Avoid using `intent.resolveActivity()` at API level 30+ due to the
new package visibility restrictions. In order for `resolveActivity()`
to "see" another package, we would need to list that package/intent
in a `<queries>` block in the Manifest.

But since we used `resolveActivity()` only to avoid an exception
if the Intent cannot be resolved, it's much easier and more robust
to just try and catch that exception if necessary.

This fixes adding contacts (and other things) on API level 30+.
2021-03-02 19:12:56 +01:00
ElaborendumandGitHub fed7d561c5 Update Spanish translation 2021-02-27 11:34:59 +01:00
Markus Fisch 8e22290c5f Refactor settings label to change language
"Custom locale" is a bit too cumbersome.
2021-02-18 16:38:17 +01:00
ss11mikandGitHub b33e474d12 Update Czech translation
* Fix screwed indentation
* Add missing plural
* Add short_description.txt
* Add full_description.txt
2021-02-18 16:37:02 +01:00
ss11mikandGitHub 40a05a1fc0 Add Czech translation
* Add Czech translation
* Add Czech strings
2021-02-18 09:58:46 +01:00
Markus Fisch d947764882 Add latest changelog for F-Droid 2021-02-13 18:00:51 +01:00
Markus Fisch 1a5a8e0fa1 Advance version number to 1.38.0 2021-02-13 18:00:00 +01:00
Markus Fisch e6d9d54866 Add a quick settings tile to scan from settings 2021-02-12 15:29:59 +01:00
Markus Fisch 538155bf04 Optimize some vector drawables with avocado 2021-02-12 15:28:16 +01:00
Markus Fisch 3266a58041 Remove double icon 2021-02-12 15:27:48 +01:00
Markus Fisch d594a43d6d Make background requests time out after 5 seconds 2021-02-07 20:26:48 +01:00
gnuhead-chiebandGitHub 0aa1f80451 Add Japanese translation 2021-01-25 21:23:00 +01:00
Markus Fisch 36eff75546 Move clipboard and sharing functions into content 2021-01-24 19:41:12 +01:00
Markus Fisch 8aef9c22dd Move file saving functions into io folder 2021-01-24 19:35:29 +01:00
Markus Fisch 1a159b35ee Move soft keyboard extensions into view 2021-01-24 19:32:29 +01:00
Markus Fisch 05ab87caa8 Move system bar functions into view folder
Because the system bars are just views in the end.
2021-01-24 19:28:55 +01:00
Markus Fisch a3f0185f08 Refactor Content.kt to Fragments.kt
Because it's all about fragments really.
2021-01-24 19:24:01 +01:00
Markus Fisch 104f409486 Move Vibrator extension functions into os folder 2021-01-24 19:19:24 +01:00
Bastien JansenandGitHub eb8f578bd8 Updated French translation
* fixed a few incorrect/confusing translations
* added missing translations
2021-01-24 19:12:21 +01:00
Markus Fisch a8e420fc95 Add changelogs for F-Droid 2021-01-23 18:52:13 +01:00
Markus Fisch 7120e18670 Advance version number to 1.37.0 2021-01-23 18:46:16 +01:00
Markus Fisch 9fb34965f5 Prettify barcode format name
Means replacing ZXing's underscore with a space at this moment.
2021-01-23 18:09:46 +01:00
Markus Fisch c5cbd1140b Format date/time in history according to locale
Easier to read.
2021-01-23 16:51:38 +01:00
Markus Fisch fecdcd7deb Remove redundant SAM-constructor 2021-01-23 14:04:28 +01:00
Markus Fisch fd3d2f9934 Fix cancelling job in DecodeFragment
Can't cancel a Job in onDestroyView() because that will get invoked
as soons as another Fragment is attached. But parentJob is a final
member that only gets initialized when the Fragment is instantiated.
So if we're returning from another Fragment, parentJob has been
cancelled already.
2021-01-23 14:00:26 +01:00
Markus Fisch 35b6ead662 Vibrate and play a tone when a HTTP request fails
To make failures stand out.
2021-01-22 15:46:43 +01:00
Markus Fisch ad62cb5a03 Advance version number to 1.36.0 2021-01-20 21:37:54 +01:00
Markus Fisch e1905b1ff2 Add query parameter to back-end URL if necessary 2021-01-19 17:34:08 +01:00
Markus Fisch 9aecf0d83b Add a button to send a test request
For the URL of background requests.
Makes using this feature a bit easier.
2021-01-19 17:24:36 +01:00
Markus Fisch 2645366baa Automatically add URL scheme if its missing
For convenience.
2021-01-19 17:23:51 +01:00
Markus Fisch 37ea4ac9af Show HTTP response for background requests
Show the first three lines (or 240 character) of the HTTP
response (or error message) in the Toast.
2021-01-17 20:03:12 +01:00
Markus Fisch 4a6f5c5f92 Set formatted to false for EC string resources
To make lint happy.
2021-01-17 19:08:37 +01:00
Markus Fisch b1ddb0fe7a Apply reformat code
To put everything back in order.
2021-01-17 18:32:08 +01:00
Markus Fisch 2f61db731d Automatically execute action after permission
Because it's quite irritating when the action doesn't automatically
continue after permission was granted.
2021-01-17 18:27:24 +01:00
Markus Fisch b0de90c702 Improve export option labels
Make it clear that we're exporting in a file.
2021-01-15 22:53:14 +01:00
Markus Fisch 8df09090c9 Add a setting to automatically return to scanning
After copy/share was hit on some decoded content.

Saves a tap to get back.
2021-01-15 22:44:10 +01:00
zmniandGitHub 9feef33275 Update Indonesian translation 2021-01-13 18:27:26 +01:00
147ad22df0 Update French translation
Co-authored-by: Grena <grena@grenabox.fr>
2021-01-13 18:26:59 +01:00
Markus Fisch be833b7645 Make static encode methods top level functions
Because it's more simple and the preferred style in Kotlin.
2021-01-02 18:21:00 +01:00
Markus Fisch 0eb921d579 Give EnumMap of encode hints to Zxing.encode()
Instead of transferring just the ErrorCorrectionLevel from the
EncodeFragment. This way we can transfer additional hints for
other Barcode formats too.
2021-01-02 18:13:16 +01:00
emintufanandGitHub 1d156e0dbd Update Turkish translation 2021-01-02 14:14:47 +01:00
Markus Fisch 82c1225848 Use a single <path/> element in SVG exports
Much more compact then using <rect/>'s.

Also, the resolution of the SVG will now match the native resolution
of the barcode so the requested size becomes meaningless for SVGs.

1D barcodes with a native resolution of N x 1 will be expanded to be
half as high as they're wide.
2020-12-24 14:44:12 +01:00
Markus Fisch 33c1697d80 Add missing changelog for version 74 2020-12-17 22:21:32 +01:00
Sergiy StuparandGitHub 1d4e5d25ae Add Ukrainian translation for fastlane 2020-12-12 15:38:18 +01:00
wars-safeandGitHub 386b47fbed Chinese language update
* Update README.md
* Rename app/src/main/res/values-zh/strings.xml to app/src/main/res/values-zh-rTW/strings.xml
* Create full_description.txt
* Create short_description.txt
2020-11-27 00:22:04 +01:00
Markus Fisch b2c2632d76 Fix percent character in resource strings
'%%' doesn't work when the string never goes through String.format().

Instead, a character reference should be used.
2020-11-23 19:25:34 +01:00
Markus Fisch 4d64a5704d Advance version number to 1.35.0 2020-11-18 22:19:30 +01:00
Markus Fisch 8875937022 Provide RenderScript context in PreprocessorTest
The RenderScript context is no longer created by Preprocessor
but must be provided.
2020-11-18 22:08:52 +01:00
Markus Fisch 3d33f62d7b Use Error Correction Level only for QR Codes
Of course. Other codes may or may not have different error
correction levels.
2020-11-18 22:05:42 +01:00
Markus Fisch a4050cbf8f Update build tools version 2020-11-18 21:23:29 +01:00
Markus Fisch abb362230e Put images with transparent pixels on white/black
So the barcode can be scanned even if an image contains transparent
pixels.

To do this, a reduction kernel is used to find out if an image has
transparent pixels and if it's dominant light or dark. Then, the
image is put on the opposite background color to maximize contrast.

This commit also make the RenderScript context a property of the
Activity rather than the processing object/function because there
can be only one RenderScript alive at any moment. So it's better
to bind it to the Activity.
2020-11-18 21:11:44 +01:00
Markus Fisch 967cab9c7b Remove translation restrictions from image to scan
So every part of the image can be moved into the region of
interest (which is always in the center of the screen).
2020-11-16 19:53:58 +01:00
Markus Fisch c8d7be95e9 Use cropping limiter for scanning in images too
So it's possible to restrict scanning to a non-square section of
the image. Especially useful for barcodes that are more rectangular
than square.

Right now, the handling of the cropping limiter is exactly the same
as it is when scanning with the camera: The region of interest (ROI)
is always centered on the screen. A user is supposed to move the
image with the barcode using the well-known pinch and zoom gesture.

While this should work in most cases, it's possible that a barcode
at the far edge of an image can't be moved to the center of the
screen because of ScalingImageView's move restrictions.

There are two options: either make the ROI freely moveable on the
screen or remove the move restrictions from ScalingImageView.
2020-11-16 11:22:52 +01:00
Markus Fisch 18146ec41a Set error correction level when creating QR Codes
As requested.
2020-11-08 18:52:33 +01:00
Markus Fisch 5cd6114ba1 Add adaptive icons for shortcuts 2020-11-01 17:56:00 +01:00
Markus Fisch 8a310a1144 Replace background of adaptive icon with a color
Much simpler.
2020-11-01 17:24:18 +01:00
Markus Fisch afc597ab90 Support setting a custom deviating locale
As requested.

In some countries and languages the translations of apps are really
bad or even non-existent, so people there use their devices set to
English. This setting makes it possible to use a deviating locale
for Binary Eye only, when there is a good translation, while keeping
the system set to English.
2020-11-01 17:09:42 +01:00
Markus Fisch 6d742c4d19 Use Gradle wrapper with sources
So it's easier to peek into it.
2020-10-18 14:20:32 +02:00
Markus Fisch 1776559d40 Save PNG for sharing on IO dispatcher 2020-10-18 14:13:32 +02:00
Markus Fisch 75c1eb6b64 Pick MIME type when sharing a new barcode too
So a barcode can be saved *and* shared as PNG, SVG or plain text.
2020-10-18 14:08:49 +02:00
Markus Fisch c5cffc00c3 Add exporting barcodes in text form
As requested.
2020-10-17 17:43:02 +02:00
Markus Fisch c0153e822c Update tools version and gradle wrapper 2020-10-17 17:40:05 +02:00
Sergiy StuparandGitHub f070269c2c Fix typo in Ukrainian translation 2020-10-06 10:03:03 +02:00
Sergiy StuparandGitHub 3e8df89db1 Add Ukrainian translation 2020-10-05 13:32:11 +02:00
Markus Fisch 56237c43cd Update to SDK 30 / Android 11 2020-10-03 16:59:22 +02:00
Markus Fisch 7fc565ae67 Replace resource reference with color literal
Because resource references don't work on API < 21.
2020-10-03 16:25:21 +02:00
Markus Fisch 656c1e36a8 Update Kotlin version 2020-10-03 16:20:21 +02:00
emintufanandGitHub 42a6043f63 Add Turkish translation 2020-09-30 09:24:23 +02:00
Markus Fisch 6a5fdae584 Explain requested permissions in privacy policy 2020-09-28 11:24:07 +02:00
Giorgi AbulashviliandMarkus Fisch 899deb77b5 Add Georgian translation 2020-09-25 15:00:39 +02:00
zmniandGitHub 3419194bfd Update Indonesian translation 2020-09-07 21:41:25 +02:00
dmitriy-ilchenkoandGitHub 3b55d48f65 Update Russian translation 2020-08-17 21:56:31 +02:00
Markus Fisch d34673ddc3 Use different URL hints for open/send URLs
While the ".../?content=" part is required for Open-With-URLs,
it's confusing for Send-Scan-URLs.
2020-08-15 13:37:22 +02:00
unbranchedandGitHub 242bf4c0a1 Update Italian translation 2020-08-13 19:15:24 +02:00
Markus Fisch ed2a12eef8 Update screenshots 2020-08-11 20:30:36 +02:00
Markus Fisch 4dd5c12e85 Add missing changelogs for F-Droid 2020-08-11 20:00:43 +02:00
Markus Fisch d3fae01786 Update Simplified Chinese translation 2020-08-11 20:00:04 +02:00
Markus Fisch 37b6e6f832 Update version code to upload new build
Because building with the NDK still doesn't work for all Android
versions :(
2020-08-08 15:37:32 +02:00
Markus Fisch e3274fd065 Advance version number to 1.34.0 2020-08-07 14:34:05 +02:00
Markus Fisch c75b9f73ab Enable R8 and BLAS mode explicitly
Makes building app bundles finally work for Android 7+.

For Android 6 and lower, app bundles are still broken - but an APK
still works!

With R8 and build tools 4.0.1, we can also build on macOS Catalina
again, even when an NDK is available. So hopefully, this commit
should also fix builds by F-Droid where we cannot disable the NDK
(for details see issue #111).
2020-08-07 14:12:59 +02:00
Markus Fisch 1432669aee Add an option to send JSON in POST request 2020-08-06 14:57:38 +02:00
Markus Fisch 00a8a94b59 Add a setting to choose type of background request
Now, the app can do
* simple GET requests where the scanned data is just appended to
whatever URL is given
* GET requests with all available data fields as URL encoded query
parameters
* POST requests with all available data fields in content type
application/x-www-form-urlencoded
2020-08-05 18:22:47 +02:00
Markus Fisch c8c3da91ae Make hexDump() private in DecodeFragment
It's only ever used there and we can always make it public should
this ever change.
2020-08-05 10:57:57 +02:00
Markus Fisch 8cbc73b7f8 Use CoroutineScope in place
And remove explicit value. This code doesn't get any clearer
by using an explicit value.
2020-08-05 10:55:58 +02:00
Markus Fisch 941d875b3e Show an error when background request fails
The app shouldn't be silent if a background request fails.
2020-08-05 10:53:14 +02:00
Markus Fisch 1ac578845e Use a custom preference dialog for URL settings 2020-08-04 17:49:02 +02:00
Markus Fisch e816e8bf89 Migrate preferences to PreferenceFragmentCompat
To get proper Material Design preferences. Long overdue.
2020-08-03 18:07:00 +02:00
Markus Fisch 54c368fcd8 Update tools version 2020-08-03 18:06:30 +02:00
Markus Fisch c225e7d9eb Advance version number to 1.33.0 2020-07-26 19:25:53 +02:00
Markus Fisch 6a0ce45a65 Optimize copy password button with avocado 2020-07-26 19:15:18 +02:00
Markus Fisch 11c97cc4ef Send scanned code to custom URL in background
As requested in #95.
2020-07-26 19:12:26 +02:00
Markus Fisch e3f6732bb8 Move onDestroyView() below onCreateView()
Better keep these next to each other.
2020-07-23 11:06:15 +02:00
Markus Fisch 53ae706b9c Add a button to copy WiFi password into clipboard
That can't happen automatically anymore since there's now a setting
to automatically put the barcode contents into the clipboard. If that
is enabled, it'd interfere with this.

Also, it's probably better to make it explict anyway.
Not everybody wants to have passwords in the clipboard.
2020-07-22 14:49:15 +02:00
Markus Fisch 26a7ad2e6e Add a setting for putting contents into clipboard
Automatically, always, in the background.

This setting should come with a warning that, if enabled, all your
scans may leak into other apps through the clipboard.

You know, there are apps that continously probe the cliboard in the
background to send any data they find to their server. Nasty stuff.
2020-07-22 14:29:48 +02:00
Markus Fisch 9c3984ce7a Continue with layout even if it didn't change
Sometimes, on some devices, the soft keyboard takes a bit too long
to vanish and `onLayout()` seems to run while the keyboard's height
is still taken into account. Then, the crop handle may sometime end
up being in the wrong position.

I suppose it's either because `onLayout()` is somehow not invoked
again after the soft keyboard is gone, or because the `changed`
parameter is `false` (despite `bottom` has changed).

This may be because the view itself isn't resized by the soft
keyboard (because of View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN) but only
gets bottom padding from `WindowInsets` when the soft keyboard is
visible.

So, because this is quite hard to provoke, I will simply remove
doing nothing when `changed` is `true` and simply (re-)calculate
the position of the crop handle for every `onLayout()`.
2020-07-22 14:00:45 +02:00
zmniandGitHub f7d07e0b8a Update Indonesian translation 2020-07-11 15:24:21 +02:00
Markus Fisch 3fe0f50289 Advance version number to 1.32.1 2020-07-04 15:28:44 +02:00
Markus Fisch e86cf9d2cc Remove ROI when handle is dragged to screen corner
As an alternative way to get rid of the ROI.
2020-07-04 15:15:32 +02:00
Markus Fisch 4f1482d2c9 Restrict ROI calculation to screen space
CameraView.previewRect may be larger than the screen (and thus as
the DetectorView which is at most as big as the screen).

If previewRect is larger, its left and/or top coordinate will be
negative which must then be clamped to the screen/DetectorView.
2020-07-04 14:26:21 +02:00
Markus Fisch c42ad7dbea Set ANDROID_NDK_HOME to nothing in gradlew
This is quite a hack to ensure this app builds *without* the NDK
because using the NDK will produce broken builds for Android 6.

For details see:
https://github.com/markusfisch/BinaryEye/issues/111

Unfortunately, it's (currently) not possible to configure Gradle
to simply ignore the NDK. `nkd.dir` cannot be set to nothing or
an invalid value.
2020-07-04 14:09:57 +02:00
Markus Fisch fa3cc8c408 Remove extra action item to create bar code
Since the FAB's now always above the keyboard, there's no
need for an extra action item.
2020-06-29 19:47:27 +02:00
Markus Fisch e4863bad4c Keep soft keyboard from repositioning the layout
Only important for the EncodingFragment.
2020-06-29 19:46:52 +02:00
Markus Fisch 484bbbc997 Add changelog for latest version for f-droid 2020-06-23 21:38:07 +02:00
Markus Fisch 565643f19e Advance version number to 1.32.0 2020-06-23 21:31:37 +02:00
Markus Fisch 4c92045563 Clean up permission handling 2020-06-23 21:12:38 +02:00
Markus Fisch 7116aa7278 Add an action to remove a scan from result view
So there's no need for a long tap to remove individual scans.
2020-06-23 21:10:21 +02:00
Markus Fisch 860e867ad4 Fix restoring crop handle position 2020-06-23 16:57:16 +02:00
Markus Fisch 3b509cd09c Make tests fail if there are no/wrong samples 2020-06-22 14:54:53 +02:00
Markus Fisch 51ec228fd8 Remove unused quantities from translations 2020-06-22 14:35:48 +02:00
Markus Fisch 9d1dec146d Add bulk mode
Scan continuously to add items to the history (if enabled)
instead of showing the result screen.
2020-06-22 14:30:51 +02:00
Markus Fisch 7bd9fe36fe Handle storing/restoring crop handle like zoom 2020-06-22 13:21:40 +02:00
Markus Fisch 85dba0604c Make ROI handle position permanent
So a user doesn't have to restore it every time.
2020-06-22 11:17:38 +02:00
Markus Fisch 56c70f5974 Make default ROI bigger and always a square 2020-06-22 10:27:31 +02:00
Markus Fisch 098b7740a0 Give ROI handle precedence over other controls
Because it can easily overlap with other controls and would
then be inaccessible.
2020-06-22 10:19:42 +02:00
Markus Fisch 7d92481fa6 Update ROI immediately when handle is tapped 2020-06-22 10:09:18 +02:00
zmniandGitHub 2e285a5b1d Update Indonesian translation (#123) 2020-06-21 16:36:45 +02:00
Markus Fisch ad25c4681f Calculate default ROI according to orientation
So it's probably a better fit.
2020-06-18 17:08:36 +02:00
Markus Fisch c00c87ceca Add missing changelog files for f-droid 2020-06-18 09:55:08 +02:00
Markus Fisch e001a92870 Run detection in pick activity in background too
Because the TRY_HARDER flag is always set when scanning still
images, ZXing may take quite some time on slow devices. So it's
better to run it in a background thread.
2020-06-15 16:53:03 +02:00
Markus Fisch 7a32b00e1f Add a menu item to export generated barcode as SVG
And use clear icons for PNG and SVG export.
2020-06-11 16:04:00 +02:00
Markus Fisch 50cedf782d Automatically convert JPEG test samples to YUV
And convert the existing test samples from YUV to JPEG.

JPEGs are much smaller and it's much easier to work with them.
2020-06-10 14:28:31 +02:00
Markus Fisch fb0d22198f Refactor rotator to just rotate
To match the intrinsic names.

Also move the code into the same namespace as Processor so
we don't need the explicit import.
2020-06-09 15:20:33 +02:00
Markus Fisch af528df448 Advance version number to 1.31.0 2020-06-07 16:19:23 +02:00
Markus Fisch a9b5771d57 Fix initializing preprocessor for device tests 2020-06-07 16:19:08 +02:00
Markus Fisch a161e8fc0f Remove app name from polish translation
Because the app name isn't part of the default locale but the
same for all languages. Lint will fail otherwise.
2020-06-07 16:07:54 +02:00
Markus Fisch ec17d07695 Reset ROI when handle snaps back in place
And make sure ROI dimensions are never smaller than 4.
2020-06-07 15:51:53 +02:00
Karol KosekandGitHub be2fc2f559 Add Polish translation 2020-06-07 15:19:00 +02:00
Markus Fisch de3aa752ff Refactor data namespace to database
It's better to be explicit.
2020-06-04 09:19:50 +02:00
Markus Fisch 411cbb243f Clean up app initialization
This got a bit mixed up by all the special RenderScript handling.
2020-06-04 09:13:33 +02:00
Markus Fisch f76c1521fe Stop detection when ROI is modified
Avoids accidently detecting a barcode while a user is about
to change the region of interest.
2020-06-03 22:49:01 +02:00
Markus Fisch 9b9224b100 Optimize crop button drawable with avocado 2020-06-03 22:39:42 +02:00
Markus Fisch 62d3fd7e94 Advance version number to 1.30.1 2020-06-02 21:48:56 +02:00
Markus Fisch acdac4ca30 Improve german translation for ROI handle setting 2020-06-02 17:36:19 +02:00
Markus Fisch 221cf11576 Convert ROI handle positions to integer points
Because they're always discrete values and can never be inbetween.

Also, calling `drawBitmap()` with float values can lead to slightly
distorted renderings because the image is interpolated between
pixels then.

So let's just avoid this by using integers.
2020-06-02 16:04:39 +02:00
Markus Fisch d34fceb48f Move ROI button back in place if ROI is too small
It's probably better to restore the original situation than
having the button stay where it is.

Also clean up DetectorView a bit and refactor some variables.
2020-06-02 09:59:18 +02:00
Markus Fisch 4cc4cef4ca Show plus in FAB if flash is not available
So it doesn't look awkward when a device doesn't have a flash.
2020-06-01 22:49:25 +02:00
Markus Fisch 969ffd3d0a Move ROI button above FAB
Looks tidier.
2020-06-01 22:48:42 +02:00
Markus Fisch 81a505c752 Refactor Candidates to Dots
The dots aren't just candidates but may already be result points
so this name doesn't really fit.
2020-06-01 20:04:06 +02:00
Markus Fisch eafcf0dba6 Rename ic_crop_handle to button_crop
It's not an icon but a button.
2020-06-01 19:58:29 +02:00
zmniandGitHub 0539468d3d Update Indonesian translation 2020-06-01 19:48:09 +02:00
Markus Fisch 5e9361ac77 Keep ROI over orientation changes 2020-06-01 10:44:48 +02:00
Markus Fisch 54fb31d6a5 Show a default ROI when button is just tapped
Nobody knows the button is a handle that can be used.

So the natural thing to do is to tap on it which should do the
next best thing and that is showing a default region of interest.

Maybe the button should be positioned above the fab and have a
different color when inactive.
2020-05-31 22:11:43 +02:00
Markus Fisch 0f7da91e77 Draw round corners for ROI box
Looks much better.
2020-05-31 19:29:00 +02:00
Markus Fisch 13bbb3e435 Update build tools and gradle wrapper 2020-05-31 13:15:46 +02:00
Markus Fisch e318385032 Advance version number to 1.30.0 2020-05-30 14:42:17 +02:00
Markus Fisch 9b1445efb5 Draw a dotted rectangle around ROI below Kitkat
Because Canvas.clipRect() doesn't work very well below
Kitkat.
2020-05-30 14:32:35 +02:00
Markus Fisch 9a74098f11 Commit tryHarder and autoRotate instead of apply
It's important those values are updated before returning to the
CameraActivity. `apply()` saves preferences asynchronously what
can be too late.
2020-05-30 13:46:52 +02:00
Markus Fisch e22d242248 Add a setting to show/hide crop handle
In case somebody doesn't like/want this feature.
2020-05-30 13:40:01 +02:00
Markus Fisch 77d419aa7b Refactor axis to center in DetectorView
And name all crop handle variables just handle.
2020-05-30 13:22:05 +02:00
Markus Fisch b2fbdceee4 Put crop handle to center bottom for portait
And to center right in landscape.
2020-05-30 12:40:23 +02:00
Markus Fisch 8a625dc5d0 Fix malformed XML of spanish translation 2020-05-30 12:40:11 +02:00
Markus Fisch 6d9c6aa53f Snap crop area for cancelled touches too
And make handlePos private because it's not used from the outside yet.
2020-05-30 12:37:29 +02:00
Markus Fisch 6b03935b9a Add a drop shadow to crop handle
To give it a similar look like the FAB.
2020-05-30 12:34:32 +02:00
Markus Fisch fcac0add8b Add a handle to define a region of interest
And scan only within that region.

Helps to scan only a certain barcode when there are a lot
of barcodes very close together.
2020-05-29 21:02:45 +02:00
Markus Fisch f63b78cc28 Remove insignificant inline argument
Because "inlining works best for functions with parameters
of functional types".
2020-05-29 09:03:50 +02:00
Diego SanguniettiandGitHub f34194c5b5 Add spanish translation 2020-05-29 08:57:24 +02:00
Markus Fisch f2a22d2312 Keep camera selection over orientation changes
And save the facing flag in the state bundle too.
2020-05-28 22:24:56 +02:00
Markus Fisch 746bcc0706 Simplify generating hex dump
To export binary data.
2020-05-27 22:29:27 +02:00
Markus Fisch 2525b693c7 Add support for VCALENDAR types
Treat VCALENDAR like VEVENT types as VCALENDAR is just a wrapper
around a VEVENT.

Also add a few simple tests for VCARD, VCALENDAR and VEVENT to
make sure everything works as expected.
2020-05-27 22:23:00 +02:00
Markus Fisch ba9970eb4d Vibrate only if activity isn't finishing
With Gesture Navigation, a back may also move the image a bit
what will cause the view to redraw and to vibrate even if the
activity is already finishing.
2020-05-24 15:37:44 +02:00
Markus Fisch 0495985518 Add copy to clipboard button to context menu
So it's easy to quickly copy something from the history.
2020-05-22 19:18:33 +02:00
Markus Fisch b662ba9307 Enable/disable menu items in history list
Depending on if there are scans visible or not.
2020-05-22 19:04:47 +02:00
Markus Fisch c4fe713b6d Check for null before using last list child 2020-05-22 18:55:13 +02:00
Markus Fisch 3609c377d2 Always remove current listing in history only
When the list is filtered, the remove all button should only
affect those records.
2020-05-22 18:53:11 +02:00
Markus Fisch 8043ab7eff Advance version number to 1.29.0 2020-05-22 13:39:41 +02:00
Markus Fisch 4f7b392611 Add a note about using the NDK and RenderScript
At the time of writing, it's important to _not_ have the NDK available
when compiling the custom rotation kernel. This will produce a broken
build for some ARMv7 devices running Android 6 (e.g. One Plus X,
Yotaphone 2, Moto E) while it works for newer Android versions.
2020-05-19 22:12:19 +02:00
Markus Fisch 27fb611f4e Add a note about scanning 2D barcodes with ZXing 2020-05-19 22:07:35 +02:00
Markus Fisch 3c693c7bd5 Rotate every other frame if auto rotate is set
Add a new setting that will automatically rotate every other frame
by 90 degrees. This makes it possible to read vertical 1D barcodes
too. ZXing can read 1D barcodes horizontally only.

The setting is unset by default because it makes recognition of 1D
barcodes a tiny bit slower since half of the frames are now useless
for ZXing (because the barcode is in the wrong orientation on them).

On a fast device, this is hardly noticeable. But on a low-end device
it makes a slight difference which is why the setting is unset by
default.
2020-05-19 20:21:27 +02:00
Markus Fisch b2cc1c3c34 Move posting result into its on function
Easier to read.
2020-05-15 13:46:08 +02:00
Markus Fisch e2effb613f Add a rule to dump meminfo of debug version 2020-05-15 13:38:51 +02:00
199 changed files with 5304 additions and 2244 deletions
+117
View File
@@ -1,5 +1,122 @@
# Change Log
## 1.43.0
* Add a preference item to remove WiFi network suggestions
* Update Traditional Chinese Translation
* Update Russian language
## 1.42.0
* Add a setting to pick a default search engine
* Update Traditional Chinese
* Update Dutch translation
## 1.41.1
* Update Bengali translation
* Update Simplified Chinese
## 1.41.0
* Use cropping limiter by default for new installations
* Truncate all toasts with strings from the outside
* Add Bangla translation
* Add missing Japanese locale
* Update Hungarian translation
* Update Russian app description
## 1.40.0
* Add a setting to show/hide scanned data when scanning continuously
* Only show the first 50 characters when scanning continuously
* Share binary data as hex dump
* Update Russian language
## 1.39.0
* Fix adding contacts and calendar entries on Android 11+
* Add a setting to always start in bulk mode
* Add Czech translation
* Update Spanish translation
* Update Czech translation
## 1.38.0
* Add a quick settings tile to scan from settings
* Make custom background requests time out after 5 seconds
* Add Japanese translation
* Updated French translation
## 1.37.0
* Improve formatting date and time in history listing
* Add haptic and audible feedback for background request errors
## 1.36.0
* Add a setting to automatically return to scanning screen after copying
* Add a button to test the background request
* Show HTTP response for background requests
* Improve SVG export
* Automatically continue with an action after granting a permission
* Update Chinese translation
* Update French translation
* Update Indonesian translation
* Update Turkish translation
* Update Ukrainian translation
## 1.35.0
* Put images with transparent pixels on white/black
* Use cropping limiter when scanning from images too
* Support setting the error correction level when creating QR Codes
* Support setting a custom deviating locale
* Use adaptive icons for shortcut icons
* Add exporting barcodes as plain text
* Add Ukrainian translation
* Add Turkish translation
* Add Georgian translation
* Update Indonesian translation
* Update Russian translation
* Update Italian translation
* Update Simplified Chinese translation
## 1.34.0
* Use Material Design settings with headers
* Add a setting to choose the message type when forwarding scanned content
## 1.33.0
* Add a setting to automatically send the scanned content to a custom URL
* Add a setting to automatically put contents into clipboard
* Add an explicit button to copy WiFi password into clipboard
* Update Indonesian translation
## 1.32.1
* Alternatively remove region of interest by dragging handle to a screen corner
* Restrict region of interest to screen space
* Remove extra action item to create barcode
## 1.32.0
* Add bulk mode
* Remember region of interest permanently
* Export generated barcode as SVG
* Run detection on images in background
* Remove a scan from result view
* Update Indonesian translation
## 1.31.0
* Stop detection while region of interest is modified
* Fix resetting region of interest
* Add Polish translation
## 1.30.1
* Improve usability of cropping limiter
* Draw round corners around region of interest
* Update Indonesian translation
## 1.30.0
* Add a handle to define a region of interest
* Add a setting to show/hide cropping limiter
* Add support for VCALENDAR types
* Add spanish translation
* Add copy to clipboard button to context menu in history listing
* Make history actions work on current listing only
* Keep camera selection over orientation changes
## 1.29.0
* Add a setting to enable reading of vertical 1D barcodes
## 1.28.0
* Share history as CSV and JSON too
* Export history in JSON format
+3
View File
@@ -33,6 +33,9 @@ start:
uninstall:
adb $(TARGET) uninstall $(PACKAGE).debug
meminfo:
adb shell dumpsys meminfo $(PACKAGE).debug
images:
svg/update.sh
+47
View File
@@ -4,3 +4,50 @@ Binary Eye does not collect any personal information.
If you have any questions or suggestions about my Privacy Policy, do not
hesitate to contact me.
## Permissions
### [ACCESS_FINE_LOCATION](https://developer.android.com/reference/android/Manifest.permission#ACCESS_FINE_LOCATION)
Required to add a WiFi network from a barcode (usually a QR Code).
Before Android Q, this feature requires using
[WifiManager.getConfiguredNetworks][configuredNetworks]
which requires `ACCESS_FINE_LOCATION`.
On Android Q an better, this permission is not requested nor required.
### [CAMERA](https://developer.android.com/reference/android/Manifest.permission#CAMERA)
Required to read a barcode from the camera image.
### [INTERNET](https://developer.android.com/reference/android/Manifest.permission#INTERNET)
Required to send a scanned code to a custom URL you can specify in settings.
### [VIBRATE](https://developer.android.com/reference/android/Manifest.permission#VIBRATE)
Required to vibrate on detection.
### [WRITE_EXTERNAL_STORAGE](https://developer.android.com/reference/android/Manifest.permission#WRITE_EXTERNAL_STORAGE)
Required to save generated barcodes as files before Android Q.
### [CHANGE_WIFI_STATE](https://developer.android.com/reference/android/Manifest.permission#CHANGE_WIFI_STATE)
Required to add a WiFi network from a barcode (usually a QR Code).
On Android Q and better, this feature requires using
[WifiManager.addNetworkSuggestions][addNetworkSuggestions]
which requires `CHANGE_WIFI_STATE`.
### [ACCESS_WIFI_STATE](https://developer.android.com/reference/android/Manifest.permission#ACCESS_FINE_LOCATION)
Required to add a WiFi network from a barcode (usually a QR Code).
Before Android Q, this feature requires using
[WifiManager.getConfiguredNetworks][configuredNetworks]
which requires `ACCESS_WIFI_STATE`.
[configuredNetworks]: https://developer.android.com/reference/android/net/wifi/WifiManager#getConfiguredNetworks()
[addNetworkSuggestions]: https://developer.android.com/reference/android/net/wifi/WifiManager#addNetworkSuggestions(java.util.List%3Candroid.net.wifi.WifiNetworkSuggestion%3E)
+14 -16
View File
@@ -12,9 +12,14 @@ library.
## Screenshots
![Screenshot Scanning](fastlane/metadata/android/en-US/images/phoneScreenshots/screencap-scanning.png)
![Screenshot Result](fastlane/metadata/android/en-US/images/phoneScreenshots/screencap-decoded.png)
![Screenshot Compose Barcode](fastlane/metadata/android/en-US/images/phoneScreenshots/screencap-compose-barcode.png)
<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/screencap-scanning.png"
alt="Screenshot Gallery" width="160"/>
<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/screencap-scanning-cropped.png"
alt="Screenshot Gallery" width="160"/>
<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/screencap-decoded.png"
alt="Screenshot Theme" width="160"/>
<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/screencap-compose-barcode.png"
alt="Screenshot Editor" width="160"/>
## Download
@@ -79,21 +84,14 @@ be thrown and the only option is to restart the app, this time with calling
Calling `RenderScript.forceCompat()` means the processing is done in
software so you probably don't want to enable it by default.
### macOS Catalina
### 2D barcodes
At the time of writing, calling `RenderScript.forceCompat()` in a build made
on macOS Catalina with build tools 29.0.3 will crash the app. This is a bug
in the build tools for Catalina.
If you want to fork this and are only interested in reading 2D barcodes
(like QR or Aztec), you may want to remove the custom rotation kernel
altogether as ZXing can read 2D barcodes in any orientation.
So *do not* build release builts on macOS Catalina with build tools 29.0.3.
Building on Linux with 29.0.3 works fine.
### App Bundle
At the time of writing, `./gradlew bundleRelease` will produce a broken
App Bundle without the necessary RenderScript libraries. This is another
bug in the build tools.
This will make your app a bit simpler and saves you from compiling a
custom RenderScript kernel for each architecture you want to support.
[play]: https://play.google.com/store/search?q=barcode%20scanner&c=apps
[zxing]: https://github.com/zxing/zxing
+6 -3
View File
@@ -9,13 +9,14 @@ android {
minSdkVersion 9
targetSdkVersion sdk_version
versionCode 64
versionName '1.28.0'
versionCode 83
versionName '1.43.0'
// it's recommended to set this value to the lowest API level
// able to provide all the functionality
renderscriptTargetApi 24
renderscriptSupportModeEnabled true
renderscriptSupportModeBlasEnabled true
testInstrumentationRunner 'android.support.test.runner.AndroidJUnitRunner'
}
@@ -70,9 +71,11 @@ dependencies {
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.2"
implementation "com.android.support:appcompat-v7:$support_version"
implementation "com.android.support:design:$support_version"
implementation "com.android.support:preference-v7:$support_version"
implementation "com.android.support:preference-v14:$support_version"
// Do NOT upgrade ZXing because newer versions won't work on API < 24
// https://github.com/zxing/zxing/issues/1170
implementation 'com.google.zxing:core:3.3.3'
implementation 'com.github.markusfisch:CameraView:1.9.1'
implementation 'com.github.markusfisch:ScalingImageView:1.2.0'
implementation 'com.github.markusfisch:ScalingImageView:1.3.0'
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,14 +1,15 @@
package de.markusfisch.android.binaryeye
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.support.test.InstrumentationRegistry
import android.support.test.runner.AndroidJUnit4
import android.support.v8.renderscript.RenderScript
import de.markusfisch.android.binaryeye.rs.Preprocessor
import de.markusfisch.android.binaryeye.zxing.Zxing
import org.junit.Assert.fail;
import org.junit.Test
import org.junit.runner.RunWith
import java.util.regex.Pattern
@RunWith(AndroidJUnit4::class)
@@ -19,33 +20,89 @@ class PreprocessorTest {
val assets = InstrumentationRegistry.getInstrumentation()
.context.assets
val pattern = Pattern.compile(
"[0-9]+-([0-9]+)x([0-9]+)-([0-9]+)deg.yuv"
"[0-9]+-([0-9]+)deg.jpg"
)
val files = assets.list("yuv") ?: return
for (file in files) {
val m = pattern.matcher(file)
if (!m.find() || m.groupCount() < 3) {
continue
val samples = assets.list("samples")
checkNotNull(samples) { "no samples found" }
for (sample in samples) {
val m = pattern.matcher(sample)
if (!m.find() || m.groupCount() < 1) {
fail("invalid sample: $sample")
}
val frameWidth = m.group(1) ?: return
val frameHeight = m.group(2) ?: return
val frameOrientation = m.group(3) ?: return
val frameData = assets.open("yuv/$file").readBytes()
val preprocessor = Preprocessor(
val bitmap = BitmapFactory.decodeStream(
assets.open("samples/$sample")
)
val frameData = getNV21(bitmap)
val frameWidth = bitmap.width
val frameHeight = bitmap.height
val frameOrientation = m.group(1) ?: return
val rs = RenderScript.create(
InstrumentationRegistry.getTargetContext(),
frameWidth.toInt(),
frameHeight.toInt(),
frameOrientation.toInt()
)
preprocessor.process(frameData)
val result = zxing.decode(
frameData,
preprocessor.outWidth,
preprocessor.outHeight
val preprocessor = Preprocessor(
rs,
frameWidth,
frameHeight,
null
)
val outWidth: Int
val outHeight: Int
val orientation = frameOrientation.toInt()
if (orientation == 90 || orientation == 270) {
preprocessor.resizeAndRotate(frameData)
outWidth = preprocessor.outHeight
outHeight = preprocessor.outWidth
} else {
preprocessor.resizeOnly(frameData)
outWidth = preprocessor.outWidth
outHeight = preprocessor.outHeight
}
val result = zxing.decode(frameData, outWidth, outHeight)
preprocessor.destroy()
rs.destroy()
checkNotNull(result) { "no barcode found in $file" }
checkNotNull(result) { "no barcode found in $sample" }
}
}
}
fun getNV21(bitmap: Bitmap): ByteArray {
val width = bitmap.width
val height = bitmap.height
val argb = IntArray(width * height)
bitmap.getPixels(argb, 0, width, 0, 0, width, height)
return encodeYUV420SP(argb, width, height)
}
private fun encodeYUV420SP(argb: IntArray, width: Int, height: Int): ByteArray {
val yuv = ByteArray(ceilIfUneven(width) * ceilIfUneven(height) * 3 / 2)
var yIndex = 0
var uvIndex = width * height
var r: Int
var g: Int
var b: Int
var Y: Int
var u: Int
var v: Int
var index = 0
for (y in 0 until height) {
for (x in 0 until width) {
val pixel = argb[index]
r = pixel and 0xff0000 shr 16
g = pixel and 0xff00 shr 8
b = pixel and 0xff
Y = (66 * r + 129 * g + 25 * b + 128 shr 8) + 16
u = (-38 * r - 74 * g + 112 * b + 128 shr 8) + 128
v = (112 * r - 94 * g - 18 * b + 128 shr 8) + 128
yuv[yIndex++] = (if (Y < 0) 0 else if (Y > 255) 255 else Y).toByte()
if (y % 2 == 0 && index % 2 == 0) {
yuv[uvIndex++] = (if (v < 0) 0 else if (v > 255) 255 else v).toByte()
yuv[uvIndex++] = (if (u < 0) 0 else if (u > 255) 255 else u).toByte()
}
++index
}
}
return yuv
}
private fun ceilIfUneven(n: Int) = if (n and 1 == 1) n + 1 else n
+22
View File
@@ -7,15 +7,28 @@
android:xlargeScreens="true"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.CAMERA"/>
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.VIBRATE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE"/>
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
<uses-feature android:name="android.hardware.camera"/>
<uses-feature android:name="android.hardware.camera.autofocus"/>
<uses-sdk tools:overrideLibrary="android.support.v14.preference"/>
<queries>
<intent>
<action android:name="android.intent.action.VIEW"/>
<data android:scheme="https"/>
</intent>
<intent>
<action android:name="android.intent.action.VIEW"/>
<data android:scheme="http"/>
</intent>
</queries>
<application
tools:ignore="UnusedAttribute"
android:name=".app.BinaryEyeApp"
android:usesCleartextTraffic="true"
android:allowBackup="true"
android:fullBackupContent="@xml/backup_rules"
android:supportsRtl="true"
@@ -76,6 +89,15 @@
android:name=".activity.MainActivity"
android:label="@string/app_name"
android:exported="true"/>
<service
android:name=".service.ScanTileService"
android:icon="@drawable/ic_action_scan"
android:label="@string/scan_code"
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
<intent-filter>
<action android:name="android.service.quicksettings.action.QS_TILE"/>
</intent-filter>
</service>
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="${applicationId}.provider"
@@ -2,8 +2,8 @@ package de.markusfisch.android.binaryeye.actions
import android.content.Context
import android.content.Intent
import de.markusfisch.android.binaryeye.app.execShareIntent
import de.markusfisch.android.binaryeye.app.parseAndNormalizeUri
import de.markusfisch.android.binaryeye.content.execShareIntent
import de.markusfisch.android.binaryeye.widget.toast
interface IAction {
@@ -21,7 +21,7 @@ abstract class IntentAction : IAction {
val intent = createIntent(context, data) ?: return context.toast(
errorMsg
)
execShareIntent(context, intent)
context.execShareIntent(intent)
}
abstract suspend fun createIntent(context: Context, data: ByteArray): Intent?
@@ -43,6 +43,6 @@ abstract class SchemeAction : IAction {
final override suspend fun execute(context: Context, data: ByteArray) {
val uri = parseAndNormalizeUri(String(data))
execShareIntent(context, Intent(intentAction, uri))
context.execShareIntent(Intent(intentAction, uri))
}
}
@@ -5,10 +5,9 @@ import android.content.Intent
import de.markusfisch.android.binaryeye.R
import de.markusfisch.android.binaryeye.actions.IAction
import de.markusfisch.android.binaryeye.app.alertDialog
import de.markusfisch.android.binaryeye.app.execShareIntent
import de.markusfisch.android.binaryeye.app.parseAndNormalizeUri
import de.markusfisch.android.binaryeye.app.prefs
import de.markusfisch.android.binaryeye.widget.toast
import de.markusfisch.android.binaryeye.content.startIntent
import java.net.URLEncoder
object OpenOrSearchAction : IAction {
@@ -18,28 +17,36 @@ object OpenOrSearchAction : IAction {
override fun canExecuteOn(data: ByteArray): Boolean = false
override suspend fun execute(context: Context, data: ByteArray) {
val intent = openUri(context, String(data)) ?: return
execShareIntent(context, intent)
view(context, String(data), true)
}
private suspend fun openUri(context: Context, data: String, search: Boolean = true): Intent? {
val uri = parseAndNormalizeUri(data)
val intent = Intent(Intent.ACTION_VIEW, uri)
when {
intent.resolveActivity(context.packageManager) != null -> return intent
search -> return getSearchIntent(context, data)
else -> context.toast(R.string.cannot_resolve_action)
private suspend fun view(context: Context, s: String, search: Boolean) {
val intent = Intent(Intent.ACTION_VIEW, parseAndNormalizeUri(s))
if (!context.startIntent(intent) && search) {
openSearch(context, s)
}
return null
}
private suspend fun getSearchIntent(context: Context, query: String): Intent? {
private suspend fun openSearch(context: Context, query: String) {
val defaultSearchUrl = prefs.defaultSearchUrl
if (defaultSearchUrl.isNotEmpty()) {
view(
context,
defaultSearchUrl + URLEncoder.encode(query, "utf-8"),
false
)
return
}
val names = context.resources.getStringArray(
R.array.search_engines_names
).toMutableList()
val urls = context.resources.getStringArray(
R.array.search_engines_values
).toMutableList()
// Remove the "Always ask" entry. The arrays search_engines_*
// are used in the preferences too.
names.removeFirst()
urls.removeFirst()
if (prefs.openWithUrl.isNotEmpty()) {
names.add(prefs.openWithUrl)
urls.add(prefs.openWithUrl)
@@ -49,7 +56,7 @@ object OpenOrSearchAction : IAction {
setItems(names.toTypedArray()) { _, which ->
resume(urls[which] + URLEncoder.encode(query, "utf-8"))
}
} ?: return null
return openUri(context, queryUri, false)
} ?: return
view(context, queryUri, false)
}
}
@@ -6,17 +6,25 @@ object VTypeParser {
private const val BACKSLASH_R_LEGACY =
"(?:\u000D\u000A|[\u000A\u000B\u000C\u000D\u0085\u2028\u2029])"
private val vTypeRegex =
"""^BEGIN:(VCARD|VEVENT)(?:$BACKSLASH_R_LEGACY.+?:[\s\S]+?)+?${BACKSLASH_R_LEGACY}END:\1$BACKSLASH_R_LEGACY?$""".toRegex(
"""^BEGIN:(V.+)(?:$BACKSLASH_R_LEGACY.+?:[\s\S]+?)+?${BACKSLASH_R_LEGACY}END:\1$BACKSLASH_R_LEGACY?$""".toRegex(
RegexOption.IGNORE_CASE
)
private val propertyRegex = """^(.+?):([\s\S]*?)$""".toRegex(RegexOption.MULTILINE)
private val propertyRegex = """^(.+?):([\s\S]*?)$""".toRegex(
RegexOption.MULTILINE
)
fun parseVType(data: String): String? =
vTypeRegex.matchEntire(data)?.groupValues?.get(1)?.toUpperCase(Locale.US)
fun parseVType(data: String): String? = vTypeRegex.matchEntire(
data
)?.groupValues?.get(1)?.toUpperCase(Locale.US)
fun parseMap(data: String): Map<String, List<VTypeProperty>> = propertyRegex.findAll(data).map {
fun parseMap(
data: String
): Map<String, List<VTypeProperty>> = propertyRegex.findAll(data).map {
it.groupValues[1].split(';').let { typeAndInfo ->
typeAndInfo[0] to VTypeProperty(typeAndInfo.drop(1), it.groupValues[2])
typeAndInfo[0] to VTypeProperty(
typeAndInfo.drop(1),
it.groupValues[2]
)
}
}.groupBy({ it.first.toUpperCase(Locale.US) }) { it.second }
}
@@ -21,8 +21,11 @@ object VEventAction : IntentAction() {
get() = R.string.vevent_failed
override fun canExecuteOn(data: ByteArray): Boolean {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH
&& VTypeParser.parseVType(String(data)) == "VEVENT"
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
return false
}
val type = VTypeParser.parseVType(String(data))
return type == "VEVENT" || type == "VCALENDAR"
}
@RequiresApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
@@ -53,7 +56,8 @@ object VEventAction : IntentAction() {
}
}
@SuppressLint("SimpleDateFormat") // we definitely don't wan't the local format
// We definitely don't wan't the local format.
@SuppressLint("SimpleDateFormat")
private val dateFormats = listOf(
SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"),
SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss"),
@@ -10,6 +10,12 @@ import android.support.annotation.RequiresApi
import de.markusfisch.android.binaryeye.R
import java.util.*
@RequiresApi(Build.VERSION_CODES.Q)
fun clearNetworkSuggestions(context: Context): Int =
(context.applicationContext.getSystemService(
Context.WIFI_SERVICE
) as WifiManager).removeNetworkSuggestions(listOf())
/**
* Normal:
* WIFI:S:[network SSID];T:<WPA|WEP|nopass|>;P:[network password];H:<true|false|>;;
@@ -45,7 +51,7 @@ object WifiConnector {
invoke(parsedData.password)
}
return if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
// WifiConfiguration is deprecated in Android Q
// WifiConfiguration is deprecated in Android Q.
@Suppress("DEPRECATION")
WifiConfiguration().apply(parsedData)
} else {
@@ -58,7 +64,7 @@ object WifiConnector {
Context.WIFI_SERVICE
) as WifiManager
return if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
// WifiConfiguration is deprecated in Android Q
// WifiConfiguration is deprecated in Android Q.
@Suppress("DEPRECATION")
val wifiConfig = config as WifiConfiguration
if (wifiManager.enableWifi() &&
@@ -72,7 +78,7 @@ object WifiConnector {
} else {
val suggestion = (config as WifiNetworkSuggestion.Builder).build()
val suggestions = listOf(suggestion)
// remove previous conflicting network suggestion
// Remove previous conflicting network suggestion.
wifiManager.removeNetworkSuggestions(suggestions)
val result = wifiManager.addNetworkSuggestions(suggestions)
if (result == WifiManager.STATUS_NETWORK_SUGGESTIONS_SUCCESS) {
@@ -84,11 +90,11 @@ object WifiConnector {
}
internal fun parseMap(string: String): Map<String, String>? {
// normally those codes should have the last semicolon, but many
// generators don't add it
// Normally those codes should have the last semicolon, but many
// generators don't add it.
val wifiRegex = """^WIFI:((?:.+?:(?:[^\\;]|\\.)*;)+);?$""".toRegex()
// should be: ^(.+):((?:[^\\;,":]|\\.)*);$ but allows unescaped , "
// and : because many QR Code creators don't escape properly
// Should be: ^(.+):((?:[^\\;,":]|\\.)*);$ but allows unescaped , "
// and : because many QR Code creators don't escape properly.
val pairRegex = """(.+?):((?:[^\\;]|\\.)*);""".toRegex()
return wifiRegex.matchEntire(
string
@@ -102,7 +108,7 @@ object WifiConnector {
internal class SimpleDataAccessor private constructor(
private val inputMap: Map<String, String>
) {
// this should be private but because of testing it isn't possible
// This should be private but because of testing it isn't possible.
internal val ssid: String
get() = inputMap.getValue("S").unescape
internal val securityType: String
@@ -212,7 +218,7 @@ object WifiConnector {
return this.applyCommon(data)?.applySecurity(data)
}
// WifiConfiguration is deprecated in Android Q
// WifiConfiguration is deprecated in Android Q.
@Suppress("DEPRECATION")
private fun WifiConfiguration.apply(
data: SimpleDataAccessor
@@ -274,7 +280,7 @@ object WifiConnector {
allowedAuthAlgorithms.set(WifiConfiguration.AuthAlgorithm.OPEN)
allowedProtocols.set(WifiConfiguration.Protocol.RSN) // WPA2
allowedKeyManagement.set(WifiConfiguration.KeyMgmt.WPA_EAP)
@Suppress("DEPRECATION") // TKIP is insecure and has bad performance
@Suppress("DEPRECATION") // TKIP is insecure and has bad performance.
allowedPairwiseCiphers.set(WifiConfiguration.PairwiseCipher.TKIP)
allowedPairwiseCiphers.set(WifiConfiguration.PairwiseCipher.CCMP)
allowedGroupCiphers.set(WifiConfiguration.GroupCipher.TKIP)
@@ -300,8 +306,8 @@ object WifiConnector {
}
}
// keep possibility of wrongly unescaped \ by explicitly searching
// for special chars
// Keep possibility of wrongly unescaped \ by explicitly searching
// for special chars.
private val escapedRegex = """\\([\\;,":])""".toRegex()
private val String.unescape: String
get() = this.replace(escapedRegex) { escaped ->
@@ -327,12 +333,12 @@ private fun String.isHex() = length == 64 && matches(hexRegex)
private fun WifiManager.enableWifi(): Boolean {
// setWifiEnabled() will always return false for Android Q
// because Q doesn't allow apps to enable/disable Wi-Fi anymore
// because Q doesn't allow apps to enable/disable Wi-Fi anymore.
@Suppress("DEPRECATION")
return isWifiEnabled || setWifiEnabled(true)
}
// WifiConfiguration is deprecated in Android Q
// WifiConfiguration is deprecated in Android Q.
@Suppress("DEPRECATION")
private fun WifiManager.removeOldNetwork(
wifiConfig: WifiConfiguration
@@ -345,13 +351,13 @@ private fun WifiManager.removeOldNetwork(
removeNetwork(it)
}
} catch (e: SecurityException) {
// the user didn't allow ACCESS_FINE_LOCATION which is
// required to access configuredNetworks and that's fine
// The user didn't allow ACCESS_FINE_LOCATION which is
// required to access configuredNetworks and that's fine.
}
return true
}
// WifiConfiguration is deprecated in Android Q
// WifiConfiguration is deprecated in Android Q.
@Suppress("DEPRECATION")
private fun WifiManager.enableNewNetwork(
wifiConfig: WifiConfiguration
@@ -4,13 +4,19 @@ import android.app.Activity
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.graphics.Rect
import android.graphics.RectF
import android.hardware.Camera
import android.media.AudioManager
import android.media.ToneGenerator
import android.net.Uri
import android.os.Bundle
import android.os.Vibrator
import android.support.design.widget.FloatingActionButton
import android.support.v7.app.AppCompatActivity
import android.support.v7.widget.Toolbar
import android.support.v8.renderscript.RSRuntimeException
import android.support.v8.renderscript.RenderScript
import android.view.Menu
import android.view.MenuItem
import android.view.MotionEvent
@@ -18,46 +24,58 @@ import android.view.View
import android.widget.SeekBar
import com.google.zxing.Result
import com.google.zxing.ResultMetadataType
import com.google.zxing.ResultPointCallback
import de.markusfisch.android.binaryeye.R
import de.markusfisch.android.binaryeye.app.*
import de.markusfisch.android.binaryeye.data.Scan
import de.markusfisch.android.binaryeye.content.copyToClipboard
import de.markusfisch.android.binaryeye.content.execShareIntent
import de.markusfisch.android.binaryeye.database.Scan
import de.markusfisch.android.binaryeye.graphics.Mapping
import de.markusfisch.android.binaryeye.graphics.frameToView
import de.markusfisch.android.binaryeye.graphics.isPortrait
import de.markusfisch.android.binaryeye.net.sendAsync
import de.markusfisch.android.binaryeye.os.error
import de.markusfisch.android.binaryeye.os.vibrate
import de.markusfisch.android.binaryeye.rs.Preprocessor
import de.markusfisch.android.binaryeye.view.initSystemBars
import de.markusfisch.android.binaryeye.view.setPaddingFromWindowInsets
import de.markusfisch.android.binaryeye.widget.DetectorView
import de.markusfisch.android.binaryeye.widget.toast
import de.markusfisch.android.binaryeye.zxing.Zxing
import de.markusfisch.android.cameraview.widget.CameraView
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlin.math.max
import kotlin.math.min
import kotlin.math.roundToInt
class CameraActivity : AppCompatActivity() {
private val zxing = Zxing(ResultPointCallback { point ->
private val zxing = Zxing { point ->
point?.let {
mapping?.map(it)?.let {
getMapping()?.map(it)?.let {
detectorView.post {
detectorView.mark(listOf(it))
}
}
}
})
}
private lateinit var rs: RenderScript
private lateinit var vibrator: Vibrator
private lateinit var cameraView: CameraView
private lateinit var detectorView: DetectorView
private lateinit var zoomBar: SeekBar
private lateinit var flashFab: View
private lateinit var flashFab: FloatingActionButton
private var preprocessor: Preprocessor? = null
private var mapping: Mapping? = null
private var nativeMapping: Mapping? = null
private var rotatedMapping: Mapping? = null
private var recreatePreprocessor = false
private var rotate = false
private var invert = false
private var flash = false
private var decoding = true
private var returnResult = false
private var frontFacing = false
private var bulkMode = prefs.bulkMode
private var ignoreNext: String? = null
private var fallbackBuffer: IntArray? = null
override fun onRequestPermissionsResult(
@@ -66,7 +84,7 @@ class CameraActivity : AppCompatActivity() {
grantResults: IntArray
) {
when (requestCode) {
REQUEST_CAMERA -> if (grantResults.isNotEmpty() &&
PERMISSION_CAMERA -> if (grantResults.isNotEmpty() &&
grantResults[0] != PackageManager.PERMISSION_GRANTED
) {
toast(R.string.no_camera_no_fun)
@@ -92,10 +110,20 @@ class CameraActivity : AppCompatActivity() {
}
}
override fun attachBaseContext(base: Context?) {
base?.applyLocale(prefs.customLocale)
super.attachBaseContext(base)
}
override fun onCreate(state: Bundle?) {
super.onCreate(state)
setContentView(R.layout.activity_camera)
// Necessary to get the right translation after setting a
// custom locale.
setTitle(R.string.scan_code)
rs = RenderScript.create(this)
vibrator = getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
initSystemBars(this)
@@ -104,12 +132,11 @@ class CameraActivity : AppCompatActivity() {
cameraView = findViewById(R.id.camera_view) as CameraView
detectorView = findViewById(R.id.detector_view) as DetectorView
zoomBar = findViewById(R.id.zoom) as SeekBar
flashFab = findViewById(R.id.flash)
flashFab.setOnClickListener { toggleTorchMode() }
flashFab = findViewById(R.id.flash) as FloatingActionButton
initCameraView()
initZoomBar()
restoreZoom()
initDetectorView()
if (intent?.action == Intent.ACTION_SEND &&
intent.type == "text/plain"
@@ -123,14 +150,21 @@ class CameraActivity : AppCompatActivity() {
resetPreProcessor()
fallbackBuffer = null
saveZoom()
saveCropHandlePos()
rs.destroy()
}
override fun onResume() {
super.onResume()
System.gc()
zxing.updateHints(prefs.tryHarder)
if (prefs.bulkMode && bulkMode != prefs.bulkMode) {
bulkMode = prefs.bulkMode
invalidateOptionsMenu()
ignoreNext = null
}
returnResult = "com.google.zxing.client.android.SCAN" == intent.action
if (hasCameraPermission(this, REQUEST_CAMERA)) {
if (hasCameraPermission(this)) {
openCamera()
}
}
@@ -160,7 +194,7 @@ class CameraActivity : AppCompatActivity() {
private fun closeCamera() {
cameraView.close()
// closing the camera will also shut off the flash
// Closing the camera will also shut off the flash.
flash = false
}
@@ -168,23 +202,37 @@ class CameraActivity : AppCompatActivity() {
super.onRestoreInstanceState(savedState)
zoomBar.max = savedState.getInt(ZOOM_MAX)
zoomBar.progress = savedState.getInt(ZOOM_LEVEL)
frontFacing = savedState.getBoolean(FRONT_FACING)
bulkMode = savedState.getBoolean(BULK_MODE)
}
override fun onSaveInstanceState(outState: Bundle) {
outState.putInt(ZOOM_MAX, zoomBar.max)
outState.putInt(ZOOM_LEVEL, zoomBar.progress)
outState.putBoolean(FRONT_FACING, frontFacing)
outState.putBoolean(BULK_MODE, bulkMode)
super.onSaveInstanceState(outState)
}
override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
// Always give crop handle precedence over other controls
// because it can easily overlap and would then be inaccessible.
if (detectorView.onTouchEvent(ev)) {
return true
}
return super.dispatchTouchEvent(ev)
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.activity_camera, menu)
menu.findItem(R.id.bulk_mode).isChecked = bulkMode
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.create -> {
startActivity(MainActivity.getEncodeIntent(this))
createBarcode()
true
}
R.id.history -> {
@@ -207,6 +255,12 @@ class CameraActivity : AppCompatActivity() {
switchCamera()
true
}
R.id.bulk_mode -> {
bulkMode = bulkMode xor true
item.isChecked = bulkMode
ignoreNext = null
true
}
R.id.preferences -> {
startActivity(MainActivity.getPreferencesIntent(this))
true
@@ -219,6 +273,10 @@ class CameraActivity : AppCompatActivity() {
}
}
private fun createBarcode() {
startActivity(MainActivity.getEncodeIntent(this))
}
private fun switchCamera() {
closeCamera()
frontFacing = frontFacing xor true
@@ -230,11 +288,7 @@ class CameraActivity : AppCompatActivity() {
Intent.ACTION_VIEW,
Uri.parse(getString(R.string.project_url))
)
if (intent.resolveActivity(packageManager) != null) {
startActivity(intent)
} else {
toast(R.string.project_url)
}
execShareIntent(intent)
}
private fun handleSendText(intent: Intent) {
@@ -276,8 +330,8 @@ class CameraActivity : AppCompatActivity() {
}
}
MotionEvent.ACTION_UP -> {
// stop calling focusTo() as soon as it returns false
// to avoid throwing and catching future exceptions
// Stop calling focusTo() as soon as it returns false
// to avoid throwing and catching future exceptions.
if (focus) {
focus = cameraView.focusTo(v, event.x, event.y)
if (focus) {
@@ -325,37 +379,27 @@ class CameraActivity : AppCompatActivity() {
}
override fun onCameraReady(camera: Camera) {
// Reset preprocessor to make sure it always fits the current
// frame orientation. Important for landscape to landscape
// orientation changes.
resetPreProcessor()
val frameWidth = cameraView.frameWidth
val frameHeight = cameraView.frameHeight
val frameOrientation = cameraView.frameOrientation
var decoding = true
// reset preprocessor to make sure it fits the current
// frame orientation; important for landscape to landscape
// orientation changes
resetPreProcessor()
ignoreNext = null
decoding = true
camera.setPreviewCallback { frameData, _ ->
if (decoding) {
val result = decodeFrame(
decodeFrame(
frameData,
frameWidth,
frameHeight,
frameOrientation
)
result?.let {
cameraView.post {
val rp = result.resultPoints
val m = mapping
if (m != null && rp != null && rp.isNotEmpty()) {
detectorView.mark(m.map(rp))
}
vibrator.vibrate()
showResult(
this@CameraActivity,
result,
returnResult
)
)?.let { result ->
if (result.text != ignoreNext) {
postResult(result)
decoding = false
}
decoding = false
}
}
}
@@ -385,6 +429,7 @@ class CameraActivity : AppCompatActivity() {
override fun onStopTrackingTouch(seekBar: SeekBar) {}
})
restoreZoom()
}
@Suppress("DEPRECATION")
@@ -396,7 +441,7 @@ class CameraActivity : AppCompatActivity() {
params.zoom = zoom
camera.parameters = params
} catch (e: RuntimeException) {
// ignore; there's nothing we can do
// Ignore. There's nothing we can do.
}
}
}
@@ -416,11 +461,40 @@ class CameraActivity : AppCompatActivity() {
)
}
private fun updateFlashFab(available: Boolean) {
flashFab.visibility = if (available) {
View.GONE
private fun initDetectorView() {
detectorView.onRoiChange = {
decoding = false
}
detectorView.onRoiChanged = {
decoding = true
recreatePreprocessor = true
}
detectorView.setPaddingFromWindowInsets()
restoreCropHandlePos()
}
private fun saveCropHandlePos() {
val pos = detectorView.getCropHandlePos()
prefs.cropHandleX = pos.x
prefs.cropHandleY = pos.y
prefs.cropHandleOrientation = detectorView.currentOrientation
}
private fun restoreCropHandlePos() {
detectorView.setCropHandlePos(
prefs.cropHandleX,
prefs.cropHandleY,
prefs.cropHandleOrientation
)
}
private fun updateFlashFab(unavailable: Boolean) {
if (unavailable) {
flashFab.setImageResource(R.drawable.ic_action_create)
flashFab.setOnClickListener { createBarcode() }
} else {
View.VISIBLE
flashFab.setImageResource(R.drawable.ic_action_flash)
flashFab.setOnClickListener { toggleTorchMode() }
}
}
@@ -438,7 +512,7 @@ class CameraActivity : AppCompatActivity() {
camera.parameters = parameters
flash = flash xor true
} catch (e: RuntimeException) {
// ignore; there's nothing we can do
// Ignore. There's nothing we can do.
}
}
}
@@ -450,26 +524,41 @@ class CameraActivity : AppCompatActivity() {
frameOrientation: Int
): Result? {
frameData ?: return null
invert = invert xor true
rotate = if (prefs.autoRotate) {
rotate xor true
} else {
invert = invert xor true
isPortrait(frameOrientation)
}
return try {
if (recreatePreprocessor) {
resetPreProcessor()
recreatePreprocessor = false
}
val pp = preprocessor ?: createPreprocessorAndMapping(
frameWidth,
frameHeight,
frameOrientation
)
pp.process(frameData)
val w: Int
val h: Int
if (rotate) {
pp.resizeAndRotate(frameData)
w = pp.outHeight
h = pp.outWidth
} else {
invert = invert xor true
pp.resizeOnly(frameData)
w = pp.outWidth
h = pp.outHeight
}
preprocessor = pp
zxing.decode(
frameData,
pp.outWidth,
pp.outHeight,
invert
)
zxing.decode(frameData, w, h, invert)
} catch (e: RSRuntimeException) {
prefs.forceCompat = prefs.forceCompat xor true
// now the only option is to restart the app because
// Now the only option is to restart the app because
// RenderScript.forceCompat() needs to be called before
// RenderScript is initialized
// RenderScript is initialized.
restartApp(this)
null
}
@@ -480,43 +569,166 @@ class CameraActivity : AppCompatActivity() {
frameHeight: Int,
frameOrientation: Int
): Preprocessor {
val pp = Preprocessor(
this,
val frameRoi = calculateFrameRect(
frameWidth,
frameHeight,
frameOrientation
)
mapping = frameToView(
val pp = Preprocessor(
rs,
frameWidth,
frameHeight,
frameRoi
)
val viewRect = if (frameRoi != null) {
detectorView.roi
} else {
cameraView.previewRect
}
nativeMapping = frameToView(
pp.outWidth,
pp.outHeight,
cameraView.previewRect
frameOrientation,
viewRect
)
rotatedMapping = frameToView(
pp.outHeight,
pp.outWidth,
(frameOrientation - 90 + 360) % 360,
viewRect
)
return pp
}
private fun calculateFrameRect(
frameWidth: Int,
frameHeight: Int,
frameOrientation: Int
): Rect? {
val viewRoi = detectorView.roi
return if (
viewRoi.width() == 0 ||
viewRoi.height() == 0
) {
null
} else {
// Map ROI in detectorView to cameraView.
val previewRect = cameraView.previewRect
// previewRect may be larger than the screen (and thus as the
// detectorView) in which case its left and/or top coordinate
// will be negative which must then be clamped to the
// screen/DetectorView.
val previewLeft = max(0, previewRect.left)
val previewTop = max(0, previewRect.top)
val previewRoi = Rect(
previewLeft + viewRoi.left,
previewTop + viewRoi.top,
previewLeft + viewRoi.right,
previewTop + viewRoi.bottom
)
val previewRectWidth = previewRect.width().toFloat()
val previewRectHeight = previewRect.height().toFloat()
val normalizedRoi = RectF(
previewRoi.left.toFloat() / previewRectWidth,
previewRoi.top.toFloat() / previewRectHeight,
previewRoi.right.toFloat() / previewRectWidth,
previewRoi.bottom.toFloat() / previewRectHeight
)
// Since the ROI is always centered and symmetric, we don't
// need to distinguish between 0 and 180 or 90 and 270 degree.
if (isPortrait(frameOrientation)) {
Rect(
(normalizedRoi.top * frameWidth.toFloat()).roundToInt(),
(normalizedRoi.left * frameHeight.toFloat()).roundToInt(),
(normalizedRoi.bottom * frameWidth.toFloat()).roundToInt(),
(normalizedRoi.right * frameHeight.toFloat()).roundToInt()
)
} else {
Rect(
(normalizedRoi.left * frameWidth.toFloat()).roundToInt(),
(normalizedRoi.top * frameHeight.toFloat()).roundToInt(),
(normalizedRoi.right * frameWidth.toFloat()).roundToInt(),
(normalizedRoi.bottom * frameHeight.toFloat()).roundToInt()
)
}
}
}
private fun postResult(result: Result) {
// Get mapping for the current rotate value.
val mapping = getMapping()
cameraView.post {
val rp = result.resultPoints
if (mapping != null && rp != null && rp.isNotEmpty()) {
detectorView.mark(mapping.map(rp))
}
vibrator.vibrate()
showResult(
this@CameraActivity,
result,
vibrator,
returnResult,
bulkMode
)
if (bulkMode) {
ignoreNext = result.text
if (prefs.showToastInBulkMode) {
toast(result.text)
}
detectorView.postDelayed({
decoding = true
}, 500)
}
}
}
private fun getMapping() = if (rotate) rotatedMapping else nativeMapping
companion object {
private const val REQUEST_CAMERA = 1
private const val PICK_FILE_RESULT_CODE = 1
private const val ZOOM_MAX = "zoom_max"
private const val ZOOM_LEVEL = "zoom_level"
private const val FRONT_FACING = "front_facing"
private const val BULK_MODE = "bulk_mode"
}
}
fun showResult(
activity: Activity,
result: Result,
isResult: Boolean = false
vibrator: Vibrator,
isResult: Boolean = false,
bulkMode: Boolean = false,
) {
val scan = Scan(result)
if (isResult) {
activity.setResult(Activity.RESULT_OK, getReturnIntent(result))
activity.finish()
} else {
if (prefs.useHistory) {
GlobalScope.launch {
db.insertScan(scan)
return
}
if (prefs.copyImmediately) {
activity.copyToClipboard(result.text)
}
val scan = Scan(result)
if (prefs.sendScanUrl.isNotEmpty()) {
scan.sendAsync(
prefs.sendScanUrl,
prefs.sendScanType
) { code, body ->
if (code == null || code < 200 || code > 299) {
vibrator.error()
beepBeepBeep()
}
if (body != null && body.isNotEmpty()) {
activity.toast(body)
} else if (code == null || code > 299) {
activity.toast(R.string.background_request_failed)
}
}
}
if (prefs.useHistory) {
scan.id = db.insertScan(scan)
}
if (!bulkMode) {
activity.startActivity(
MainActivity.getDecodeIntent(activity, scan)
)
@@ -563,3 +775,10 @@ fun getReturnIntent(result: Result): Intent {
}
return intent
}
private fun beepBeepBeep() {
ToneGenerator(AudioManager.STREAM_ALARM, 100).startTone(
ToneGenerator.TONE_CDMA_CONFIRM,
1000
)
}
@@ -2,23 +2,39 @@ package de.markusfisch.android.binaryeye.activity
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle
import android.support.v4.app.Fragment
import android.support.v7.app.AppCompatActivity
import android.support.v7.widget.Toolbar
import de.markusfisch.android.binaryeye.R
import de.markusfisch.android.binaryeye.app.colorSystemAndToolBars
import de.markusfisch.android.binaryeye.app.initSystemBars
import de.markusfisch.android.binaryeye.app.setFragment
import de.markusfisch.android.binaryeye.data.Scan
import de.markusfisch.android.binaryeye.app.*
import de.markusfisch.android.binaryeye.database.Scan
import de.markusfisch.android.binaryeye.fragment.DecodeFragment
import de.markusfisch.android.binaryeye.fragment.EncodeFragment
import de.markusfisch.android.binaryeye.fragment.HistoryFragment
import de.markusfisch.android.binaryeye.fragment.PreferencesFragment
import de.markusfisch.android.binaryeye.view.colorSystemAndToolBars
import de.markusfisch.android.binaryeye.view.initSystemBars
import de.markusfisch.android.binaryeye.view.recordToolbarHeight
class MainActivity : AppCompatActivity() {
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<String>,
grantResults: IntArray
) {
when (requestCode) {
PERMISSION_LOCATION, PERMISSION_WRITE -> if (grantResults.isNotEmpty() &&
grantResults[0] == PackageManager.PERMISSION_GRANTED
) {
permissionGrantedCallback?.invoke()
permissionGrantedCallback = null
}
}
}
override fun onSupportNavigateUp(): Boolean {
val fm = supportFragmentManager
if (fm != null && fm.backStackEntryCount > 0) {
@@ -29,6 +45,11 @@ class MainActivity : AppCompatActivity() {
return true
}
override fun attachBaseContext(base: Context?) {
base?.applyLocale(prefs.customLocale)
super.attachBaseContext(base)
}
override fun onCreate(state: Bundle?) {
super.onCreate(state)
setContentView(R.layout.activity_main)
@@ -52,7 +73,7 @@ class MainActivity : AppCompatActivity() {
private const val PREFERENCES = "preferences"
private const val HISTORY = "history"
private const val ENCODE = "encode"
private const val DECODED = "decoded"
const val DECODED = "decoded"
private fun getFragmentForIntent(intent: Intent?): Fragment {
intent ?: return PreferencesFragment()
@@ -3,41 +3,63 @@ package de.markusfisch.android.binaryeye.activity
import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.Point
import android.graphics.Rect
import android.graphics.RectF
import android.net.Uri
import android.os.Bundle
import android.os.Parcelable
import android.os.Vibrator
import android.support.v7.app.AppCompatActivity
import android.support.v7.widget.Toolbar
import android.support.v8.renderscript.RenderScript
import android.view.Menu
import android.view.MenuItem
import com.google.zxing.Result
import de.markusfisch.android.binaryeye.R
import de.markusfisch.android.binaryeye.app.colorSystemAndToolBars
import de.markusfisch.android.binaryeye.app.initSystemBars
import de.markusfisch.android.binaryeye.app.vibrate
import de.markusfisch.android.binaryeye.app.*
import de.markusfisch.android.binaryeye.graphics.crop
import de.markusfisch.android.binaryeye.graphics.loadImageUri
import de.markusfisch.android.binaryeye.graphics.mapResult
import de.markusfisch.android.binaryeye.view.doOnApplyWindowInsets
import de.markusfisch.android.binaryeye.os.vibrate
import de.markusfisch.android.binaryeye.rs.fixTransparency
import de.markusfisch.android.binaryeye.view.colorSystemAndToolBars
import de.markusfisch.android.binaryeye.view.initSystemBars
import de.markusfisch.android.binaryeye.view.recordToolbarHeight
import de.markusfisch.android.binaryeye.view.setPaddingFromWindowInsets
import de.markusfisch.android.binaryeye.widget.CropImageView
import de.markusfisch.android.binaryeye.widget.DetectorView
import de.markusfisch.android.binaryeye.widget.toast
import de.markusfisch.android.binaryeye.zxing.Zxing
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class PickActivity : AppCompatActivity() {
private val zxing = Zxing()
private lateinit var rs: RenderScript
private lateinit var vibrator: Vibrator
private lateinit var cropImageView: CropImageView
private lateinit var detectorView: DetectorView
private var result: Result? = null
override fun attachBaseContext(base: Context?) {
base?.applyLocale(prefs.customLocale)
super.attachBaseContext(base)
}
override fun onCreate(state: Bundle?) {
super.onCreate(state)
setContentView(R.layout.activity_pick)
// Necessary to get the right translation after setting a custom
// locale.
setTitle(R.string.pick_code_to_scan)
zxing.updateHints(true)
rs = RenderScript.create(this)
vibrator = getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
initSystemBars(this)
@@ -49,19 +71,10 @@ class PickActivity : AppCompatActivity() {
colorSystemAndToolBars(this@PickActivity)
}
val bitmap = if (
intent?.action == Intent.ACTION_SEND &&
intent.type?.startsWith("image/") == true
) {
loadSentImage(intent)
} else if (
intent?.action == Intent.ACTION_VIEW &&
intent.type?.startsWith("image/") == true
) {
loadImageToOpen(intent)
} else {
null
}
val bitmap = fixTransparency(
rs,
getBitmapFromIntent()
)
if (bitmap == null) {
applicationContext.toast(R.string.error_no_content)
@@ -69,42 +82,79 @@ class PickActivity : AppCompatActivity() {
return
}
val scannedRect = Rect()
val scanWithinBounds: () -> Result? = {
var result: Result? = null
crop(
bitmap,
cropImageView.normalizedRectInBounds,
cropImageView.imageRotation
)?.also {
scannedRect.set(0, 0, it.width, it.height)
result = zxing.decodePositiveNegative(it)
}
result
}
cropImageView = findViewById(R.id.image) as CropImageView
cropImageView.restrictTranslation = false
cropImageView.setImageBitmap(bitmap)
cropImageView.onScan = {
var points: List<Point>? = null
scanWithinBounds()?.let {
points = mapResult(
scannedRect.width(),
scannedRect.height(),
cropImageView.getBoundsRect(),
it
)
vibrator.vibrate()
}
points
}
cropImageView.doOnApplyWindowInsets { v, insets ->
(v as CropImageView).windowInsets.set(insets)
scanWithinBounds(bitmap)
}
findViewById(R.id.scan).setOnClickListener {
scanImage(scanWithinBounds())
detectorView = findViewById(R.id.detector_view) as DetectorView
detectorView.onRoiChanged = {
scanWithinBounds(bitmap)
}
detectorView.setPaddingFromWindowInsets()
findViewById(R.id.scan).setOnClickListener {
showResult()
}
}
private fun getBitmapFromIntent(): Bitmap? = if (
intent?.action == Intent.ACTION_SEND &&
intent.type?.startsWith("image/") == true
) {
loadSentImage(intent)
} else if (
intent?.action == Intent.ACTION_VIEW &&
intent.type?.startsWith("image/") == true
) {
loadImageToOpen(intent)
} else {
null
}
private fun scanWithinBounds(bitmap: Bitmap) {
val roi = detectorView.roi
val rectInView = if (roi.width() < 1) {
cropImageView.getBoundsRect()
} else {
roi
}
val imageRect = cropImageView.mappedRect
val rectInImage = normalizeRoi(imageRect, rectInView)
val cropped = crop(
bitmap,
rectInImage,
cropImageView.imageRotation
) ?: return
CoroutineScope(Dispatchers.IO).launch(Dispatchers.IO) {
result = zxing.decodePositiveNegative(cropped)
result?.let {
withContext(Dispatchers.Main) {
if (!isFinishing) {
vibrator.vibrate()
}
val rc = Rect()
imageRect.round(rc)
rectInView.intersect(rc)
detectorView.mark(
mapResult(
cropped.width,
cropped.height,
0,
rectInView,
it
)
)
}
}
}
}
override fun onDestroy() {
super.onDestroy()
rs.destroy()
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
@@ -138,12 +188,23 @@ class PickActivity : AppCompatActivity() {
return loadImageUri(contentResolver, uri)
}
private fun scanImage(result: Result?) {
if (result != null) {
showResult(this, result)
private fun showResult() {
result?.let {
showResult(this, it, vibrator)
finish()
return
}
applicationContext.toast(R.string.no_barcode_found)
}
}
private fun normalizeRoi(imageRect: RectF, roi: Rect): RectF {
val w = imageRect.width()
val h = imageRect.height()
return RectF(
(roi.left - imageRect.left) / w,
(roi.top - imageRect.top) / h,
(roi.right - imageRect.left) / w,
(roi.bottom - imageRect.top) / h
)
}
@@ -9,9 +9,9 @@ class SplashActivity : AppCompatActivity() {
override fun onCreate(state: Bundle?) {
super.onCreate(state)
// it's important _not_ to inflate a layout file here
// It's important _not_ to inflate a layout file here
// because that would happen after the app is fully
// initialized what is too late
// initialized what is too late.
setRestartCount(intent)
startActivity(Intent(applicationContext, CameraActivity::class.java))
@@ -8,7 +8,10 @@ import android.view.ViewGroup
import android.widget.CursorAdapter
import android.widget.TextView
import de.markusfisch.android.binaryeye.R
import de.markusfisch.android.binaryeye.data.Database
import de.markusfisch.android.binaryeye.database.Database
import java.text.DateFormat
import java.text.SimpleDateFormat
import java.util.*
class ScansAdapter(context: Context, cursor: Cursor) :
CursorAdapter(context, cursor, false) {
@@ -31,6 +34,17 @@ class ScansAdapter(context: Context, cursor: Cursor) :
selectedScanPosition = -1
}
fun getSelectedContent() = if (selectedScanPosition < 0) {
null
} else {
val cursor = getItem(selectedScanPosition)
if (cursor is Cursor) {
cursor.getString(contentIndex)
} else {
null
}
}
override fun newView(
context: Context,
cursor: Cursor,
@@ -45,7 +59,7 @@ class ScansAdapter(context: Context, cursor: Cursor) :
cursor: Cursor
) {
val holder = getViewHolder(view)
holder.timeView.text = cursor.getString(timeIndex)
holder.timeView.text = formatDateTime(cursor.getString(timeIndex))
val name = cursor.getString(nameIndex)
val content = cursor.getString(contentIndex)
var icon = 0
@@ -60,8 +74,8 @@ class ScansAdapter(context: Context, cursor: Cursor) :
holder.contentView.setCompoundDrawablesWithIntrinsicBounds(
icon, 0, 0, 0
)
holder.formatView.text = cursor.getString(formatIndex)
// view.isSelected needs to be put on the queue to work
holder.formatView.text = prettifyFormatName(cursor.getString(formatIndex))
// view.isSelected needs to be put on the queue to work.
val selected = cursor.getLong(idIndex) == selectedScanId
view.post {
view.isSelected = selected
@@ -84,3 +98,15 @@ class ScansAdapter(context: Context, cursor: Cursor) :
val formatView: TextView
)
}
fun prettifyFormatName(format: String) = format.replace("_", " ")
private fun formatDateTime(rfc: String): String {
SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.US).parse(rfc)?.let {
return DateFormat.getDateTimeInstance(
DateFormat.LONG,
DateFormat.SHORT
).format(it)
}
return rfc
}
@@ -2,7 +2,7 @@ package de.markusfisch.android.binaryeye.app
import android.app.Application
import android.support.v8.renderscript.RenderScript
import de.markusfisch.android.binaryeye.data.Database
import de.markusfisch.android.binaryeye.database.Database
import de.markusfisch.android.binaryeye.preference.Preferences
val db = Database()
@@ -11,12 +11,11 @@ val prefs = Preferences()
class BinaryEyeApp : Application() {
override fun onCreate() {
super.onCreate()
db.open(this)
prefs.init(this)
if (prefs.forceCompat) {
RenderScript.forceCompat()
}
db.open(this)
}
}
@@ -11,10 +11,4 @@ fun parseAndNormalizeUri(input: String): Uri = Uri.parse(input).let {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) it.normalizeScheme() else it
}
fun ByteArray.toHexString(): String {
val hex = StringBuilder()
for (i in this.indices) {
hex.append(String.format("%02X", this[i]))
}
return hex.toString()
}
fun ByteArray.toHexString() = joinToString("") { "%02X".format(it) }
@@ -0,0 +1,25 @@
package de.markusfisch.android.binaryeye.app
import android.content.Context
import android.os.Build
import java.util.*
fun Context.applyLocale(localeName: String) {
if (localeName.isNotEmpty()) {
val localeParts = localeName.split("-")
val locale = if (localeParts.size == 2) {
Locale(localeParts[0], localeParts[1])
} else {
Locale(localeName)
}
Locale.setDefault(locale)
val conf = resources.configuration
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
@Suppress("DEPRECATION")
conf.locale = locale
} else {
conf.setLocale(locale)
}
resources.updateConfiguration(conf, resources.displayMetrics)
}
}
@@ -6,41 +6,50 @@ import android.content.pm.PackageManager
import android.support.v4.app.ActivityCompat
import android.support.v4.content.ContextCompat
fun hasCameraPermission(activity: Activity, requestCode: Int = 1): Boolean {
return hasPermission(activity, Manifest.permission.CAMERA, requestCode)
}
var permissionGrantedCallback: (() -> Any)? = null
fun hasWritePermission(activity: Activity, requestCode: Int = 2): Boolean {
const val PERMISSION_CAMERA = 1
fun hasCameraPermission(activity: Activity) = hasPermission(
activity,
Manifest.permission.CAMERA,
PERMISSION_CAMERA
)
const val PERMISSION_WRITE = 2
fun hasWritePermission(activity: Activity, callback: () -> Any): Boolean {
permissionGrantedCallback = callback
return hasPermission(
activity,
Manifest.permission.WRITE_EXTERNAL_STORAGE,
requestCode
PERMISSION_WRITE
)
}
fun hasLocationPermission(activity: Activity, requestCode: Int = 3): Boolean {
const val PERMISSION_LOCATION = 3
fun hasLocationPermission(activity: Activity, callback: () -> Any): Boolean {
permissionGrantedCallback = callback
return hasPermission(
activity,
Manifest.permission.ACCESS_FINE_LOCATION,
requestCode
PERMISSION_LOCATION
)
}
fun hasPermission(
private fun hasPermission(
activity: Activity,
permission: String,
requestCode: Int
): Boolean {
return if (ContextCompat.checkSelfPermission(activity, permission) !=
PackageManager.PERMISSION_GRANTED
) {
ActivityCompat.requestPermissions(
activity,
arrayOf(permission),
requestCode
)
false
} else {
true
}
) = if (ContextCompat.checkSelfPermission(
activity,
permission
) != PackageManager.PERMISSION_GRANTED
) {
ActivityCompat.requestPermissions(
activity,
arrayOf(permission),
requestCode
)
false
} else {
true
}
@@ -1,24 +0,0 @@
package de.markusfisch.android.binaryeye.app
import android.os.Build
import android.os.VibrationEffect
import android.os.Vibrator
private const val MILLISECONDS = 100L
fun Vibrator.vibrate() {
if (!prefs.vibrate) {
return
}
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
@Suppress("DEPRECATION")
this.vibrate(MILLISECONDS)
} else {
this.vibrate(
VibrationEffect.createOneShot(
MILLISECONDS,
VibrationEffect.DEFAULT_AMPLITUDE
)
)
}
}
@@ -1,4 +1,4 @@
package de.markusfisch.android.binaryeye.app
package de.markusfisch.android.binaryeye.content
import android.content.ClipData
import android.content.Context
@@ -11,7 +11,7 @@ fun Context.copyToClipboard(text: String) {
(service as android.text.ClipboardManager).text = text
} else {
(service as android.content.ClipboardManager).setPrimaryClip(
ClipData.newPlainText("simple text", text)
ClipData.newPlainText("plain text", text)
)
}
}
@@ -1,5 +1,6 @@
package de.markusfisch.android.binaryeye.app
package de.markusfisch.android.binaryeye.content
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.net.Uri
@@ -10,11 +11,32 @@ import de.markusfisch.android.binaryeye.R
import de.markusfisch.android.binaryeye.widget.toast
import java.io.File
fun Context.execShareIntent(intent: Intent) {
if (!startIntent(intent)) {
toast(R.string.cannot_resolve_action)
}
}
fun Context.startIntent(intent: Intent) = try {
// Avoid using `intent.resolveActivity()` at API level 30+ due
// to the new package visibility restrictions. In order for
// `resolveActivity()` to "see" another package, we would need
// to list that package/intent in a `<queries>` block in the
// Manifest. But since we used `resolveActivity()` only to avoid
// an exception if the Intent cannot be resolved, it's much easier
// and more robust to just try and catch that exception if
// necessary.
startActivity(intent)
true
} catch (e: ActivityNotFoundException) {
false
}
fun shareText(context: Context, text: String, type: String = "text/plain") {
val intent = Intent(Intent.ACTION_SEND)
intent.putExtra(Intent.EXTRA_TEXT, text)
intent.type = type
execShareIntent(context, intent)
context.execShareIntent(intent)
}
fun shareUri(context: Context, uri: Uri, type: String) {
@@ -22,15 +44,7 @@ fun shareUri(context: Context, uri: Uri, type: String) {
intent.putExtra(Intent.EXTRA_STREAM, uri)
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
intent.type = type
execShareIntent(context, intent)
}
fun execShareIntent(context: Context, intent: Intent) {
if (intent.resolveActivity(context.packageManager) != null) {
context.startActivity(intent)
} else {
context.toast(R.string.cannot_resolve_action)
}
context.execShareIntent(intent)
}
fun shareFile(context: Context, file: File, type: String) {
@@ -1,9 +1,9 @@
package de.markusfisch.android.binaryeye.data
package de.markusfisch.android.binaryeye.database
import android.content.Context
import android.database.Cursor
import de.markusfisch.android.binaryeye.app.toHexString
import de.markusfisch.android.binaryeye.app.writeExternalFile
import de.markusfisch.android.binaryeye.io.writeExternalFile
import java.io.ByteArrayOutputStream
import java.io.OutputStream
@@ -87,6 +87,8 @@ private fun Cursor.toCsvRecord(
return sb.toString().toByteArray()
}
private fun String.quoteAndEscape() = "\"${this
.replace("\n", " ")
.replace("\"", "\"\"")}\""
private fun String.quoteAndEscape() = "\"${
this
.replace("\n", " ")
.replace("\"", "\"\"")
}\""
@@ -1,4 +1,4 @@
package de.markusfisch.android.binaryeye.data
package de.markusfisch.android.binaryeye.database
import android.content.ContentValues
import android.content.Context
@@ -50,14 +50,19 @@ class Database {
""", getWhereArguments(query)
)
private fun getWhereClause(query: String?) = if (query?.isNotEmpty() == true) {
"""WHERE $SCANS_CONTENT LIKE ?
private fun getWhereClause(
query: String?,
prefix: String = "WHERE"
) = if (query?.isNotEmpty() == true) {
"""$prefix $SCANS_CONTENT LIKE ?
OR $SCANS_NAME LIKE ?"""
} else {
""
}
private fun getWhereArguments(query: String?) = if (query?.isNotEmpty() == true) {
private fun getWhereArguments(
query: String?
) = if (query?.isNotEmpty() == true) {
val instr = "%$query%"
arrayOf(instr, instr)
} else {
@@ -173,8 +178,8 @@ class Database {
db.delete(SCANS, "$SCANS_ID = ?", arrayOf("$id"))
}
fun removeScans() {
db.delete(SCANS, null, null)
fun removeScans(query: String? = null) {
db.delete(SCANS, getWhereClause(query, ""), getWhereArguments(query))
}
fun renameScan(id: Long, name: String) {
@@ -1,16 +1,12 @@
package de.markusfisch.android.binaryeye.data
package de.markusfisch.android.binaryeye.database
import android.app.Activity
import android.os.Environment
import de.markusfisch.android.binaryeye.app.hasWritePermission
import de.markusfisch.android.binaryeye.app.writeExternalFile
import de.markusfisch.android.binaryeye.io.writeExternalFile
import java.io.File
import java.io.FileInputStream
fun exportDatabase(activity: Activity, fileName: String): Boolean {
if (!hasWritePermission(activity)) {
return false
}
val dbFile = File(
Environment.getDataDirectory(),
"//data//${activity.packageName}//databases//${Database.FILE_NAME}"
@@ -1,9 +1,9 @@
package de.markusfisch.android.binaryeye.data
package de.markusfisch.android.binaryeye.database
import android.content.Context
import android.database.Cursor
import de.markusfisch.android.binaryeye.app.toHexString
import de.markusfisch.android.binaryeye.app.writeExternalFile
import de.markusfisch.android.binaryeye.io.writeExternalFile
import org.json.JSONArray
import org.json.JSONException
import org.json.JSONObject
@@ -1,4 +1,4 @@
package de.markusfisch.android.binaryeye.data
package de.markusfisch.android.binaryeye.database
import android.os.Parcel
import android.os.Parcelable
@@ -19,7 +19,7 @@ data class Scan(
val suggestedPrice: String?,
val upcEanExtension: String?,
val timestamp: String = getDateTime(),
val id: Long = 0L
var id: Long = 0L
) : Parcelable {
constructor(result: Result) : this(
result.text,
@@ -138,8 +138,12 @@ private fun getRawBytes(result: Result): ByteArray? {
for (seg in segments as Iterable<ByteArray>) {
bytes += seg
}
// byte segments can never be shorter than the text.
// Zxing cuts off content prefixes like "WIFI:"
// If the byte segments are shorter than the converted string, the
// content of the QR Code has been encoded with different encoding
// modes (e.g. some parts in alphanumeric, some in byte encoding).
// This is because Zxing only records byte segments for byte encoded
// parts. Please note the byte segments can actually be longer than
// the string because Zxing cuts off prefixes like "WIFI:".
return if (bytes.size >= result.text.length) bytes else null
}
@@ -2,19 +2,30 @@ package de.markusfisch.android.binaryeye.fragment
import android.annotation.SuppressLint
import android.app.AlertDialog
import android.content.Context
import android.graphics.Bitmap
import android.os.Bundle
import android.support.v4.app.Fragment
import android.view.*
import android.widget.EditText
import com.google.zxing.BarcodeFormat
import com.google.zxing.EncodeHintType
import de.markusfisch.android.binaryeye.R
import de.markusfisch.android.binaryeye.app.*
import de.markusfisch.android.binaryeye.app.hasWritePermission
import de.markusfisch.android.binaryeye.content.copyToClipboard
import de.markusfisch.android.binaryeye.content.shareFile
import de.markusfisch.android.binaryeye.content.shareText
import de.markusfisch.android.binaryeye.io.addSuffixIfNotGiven
import de.markusfisch.android.binaryeye.io.toSaveResult
import de.markusfisch.android.binaryeye.io.writeExternalFile
import de.markusfisch.android.binaryeye.view.doOnApplyWindowInsets
import de.markusfisch.android.binaryeye.view.setPaddingFromWindowInsets
import de.markusfisch.android.binaryeye.widget.ConfinedScalingImageView
import de.markusfisch.android.binaryeye.widget.toast
import de.markusfisch.android.binaryeye.zxing.Zxing
import de.markusfisch.android.binaryeye.zxing.encodeAsBitmap
import de.markusfisch.android.binaryeye.zxing.encodeAsSvg
import de.markusfisch.android.binaryeye.zxing.encodeAsText
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
@@ -25,7 +36,13 @@ import java.io.OutputStream
import java.util.*
class BarcodeFragment : Fragment() {
private var barcode: Bitmap? = null
private enum class FileType {
PNG, SVG, TXT
}
private var barcodeBitmap: Bitmap? = null
private var barcodeSvg: String? = null
private var barcodeTxt: String? = null
private var content: String = ""
private var format: BarcodeFormat? = null
@@ -51,9 +68,12 @@ class BarcodeFragment : Fragment() {
val args = arguments ?: return view
val content = args.getString(CONTENT) ?: return view
val format = args.getSerializable(FORMAT) as BarcodeFormat? ?: return view
val hints = args.getSerializable(HINTS) as EnumMap<EncodeHintType, Any>?
val size = args.getInt(SIZE)
try {
barcode = Zxing.encodeAsBitmap(content, format, size, size)
barcodeBitmap = encodeAsBitmap(content, format, size, size, hints)
barcodeSvg = encodeAsSvg(content, format, hints)
barcodeTxt = encodeAsText(content, format, hints)
} catch (e: Exception) {
var message = e.message
if (message == null || message.isEmpty()) {
@@ -71,16 +91,15 @@ class BarcodeFragment : Fragment() {
val imageView = view.findViewById<ConfinedScalingImageView>(
R.id.barcode
)
imageView.setImageBitmap(barcode)
imageView.setImageBitmap(barcodeBitmap)
imageView.post {
// make sure to invoke this after ScalingImageView.onLayout()
// Make sure to invoke this after ScalingImageView.onLayout().
imageView.minWidth /= 2f
}
view.findViewById<View>(R.id.share).setOnClickListener {
val bitmap = barcode
bitmap?.let {
share(bitmap)
pickFileType(context, R.string.share_as) {
shareAs(it)
}
}
@@ -98,33 +117,74 @@ class BarcodeFragment : Fragment() {
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.save -> {
askForFileNameAndSave()
R.id.copy_to_clipboard -> {
barcodeTxt?.let {
context.copyToClipboard(it)
context.toast(R.string.copied_to_clipboard)
}
true
}
R.id.export_to_file -> {
pickFileType(context, R.string.export_as) {
askForFileNameAndSave(it)
}
true
}
else -> super.onOptionsItemSelected(item)
}
}
// dialogs do not have a parent view
private fun pickFileType(
context: Context,
title: Int,
action: (FileType) -> Unit
) {
val fileTypes = FileType.values()
AlertDialog.Builder(context)
.setTitle(title)
.setItems(fileTypes.map { it.name }.toTypedArray()) { _, which ->
action(fileTypes[which])
}
.show()
}
// Dialogs do not have a parent view.
@SuppressLint("InflateParams")
private fun askForFileNameAndSave() {
private fun askForFileNameAndSave(fileType: FileType) {
val ac = activity ?: return
if (!hasWritePermission(ac) { askForFileNameAndSave(fileType) }) {
return
}
val view = ac.layoutInflater.inflate(R.layout.dialog_save_file, null)
val editText = view.findViewById<EditText>(R.id.file_name)
editText.setText(encodeFileName("${format.toString()}_$content"))
AlertDialog.Builder(ac)
.setView(view)
.setPositiveButton(android.R.string.ok) { _, _ ->
val bitmap = barcode
bitmap?.let {
saveAsFile(
bitmap,
addSuffixIfNotGiven(
editText.text.toString(),
".png"
)
)
val fileName = editText.text.toString()
when (fileType) {
FileType.PNG -> saveAs(
addSuffixIfNotGiven(fileName, ".png"),
MIME_PNG
) {
barcodeBitmap?.saveAsPng(it)
}
FileType.SVG -> saveAs(
addSuffixIfNotGiven(fileName, ".svg"),
MIME_SVG
) { outputStream ->
barcodeSvg?.let {
outputStream.write(it.toByteArray())
}
}
FileType.TXT -> saveAs(
addSuffixIfNotGiven(fileName, ".txt"),
MIME_TXT
) { outputStream ->
barcodeTxt?.let {
outputStream.write(it.toByteArray())
}
}
}
}
.setNegativeButton(android.R.string.cancel) { _, _ ->
@@ -132,23 +192,30 @@ class BarcodeFragment : Fragment() {
.show()
}
private fun saveAsFile(bitmap: Bitmap, fileName: String) {
private fun saveAs(
fileName: String,
mimeType: String,
write: (outputStream: OutputStream) -> Unit
) {
val ac = activity ?: return
if (!hasWritePermission(ac)) {
return
}
GlobalScope.launch {
val message = writeExternalFile(ac, fileName, "image/png") {
bitmap.saveAsPng(it)
}.toSaveResult()
val message = writeExternalFile(ac, fileName, mimeType, write).toSaveResult()
GlobalScope.launch(Main) {
ac.toast(message)
}
}
}
private fun shareAs(fileType: FileType) {
when (fileType) {
FileType.PNG -> barcodeBitmap?.let { share(it) }
FileType.SVG -> barcodeSvg?.let { shareText(context, it, MIME_SVG) }
FileType.TXT -> barcodeTxt?.let { shareText(context, it) }
}
}
private fun share(bitmap: Bitmap) {
GlobalScope.launch {
GlobalScope.launch(Dispatchers.IO) {
val file = File(
context.externalCacheDir,
"shared_barcode.png"
@@ -174,16 +241,24 @@ class BarcodeFragment : Fragment() {
companion object {
private const val CONTENT = "content"
private const val FORMAT = "format"
private const val HINTS = "hints"
private const val SIZE = "size"
private const val MIME_PNG = "image/png"
private const val MIME_SVG = "image/svg+xmg"
private const val MIME_TXT = "text/plain"
fun newInstance(
content: String,
format: BarcodeFormat,
size: Int
size: Int,
hints: EnumMap<EncodeHintType, Any>? = null
): Fragment {
val args = Bundle()
args.putString(CONTENT, content)
args.putSerializable(FORMAT, format)
hints?.let {
args.putSerializable(HINTS, it)
}
args.putInt(SIZE, size)
val fragment = BarcodeFragment()
fragment.arguments = args
@@ -14,8 +14,15 @@ import android.widget.TextView
import de.markusfisch.android.binaryeye.R
import de.markusfisch.android.binaryeye.actions.ActionRegistry
import de.markusfisch.android.binaryeye.actions.wifi.WifiAction
import de.markusfisch.android.binaryeye.activity.MainActivity
import de.markusfisch.android.binaryeye.adapter.prettifyFormatName
import de.markusfisch.android.binaryeye.app.*
import de.markusfisch.android.binaryeye.data.Scan
import de.markusfisch.android.binaryeye.content.copyToClipboard
import de.markusfisch.android.binaryeye.content.shareText
import de.markusfisch.android.binaryeye.database.Scan
import de.markusfisch.android.binaryeye.io.askForFileName
import de.markusfisch.android.binaryeye.io.toSaveResult
import de.markusfisch.android.binaryeye.io.writeExternalFile
import de.markusfisch.android.binaryeye.view.setPaddingFromWindowInsets
import de.markusfisch.android.binaryeye.widget.toast
import kotlinx.coroutines.CoroutineScope
@@ -31,13 +38,16 @@ class DecodeFragment : Fragment() {
private lateinit var format: String
private lateinit var fab: FloatingActionButton
private var action = ActionRegistry.DEFAULT_ACTION
private var isBinary = false
private val parentJob = Job()
private val scope: CoroutineScope = CoroutineScope(Dispatchers.Main + parentJob)
private val content: String
get() = contentView.text.toString()
private val parentJob = Job()
private val scope: CoroutineScope = CoroutineScope(Dispatchers.Main + parentJob)
private var closeAutomatically = false
private var action = ActionRegistry.DEFAULT_ACTION
private var isBinary = false
private var raw: ByteArray = ByteArray(0)
private var id = 0L
override fun onCreate(state: Bundle?) {
super.onCreate(state)
@@ -57,14 +67,18 @@ class DecodeFragment : Fragment() {
false
)
closeAutomatically = prefs.closeAutomatically &&
activity?.intent?.hasExtra(MainActivity.DECODED) == true
val scan = arguments?.getParcelable(SCAN) as Scan?
?: throw IllegalArgumentException("DecodeFragment needs a Scan")
id = scan.id
val inputContent = scan.content
isBinary = hasNonPrintableCharacters(
inputContent
) or inputContent.isEmpty()
val raw = scan.raw ?: inputContent.toByteArray()
raw = scan.raw ?: inputContent.toByteArray()
format = scan.format
contentView = view.findViewById(R.id.content)
@@ -113,15 +127,6 @@ class DecodeFragment : Fragment() {
hexView = view.findViewById(R.id.hex)
updateViewsAndAction(raw)
if (action is WifiAction) {
val password = (action as WifiAction).password
if (password != null) {
activity?.apply {
copyToClipboard(password)
toast(R.string.copied_password_to_clipboard)
}
}
}
if (prefs.showMetaData) {
fillMetaView(metaView, scan)
@@ -133,6 +138,11 @@ class DecodeFragment : Fragment() {
return view
}
override fun onDestroy() {
super.onDestroy()
parentJob.cancel()
}
private fun updateViewsAndAction(bytes: ByteArray) {
val prevAction = action
if (!prevAction.canExecuteOn(bytes)) {
@@ -141,7 +151,7 @@ class DecodeFragment : Fragment() {
formatView.text = resources.getQuantityString(
R.plurals.barcode_info,
bytes.size,
format,
prettifyFormatName(format),
bytes.size
)
hexView.text = if (prefs.showHexDump) hexDump(bytes) else ""
@@ -160,7 +170,7 @@ class DecodeFragment : Fragment() {
private fun fillMetaView(tableLayout: TableLayout, scan: Scan) {
val ctx = tableLayout.context
val spaceBetween = (ctx.resources.displayMetrics.density * 16f).toInt()
val spaceBetween = (16f * ctx.resources.displayMetrics.density).toInt()
var hasMeta = false
sortedMapOf(
R.string.error_correction_level to scan.errorCorrectionLevel,
@@ -196,16 +206,31 @@ class DecodeFragment : Fragment() {
menu.findItem(R.id.copy_to_clipboard).isVisible = false
menu.findItem(R.id.create).isVisible = false
}
if (id > 0L) {
menu.findItem(R.id.remove).isVisible = true
}
if (action is WifiAction) {
menu.findItem(R.id.copy_password).isVisible = true
}
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.copy_password -> {
copyPasswordToClipboard()
maybeBackOrFinish()
true
}
R.id.copy_to_clipboard -> {
copyToClipboard(content)
copyToClipboard(textOrHex())
maybeBackOrFinish()
true
}
R.id.share -> {
context?.also { shareText(it, content) }
context?.also {
shareText(it, textOrHex())
maybeBackOrFinish()
}
true
}
R.id.create -> {
@@ -215,10 +240,33 @@ class DecodeFragment : Fragment() {
)
true
}
R.id.remove -> {
db.removeScan(id)
backOrFinish()
true
}
else -> super.onOptionsItemSelected(item)
}
}
private fun textOrHex() = if (isBinary) {
raw.toHexString()
} else {
content
}
private fun copyPasswordToClipboard() {
val ac = action
if (ac is WifiAction) {
ac.password?.let { password ->
activity?.apply {
copyToClipboard(password)
toast(R.string.copied_password_to_clipboard)
}
}
}
}
private fun copyToClipboard(text: String) {
activity?.apply {
copyToClipboard(text)
@@ -226,24 +274,40 @@ class DecodeFragment : Fragment() {
}
}
private fun maybeBackOrFinish() {
if (closeAutomatically) {
backOrFinish()
}
}
private fun backOrFinish() {
val fm = fragmentManager
if (fm != null && fm.backStackEntryCount > 0) {
fm.popBackStack()
} else {
activity?.finish()
}
}
private fun executeAction(content: ByteArray) {
val ac = activity ?: return
if (content.isNotEmpty()) {
if (action is WifiAction &&
Build.VERSION.SDK_INT < Build.VERSION_CODES.Q &&
!hasLocationPermission(ac)
!hasLocationPermission(ac) { executeAction(content) }
) {
return
}
scope.launch {
action.execute(ac, content)
maybeBackOrFinish()
}
}
}
private fun askForFileNameAndSave(raw: ByteArray) {
val ac = activity ?: return
if (!hasWritePermission(ac)) {
if (!hasWritePermission(ac) { askForFileNameAndSave(raw) }) {
return
}
scope.launch(Dispatchers.Main) {
@@ -259,11 +323,6 @@ class DecodeFragment : Fragment() {
}
}
override fun onDestroyView() {
parentJob.cancel()
super.onDestroyView()
}
companion object {
private const val SCAN = "scan"
@@ -277,7 +336,7 @@ class DecodeFragment : Fragment() {
}
}
fun hexDump(bytes: ByteArray, charsPerLine: Int = 33): String {
private fun hexDump(bytes: ByteArray, charsPerLine: Int = 33): String {
if (charsPerLine < 4 || bytes.isEmpty()) {
return ""
}
@@ -2,17 +2,25 @@ package de.markusfisch.android.binaryeye.fragment
import android.os.Bundle
import android.support.v4.app.Fragment
import android.view.*
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.*
import com.google.zxing.BarcodeFormat
import com.google.zxing.EncodeHintType
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel
import de.markusfisch.android.binaryeye.R
import de.markusfisch.android.binaryeye.adapter.prettifyFormatName
import de.markusfisch.android.binaryeye.app.addFragment
import de.markusfisch.android.binaryeye.app.hideSoftKeyboard
import de.markusfisch.android.binaryeye.app.prefs
import de.markusfisch.android.binaryeye.view.hideSoftKeyboard
import de.markusfisch.android.binaryeye.view.setPaddingFromWindowInsets
import java.util.*
class EncodeFragment : Fragment() {
private lateinit var formatView: Spinner
private lateinit var errorCorrectionLabel: TextView
private lateinit var errorCorrectionLevel: Spinner
private lateinit var sizeView: TextView
private lateinit var sizeBarView: SeekBar
private lateinit var contentView: EditText
@@ -31,11 +39,6 @@ class EncodeFragment : Fragment() {
BarcodeFormat.UPC_A
)
override fun onCreate(state: Bundle?) {
super.onCreate(state)
setHasOptionsMenu(true)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
@@ -51,11 +54,38 @@ class EncodeFragment : Fragment() {
)
formatView = view.findViewById(R.id.format)
formatView.adapter = ArrayAdapter(
val formatAdapter = ArrayAdapter(
ac,
android.R.layout.simple_list_item_1,
writers.map { it.name }
android.R.layout.simple_spinner_item,
writers.map { prettifyFormatName(it.name) }
)
formatAdapter.setDropDownViewResource(
android.R.layout.simple_spinner_dropdown_item
)
formatView.adapter = formatAdapter
formatView.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(
parentView: AdapterView<*>?,
selectedItemView: View?,
position: Int,
id: Long
) {
val visibility = if (
writers[position] == BarcodeFormat.QR_CODE
) {
View.VISIBLE
} else {
View.GONE
}
errorCorrectionLabel.visibility = visibility
errorCorrectionLevel.visibility = visibility
}
override fun onNothingSelected(parentView: AdapterView<*>?) {}
}
errorCorrectionLabel = view.findViewById(R.id.error_correction_label)
errorCorrectionLevel = view.findViewById(R.id.error_correction_level)
sizeView = view.findViewById(R.id.size_display)
sizeBarView = view.findViewById(R.id.size_bar)
@@ -94,22 +124,17 @@ class EncodeFragment : Fragment() {
prefs.indexOfLastSelectedFormat = formatView.selectedItemPosition
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.fragment_encode, menu)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.next -> {
encode()
true
}
else -> super.onOptionsItemSelected(item)
}
}
private fun encode() {
val hints = EnumMap<EncodeHintType, Any>(EncodeHintType::class.java)
val format = writers[formatView.selectedItemPosition]
if (format == BarcodeFormat.QR_CODE) {
hints[EncodeHintType.ERROR_CORRECTION] = arrayListOf(
ErrorCorrectionLevel.L,
ErrorCorrectionLevel.M,
ErrorCorrectionLevel.Q,
ErrorCorrectionLevel.H
)[errorCorrectionLevel.selectedItemPosition]
}
val size = getSize(sizeBarView.progress)
val content = contentView.text.toString()
if (content.isEmpty()) {
@@ -122,7 +147,7 @@ class EncodeFragment : Fragment() {
activity?.hideSoftKeyboard(contentView)
addFragment(
fragmentManager,
BarcodeFragment.newInstance(content, format, size)
BarcodeFragment.newInstance(content, format, size, hints)
)
}
}
@@ -21,12 +21,15 @@ import android.widget.ListView
import de.markusfisch.android.binaryeye.R
import de.markusfisch.android.binaryeye.adapter.ScansAdapter
import de.markusfisch.android.binaryeye.app.*
import de.markusfisch.android.binaryeye.data.Database
import de.markusfisch.android.binaryeye.data.exportCsv
import de.markusfisch.android.binaryeye.data.exportDatabase
import de.markusfisch.android.binaryeye.data.exportJson
import de.markusfisch.android.binaryeye.view.setPaddingFromWindowInsets
import de.markusfisch.android.binaryeye.view.useVisibility
import de.markusfisch.android.binaryeye.content.copyToClipboard
import de.markusfisch.android.binaryeye.content.shareText
import de.markusfisch.android.binaryeye.database.Database
import de.markusfisch.android.binaryeye.database.exportCsv
import de.markusfisch.android.binaryeye.database.exportDatabase
import de.markusfisch.android.binaryeye.database.exportJson
import de.markusfisch.android.binaryeye.io.askForFileName
import de.markusfisch.android.binaryeye.io.toSaveResult
import de.markusfisch.android.binaryeye.view.*
import de.markusfisch.android.binaryeye.widget.toast
import kotlinx.coroutines.*
@@ -71,6 +74,14 @@ class HistoryFragment : Fragment() {
): Boolean {
val ac = activity ?: return false
return when (item.itemId) {
R.id.copy_scan -> {
scansAdapter?.getSelectedContent()?.let {
ac.copyToClipboard(it)
ac.toast(R.string.copied_to_clipboard)
}
closeActionMode()
true
}
R.id.edit_scan -> {
scansAdapter?.let {
askForName(
@@ -102,6 +113,8 @@ class HistoryFragment : Fragment() {
private var listViewState: Parcelable? = null
private var actionMode: ActionMode? = null
private var filter: String? = null
private var clearListMenuItem: MenuItem? = null
private var exportHistoryMenuItem: MenuItem? = null
override fun onCreate(state: Bundle?) {
super.onCreate(state)
@@ -141,7 +154,7 @@ class HistoryFragment : Fragment() {
}
true
}
listView.setOnScrollListener(systemBarScrollListener)
listView.setOnScrollListener(systemBarListViewScrollListener)
fab = view.findViewById(R.id.share)
fab.setOnClickListener { v ->
@@ -173,6 +186,8 @@ class HistoryFragment : Fragment() {
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)
}
private fun initSearchView(item: MenuItem?) {
@@ -205,11 +220,11 @@ class HistoryFragment : Fragment() {
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.clear -> {
askToRemoveAllScans(context)
askToRemoveScans(context)
true
}
R.id.export_history -> {
askToExportToFile(context)
askToExportToFile()
true
}
else -> super.onOptionsItemSelected(item)
@@ -243,13 +258,14 @@ class HistoryFragment : Fragment() {
}
ActivityCompat.invalidateOptionsMenu(ac)
}
enableMenuItems(hasScans)
fab.visibility = if (hasScans) {
View.VISIBLE
} else {
View.GONE
}
cursor?.let { cursor ->
// close previous cursor
// Close previous cursor.
scansAdapter?.also { it.changeCursor(null) }
scansAdapter = ScansAdapter(ac, cursor)
listView.adapter = scansAdapter
@@ -261,6 +277,11 @@ class HistoryFragment : Fragment() {
}
}
private fun enableMenuItems(enabled: Boolean) {
clearListMenuItem?.isEnabled = enabled
exportHistoryMenuItem?.isEnabled = enabled
}
private fun closeActionMode() {
unlockStatusBarColor()
scansAdapter?.clearSelection()
@@ -277,7 +298,7 @@ class HistoryFragment : Fragment() {
DecodeFragment.newInstance(scan)
)
} catch (e: IllegalArgumentException) {
// ignore, can never happen
// Ignore, can never happen.
}
}
@@ -286,7 +307,7 @@ class HistoryFragment : Fragment() {
return cursor?.getString(cursor.getColumnIndex(Database.SCANS_NAME))
}
// dialogs don't have a parent layout
// Dialogs don't have a parent layout.
@SuppressLint("InflateParams")
private fun askForName(context: Context, id: Long, text: String?) {
val view = LayoutInflater.from(context).inflate(
@@ -321,11 +342,17 @@ class HistoryFragment : Fragment() {
.show()
}
private fun askToRemoveAllScans(context: Context) {
private fun askToRemoveScans(context: Context) {
AlertDialog.Builder(context)
.setMessage(R.string.really_remove_all_scans)
.setMessage(
if (filter == null) {
R.string.really_remove_all_scans
} else {
R.string.really_remove_selected_scans
}
)
.setPositiveButton(android.R.string.ok) { _, _ ->
db.removeScans()
db.removeScans(filter)
updateAndClearFilter()
}
.setNegativeButton(android.R.string.cancel) { _, _ ->
@@ -333,41 +360,43 @@ class HistoryFragment : Fragment() {
.show()
}
private fun askToExportToFile(context: Context) = scope.launch {
val ac = activity ?: return@launch
progressView.useVisibility {
if (!hasWritePermission(ac)) {
return@useVisibility
}
val options = context.resources.getStringArray(
R.array.export_options_values
)
val delimiter = alertDialog<String>(context) { resume ->
setTitle(R.string.export_as)
setItems(R.array.export_options_names) { _, which ->
resume(options[which])
private fun askToExportToFile() {
scope.launch {
val ac = activity ?: return@launch
progressView.useVisibility {
if (!hasWritePermission(ac) { askToExportToFile() }) {
return@useVisibility
}
} ?: return@useVisibility
val name = withContext(Dispatchers.Main) {
ac.askForFileName(
when (delimiter) {
"db" -> ".db"
"json" -> ".json"
else -> ".csv"
}
val options = context.resources.getStringArray(
R.array.export_options_values
)
} ?: return@useVisibility
val message = when (delimiter) {
"db" -> exportDatabase(ac, name)
else -> db.getScansDetailed(filter)?.use {
when (delimiter) {
"json" -> exportJson(context, name, it)
else -> exportCsv(context, name, it, delimiter)
val delimiter = alertDialog<String>(context) { resume ->
setTitle(R.string.export_as)
setItems(R.array.export_options_names) { _, which ->
resume(options[which])
}
} ?: false
}.toSaveResult()
withContext(Dispatchers.Main) {
ac.toast(message)
} ?: return@useVisibility
val name = withContext(Dispatchers.Main) {
ac.askForFileName(
when (delimiter) {
"db" -> ".db"
"json" -> ".json"
else -> ".csv"
}
)
} ?: return@useVisibility
val message = when (delimiter) {
"db" -> exportDatabase(ac, name)
else -> db.getScansDetailed(filter)?.use {
when (delimiter) {
"json" -> exportJson(context, name, it)
else -> exportCsv(context, name, it, delimiter)
}
} ?: false
}.toSaveResult()
withContext(Dispatchers.Main) {
ac.toast(message)
}
}
}
}
@@ -1,100 +1,127 @@
package de.markusfisch.android.binaryeye.fragment
import android.app.Activity
import android.content.Intent
import android.content.SharedPreferences
import android.content.SharedPreferences.OnSharedPreferenceChangeListener
import android.net.wifi.WifiManager
import android.os.Build
import android.os.Bundle
import android.support.v4.app.Fragment
import android.support.v7.widget.SwitchCompat
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.EditText
import android.support.v7.preference.ListPreference
import android.support.v7.preference.Preference
import android.support.v7.preference.PreferenceFragmentCompat
import android.support.v7.preference.PreferenceGroup
import de.markusfisch.android.binaryeye.R
import de.markusfisch.android.binaryeye.actions.wifi.clearNetworkSuggestions
import de.markusfisch.android.binaryeye.activity.SplashActivity
import de.markusfisch.android.binaryeye.app.prefs
import de.markusfisch.android.binaryeye.preference.UrlPreference
import de.markusfisch.android.binaryeye.view.setPaddingFromWindowInsets
import de.markusfisch.android.binaryeye.view.systemBarRecyclerViewScrollListener
import de.markusfisch.android.binaryeye.widget.toast
class PreferencesFragment : Fragment() {
private lateinit var zoomBySwipingSwitch: SwitchCompat
private lateinit var tryHarderSwitch: SwitchCompat
private lateinit var vibrateSwitch: SwitchCompat
private lateinit var useHistorySwitch: SwitchCompat
private lateinit var ignoreConsecutiveDuplicatesSwitch: SwitchCompat
private lateinit var openImmediatelySwitch: SwitchCompat
private lateinit var showMetaDataSwitch: SwitchCompat
private lateinit var showHexDumpSwitch: SwitchCompat
private lateinit var openWithUrlInput: EditText
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
state: Bundle?
): View? {
activity?.setTitle(R.string.preferences)
val view = inflater.inflate(
R.layout.fragment_preferences,
container,
false
)
zoomBySwipingSwitch = view.findViewById(R.id.zoom_by_swiping)
if (prefs.zoomBySwiping) {
zoomBySwipingSwitch.toggle()
class PreferencesFragment : PreferenceFragmentCompat() {
private val changeListener = object : OnSharedPreferenceChangeListener {
override fun onSharedPreferenceChanged(
sharedPreferences: SharedPreferences,
key: String
) {
val preference = findPreference(key) ?: return
prefs.update()
if (preference.key == "custom_locale") {
activity?.restartApp()
} else {
setSummary(preference)
}
}
tryHarderSwitch = view.findViewById(R.id.try_harder)
if (prefs.tryHarder) {
tryHarderSwitch.toggle()
}
vibrateSwitch = view.findViewById(R.id.vibrate)
if (prefs.vibrate) {
vibrateSwitch.toggle()
}
useHistorySwitch = view.findViewById(R.id.use_history)
if (prefs.useHistory) {
useHistorySwitch.toggle()
}
ignoreConsecutiveDuplicatesSwitch = view.findViewById(
R.id.ignore_consecutive_duplicates
)
if (prefs.ignoreConsecutiveDuplicates) {
ignoreConsecutiveDuplicatesSwitch.toggle()
}
openImmediatelySwitch = view.findViewById(R.id.open_immediately)
if (prefs.openImmediately) {
openImmediatelySwitch.toggle()
}
showMetaDataSwitch = view.findViewById(R.id.show_meta_data)
if (prefs.showMetaData) {
showMetaDataSwitch.toggle()
}
showHexDumpSwitch = view.findViewById(R.id.show_hex_dump)
if (prefs.showHexDump) {
showHexDumpSwitch.toggle()
}
openWithUrlInput = view.findViewById(R.id.open_with_url)
openWithUrlInput.setText(prefs.openWithUrl)
(view.findViewById(R.id.scroll_view) as View).setPaddingFromWindowInsets()
return view
}
override fun onStop() {
super.onStop()
prefs.zoomBySwiping = zoomBySwipingSwitch.isChecked
prefs.tryHarder = tryHarderSwitch.isChecked
prefs.vibrate = vibrateSwitch.isChecked
prefs.useHistory = useHistorySwitch.isChecked
prefs.ignoreConsecutiveDuplicates = ignoreConsecutiveDuplicatesSwitch.isChecked
prefs.openImmediately = openImmediatelySwitch.isChecked
prefs.showMetaData = showMetaDataSwitch.isChecked
prefs.showHexDump = showHexDumpSwitch.isChecked
prefs.openWithUrl = openWithUrlInput.text.toString()
override fun onCreatePreferences(state: Bundle?, rootKey: String?) {
addPreferencesFromResource(R.xml.preferences)
activity?.setTitle(R.string.preferences)
wireClearNetworkPreferences()
}
private fun wireClearNetworkPreferences() {
val pref = findPreference("clear_network_suggestions") ?: return
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
pref.isVisible = false
} else {
pref.setOnPreferenceClickListener { _ ->
context.toast(
if (
clearNetworkSuggestions(context) == WifiManager.STATUS_NETWORK_SUGGESTIONS_SUCCESS
) {
R.string.clear_network_suggestions_success
} else {
R.string.clear_network_suggestions_nothing_to_remove
}
)
true
}
}
}
override fun onResume() {
super.onResume()
listView.setPaddingFromWindowInsets()
listView.removeOnScrollListener(systemBarRecyclerViewScrollListener)
listView.addOnScrollListener(systemBarRecyclerViewScrollListener)
preferenceScreen.sharedPreferences
.registerOnSharedPreferenceChangeListener(changeListener)
setSummaries(preferenceScreen)
}
override fun onPause() {
super.onPause()
preferenceScreen.sharedPreferences
.unregisterOnSharedPreferenceChangeListener(changeListener)
}
override fun onDisplayPreferenceDialog(preference: Preference) {
if (preference is UrlPreference) {
val fm = fragmentManager
UrlDialogFragment.newInstance(preference.key).apply {
setTargetFragment(this@PreferencesFragment, 0)
show(fm, null)
}
} else {
super.onDisplayPreferenceDialog(preference)
}
}
private fun setSummaries(screen: PreferenceGroup) {
var i = screen.preferenceCount
while (i-- > 0) {
setSummary(screen.getPreference(i))
}
}
private fun setSummary(preference: Preference) {
when (preference) {
is UrlPreference -> {
preference.setSummary(preference.getUrl())
}
is ListPreference -> {
preference.setSummary(preference.entry)
}
is PreferenceGroup -> {
setSummaries(preference)
}
}
}
}
private fun Activity.restartApp() {
val intent = Intent(this, SplashActivity::class.java)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
intent.addFlags(
Intent.FLAG_ACTIVITY_NEW_TASK or
Intent.FLAG_ACTIVITY_CLEAR_TASK
)
}
startActivity(intent)
finish()
// Restart to begin with an unmodified Locale to follow system settings.
Runtime.getRuntime().exit(0)
}
@@ -0,0 +1,96 @@
package de.markusfisch.android.binaryeye.fragment
import android.os.Bundle
import android.support.v7.preference.PreferenceDialogFragmentCompat
import android.view.View
import android.widget.TextView
import de.markusfisch.android.binaryeye.R
import de.markusfisch.android.binaryeye.app.prefs
import de.markusfisch.android.binaryeye.database.Scan
import de.markusfisch.android.binaryeye.net.sendAsync
import de.markusfisch.android.binaryeye.preference.UrlPreference
class UrlDialogFragment : PreferenceDialogFragmentCompat() {
private var urlView: TextView? = null
private var testButton: TextView? = null
override fun onBindDialogView(view: View?) {
super.onBindDialogView(view)
urlView = view?.findViewById(R.id.url)
testButton = view?.findViewById(R.id.test_url)
testButton?.setOnClickListener {
testUrl(testButton)
}
urlView?.text = urlPreference().getUrl()
urlView?.hint = getString(
when (prefs.sendScanType) {
"0" -> R.string.url_hint_add_content
else -> R.string.url_hint
}
)
}
override fun onDialogClosed(positiveResult: Boolean) {
if (positiveResult) {
urlPreference().setUrl(getUrl())
}
}
private fun getUrl() = completeUrl(urlView?.text.toString())
private fun urlPreference() = preference as UrlPreference
private fun testUrl(textView: TextView?) {
val url = getUrl()
if (url.isEmpty()) {
return
}
textView ?: return
textView.text = ""
Scan(
"test",
null,
"none",
null,
null,
null,
null,
null,
null,
null,
null
).sendAsync(url, prefs.sendScanType) { code, body ->
textView.text = when {
code != null -> "$code"
body != null -> body
else -> getString(R.string.background_request_failed)
}
}
}
companion object {
fun newInstance(key: String): UrlDialogFragment {
val args = Bundle()
args.putString(ARG_KEY, key)
val fragment = UrlDialogFragment()
fragment.arguments = args
return fragment
}
}
}
private fun completeUrl(template: String): String {
var s = template.trim()
if (s.isEmpty()) {
return ""
}
if (!s.startsWith("http")) {
s = "http://${s}"
}
if (prefs.sendScanType == "0" &&
!s.matches(".*/[a-zA-Z._-]*\\?[a-zA-Z0-9_-]+=$".toRegex())
) {
s = "${s}?content="
}
return s
}
@@ -7,6 +7,8 @@ import android.graphics.Matrix
import android.graphics.RectF
import android.net.Uri
import java.io.IOException
import kotlin.math.max
import kotlin.math.min
import kotlin.math.roundToInt
fun loadImageUri(cr: ContentResolver, uri: Uri): Bitmap? = try {
@@ -51,12 +53,14 @@ fun crop(bitmap: Bitmap, rect: RectF, rotation: Float) = try {
val erected = erect(bitmap, rotation)
val w = erected.width
val h = erected.height
val x = max(0, (rect.left * w).roundToInt())
val y = max(0, (rect.top * h).roundToInt())
Bitmap.createBitmap(
erected,
(rect.left * w).roundToInt(),
(rect.top * h).roundToInt(),
(rect.width() * w).roundToInt(),
(rect.height() * h).roundToInt()
x,
y,
min(w - x, (rect.width() * w).roundToInt()),
min(h - y, (rect.height() * h).roundToInt())
)
} catch (e: OutOfMemoryError) {
null
@@ -0,0 +1,15 @@
package de.markusfisch.android.binaryeye.graphics
import android.content.Context
import android.graphics.DashPathEffect
import android.graphics.Paint
import android.support.v4.content.ContextCompat
import de.markusfisch.android.binaryeye.R
fun Context.getDashedBorderPaint() = Paint(Paint.ANTI_ALIAS_FLAG).apply {
val dp = resources.displayMetrics.density
color = ContextCompat.getColor(applicationContext, R.color.crop_bound)
style = Paint.Style.STROKE
strokeWidth = dp * 2f
pathEffect = DashPathEffect(floatArrayOf(10f * dp, 10f * dp), 0f)
}
@@ -7,15 +7,12 @@ import android.graphics.Point
import android.support.v4.content.ContextCompat
import de.markusfisch.android.binaryeye.R
class Candidates(context: Context) {
class Dots(context: Context) {
private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
private val radius = 8f * context.resources.displayMetrics.density
init {
paint.color = ContextCompat.getColor(
context,
R.color.candidate
)
paint.color = ContextCompat.getColor(context, R.color.dot)
paint.style = Paint.Style.FILL
}
@@ -0,0 +1,36 @@
package de.markusfisch.android.binaryeye.graphics
import android.content.res.Resources
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.Drawable
import android.os.Build
fun Resources.getBitmapFromDrawable(
resId: Int
): Bitmap = getBitmapFromDrawable(getDrawableCompat(resId))
fun Resources.getDrawableCompat(
resId: Int
): Drawable = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
getDrawable(resId, null)
} else {
@Suppress("DEPRECATION")
getDrawable(resId)
}
private fun getBitmapFromDrawable(drawable: Drawable): Bitmap {
if (drawable is BitmapDrawable) {
return drawable.bitmap
}
val bitmap = Bitmap.createBitmap(
drawable.intrinsicWidth,
drawable.intrinsicHeight,
Bitmap.Config.ARGB_8888
)
val canvas = Canvas(bitmap)
drawable.setBounds(0, 0, canvas.width, canvas.height)
drawable.draw(canvas)
return bitmap
}
@@ -7,31 +7,79 @@ import com.google.zxing.ResultPoint
import kotlin.math.roundToInt
fun mapResult(
width: Int,
height: Int,
frameWidth: Int,
frameHeight: Int,
frameOrientation: Int,
viewRect: Rect,
result: Result
): List<Point> = frameToView(width, height, viewRect).map(result.resultPoints)
): List<Point> = frameToView(
frameWidth,
frameHeight,
frameOrientation,
viewRect
).map(
result.resultPoints
)
fun frameToView(width: Int, height: Int, viewRect: Rect) = Mapping(
viewRect.width().toFloat() / width.toFloat(),
viewRect.height().toFloat() / height.toFloat(),
fun frameToView(
frameWidth: Int,
frameHeight: Int,
frameOrientation: Int,
viewRect: Rect
) = Mapping(
frameWidth,
frameHeight,
frameOrientation,
viewRect.width().toFloat(),
viewRect.height().toFloat(),
viewRect.left,
viewRect.top
)
fun isPortrait(orientation: Int) = orientation == 90 || orientation == 270
data class Mapping(
val ratioX: Float,
val ratioY: Float,
val frameWidth: Int,
val frameHeight: Int,
val frameOrientation: Int,
val viewWidth: Float,
val viewHeight: Float,
val offsetX: Int,
val offsetY: Int
) {
fun map(point: ResultPoint) = Point(
(point.x * ratioX).roundToInt() + offsetX,
(point.y * ratioY).roundToInt() + offsetY
)
fun map(resultPoint: ResultPoint): Point {
val point = Point(
resultPoint.x.roundToInt(),
resultPoint.y.roundToInt()
)
rotate(point)
val w: Int
val h: Int
if (isPortrait(frameOrientation)) {
w = frameHeight
h = frameWidth
} else {
w = frameWidth
h = frameHeight
}
val ratioX = viewWidth / w.toFloat()
val ratioY = viewHeight / h.toFloat()
point.set(
(point.x * ratioX).roundToInt() + offsetX,
(point.y * ratioY).roundToInt() + offsetY
)
return point
}
fun map(points: Array<ResultPoint?>): List<Point> =
// because ZXing apparently returns null in this array sometimes
// Because ZXing apparently returns null in this array sometimes.
points.filterNotNull().map { map(it) }
private fun rotate(point: Point) = when (frameOrientation) {
90 -> point.set(frameHeight - point.y, point.x)
180 -> point.set(frameWidth - point.x, frameHeight - point.y)
270 -> point.set(point.y, frameWidth - point.x)
else -> {
}
}
}
@@ -1,4 +1,4 @@
package de.markusfisch.android.binaryeye.app
package de.markusfisch.android.binaryeye.io
import android.app.Activity
import android.content.ContentValues
@@ -9,6 +9,7 @@ import android.provider.MediaStore
import android.support.annotation.MainThread
import android.widget.EditText
import de.markusfisch.android.binaryeye.R
import de.markusfisch.android.binaryeye.app.alertDialog
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
@@ -0,0 +1,122 @@
package de.markusfisch.android.binaryeye.net
import de.markusfisch.android.binaryeye.app.toHexString
import de.markusfisch.android.binaryeye.database.Scan
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.json.JSONObject
import java.io.BufferedReader
import java.io.IOException
import java.io.InputStream
import java.io.InputStreamReader
import java.net.HttpURLConnection
import java.net.ProtocolException
import java.net.URL
import java.net.URLEncoder
fun Scan.sendAsync(url: String, type: String, callback: (Int?, String?) -> Unit) {
CoroutineScope(Dispatchers.IO).launch(Dispatchers.IO) {
val response = send(url, type)
withContext(Dispatchers.Main) {
callback(response.code, response.body)
}
}
}
private fun Scan.send(url: String, type: String): Response {
return when (type) {
"1" -> request(
url + "?" + asUrlArguments()
)
"2" -> request(url) { con ->
con.requestMethod = "POST"
con.setRequestProperty("Content-Type", "application/x-www-form-urlencoded")
con.outputStream.apply {
write(asUrlArguments().toByteArray())
close()
}
}
"3" -> request(url) { con ->
con.requestMethod = "POST"
con.setRequestProperty("Content-Type", "application/json")
con.outputStream.apply {
write(asJson().toString().toByteArray())
close()
}
}
else -> request(
url + urlEncode(content)
)
}
}
private fun Scan.asUrlArguments(): String = getMap().map { (k, v) ->
"${k}=${urlEncode(v)}"
}.joinToString("&")
private fun Scan.asJson() = JSONObject().apply {
getMap().forEach { (k, v) -> put(k, v) }
}
private fun urlEncode(s: String) = URLEncoder.encode(s, "utf-8")
private fun Scan.getMap(): Map<String, String> = mapOf(
"content" to content,
"raw" to raw?.toHexString(),
"format" to format,
"errorCorrectionLevel" to errorCorrectionLevel,
"issueNumber" to issueNumber,
"orientation" to orientation,
"otherMetaData" to otherMetaData,
"pdf417ExtraMetaData" to pdf417ExtraMetaData,
"possibleCountry" to possibleCountry,
"suggestedPrice" to suggestedPrice,
"upcEanExtension" to upcEanExtension,
"timestamp" to timestamp
).filterNullValues()
private fun Map<String, String?>.filterNullValues() =
filterValues { it != null }.mapValues { it.value as String }
private fun request(
url: String,
writer: ((HttpURLConnection) -> Any)? = null
): Response {
var con: HttpURLConnection? = null
return try {
con = URL(url).openConnection() as HttpURLConnection
con.connectTimeout = 5000
writer?.invoke(con)
val body = con.inputStream.readHead()
Response(con.responseCode, body)
} catch (e: ProtocolException) {
Response(null, e.message)
} catch (e: IOException) {
try {
val body = con?.errorStream?.readHead() ?: e.message
Response(con?.responseCode, body)
} catch (e: IOException) {
Response(null, e.message)
}
} finally {
con?.disconnect()
}
}
private fun InputStream.readHead(): String {
val sb = StringBuilder()
val br = BufferedReader(InputStreamReader(this))
var line = br.readLine()
var i = 0
while (line != null && sb.length < 240) {
sb.append(line)
line = br.readLine()
++i
}
br.close()
return sb.toString()
}
private data class Response(val code: Int?, val body: String?)
@@ -0,0 +1,49 @@
package de.markusfisch.android.binaryeye.os
import android.os.Build
import android.os.VibrationEffect
import android.os.Vibrator
import de.markusfisch.android.binaryeye.app.prefs
private const val MILLISECONDS = 100L
private val errorPatternTimings = longArrayOf(
50L, 100L, 50L, 100L, 50L
)
private val errorPatternAmplitudes = intArrayOf(
255, 0, 255, 0, 255
)
fun Vibrator.vibrate() {
if (!prefs.vibrate) {
return
}
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
@Suppress("DEPRECATION")
vibrate(MILLISECONDS)
} else {
vibrate(
VibrationEffect.createOneShot(
MILLISECONDS,
VibrationEffect.DEFAULT_AMPLITUDE
)
)
}
}
fun Vibrator.error() {
if (!prefs.vibrate) {
return
}
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
@Suppress("DEPRECATION")
vibrate(errorPatternTimings, -1)
} else {
vibrate(
VibrationEffect.createWaveform(
errorPatternTimings,
errorPatternAmplitudes,
-1
)
)
}
}
@@ -7,14 +7,57 @@ import android.preference.PreferenceManager
class Preferences {
lateinit var preferences: SharedPreferences
var cropHandleX = -2 // -2 means set default roi.
set(value) {
apply(CROP_HANDLE_X, value)
field = value
}
var cropHandleY = -2
set(value) {
apply(CROP_HANDLE_Y, value)
field = value
}
var cropHandleOrientation = 0
set(value) {
apply(CROP_HANDLE_ORIENTATION, value)
field = value
}
var showCropHandle = true
set(value) {
apply(SHOW_CROP_HANDLE, value)
field = value
}
var zoomBySwiping = true
set(value) {
apply(ZOOM_BY_SWIPING, value)
field = value
}
var autoRotate = false
set(value) {
// Immediately save this setting before it shouldn't change
// on the fly while scanning.
commit(AUTO_ROTATE, value)
field = value
}
var tryHarder = false
set(value) {
apply(TRY_HARDER, value)
// Immediately save this setting because it's only ever read
// before the camera is opened.
commit(TRY_HARDER, value)
field = value
}
var bulkMode = false
set(value) {
// Immediately save this setting because it's only ever read
// before the camera is opened.
commit(BULK_MODE, value)
field = value
}
var showToastInBulkMode = true
set(value) {
// Immediately save this setting because it's only ever read
// before the camera is opened.
commit(SHOW_TOAST_IN_BULK_MODE, value)
field = value
}
var vibrate = true
@@ -32,6 +75,11 @@ class Preferences {
apply(IGNORE_CONSECUTIVE_DUPLICATES, value)
field = value
}
var copyImmediately = false
set(value) {
apply(COPY_IMMEDIATELY, value)
field = value
}
var openImmediately = false
set(value) {
apply(OPEN_IMMEDIATELY, value)
@@ -47,11 +95,36 @@ class Preferences {
apply(SHOW_HEX_DUMP, value)
field = value
}
var closeAutomatically = false
set(value) {
apply(CLOSE_AUTOMATICALLY, value)
field = value
}
var defaultSearchUrl = ""
set(value) {
apply(DEFAULT_SEARCH_URL, value)
field = value
}
var openWithUrl: String = ""
set(value) {
apply(OPEN_WITH_URL, value)
field = value
}
var sendScanUrl: String = ""
set(value) {
apply(SEND_SCAN_URL, value)
field = value
}
var sendScanType: String = "0"
set(value) {
apply(SEND_SCAN_TYPE, value)
field = value
}
var customLocale: String = ""
set(value) {
apply(CUSTOM_LOCALE, value)
field = value
}
var indexOfLastSelectedFormat: Int = 0
set(value) {
apply(INDEX_OF_LAST_SELECTED_FORMAT, value)
@@ -59,8 +132,8 @@ class Preferences {
}
var forceCompat: Boolean = false
set(value) {
// since the app may be about to crash when forceCompat is set,
// it's necessary to `commit()` this synchronously
// Since the app may be about to crash when forceCompat is set,
// it's necessary to `commit()` this synchronously.
commit(FORCE_COMPAT, value)
field = value
}
@@ -71,23 +144,59 @@ class Preferences {
}
fun update() {
cropHandleX = preferences.getInt(CROP_HANDLE_X, cropHandleX)
cropHandleY = preferences.getInt(CROP_HANDLE_Y, cropHandleY)
cropHandleOrientation = preferences.getInt(
CROP_HANDLE_ORIENTATION,
cropHandleOrientation
)
showCropHandle = preferences.getBoolean(
SHOW_CROP_HANDLE,
showCropHandle
)
zoomBySwiping = preferences.getBoolean(ZOOM_BY_SWIPING, zoomBySwiping)
autoRotate = preferences.getBoolean(AUTO_ROTATE, autoRotate)
tryHarder = preferences.getBoolean(TRY_HARDER, tryHarder)
bulkMode = preferences.getBoolean(BULK_MODE, bulkMode)
showToastInBulkMode = preferences.getBoolean(
SHOW_TOAST_IN_BULK_MODE,
showToastInBulkMode
)
vibrate = preferences.getBoolean(VIBRATE, vibrate)
useHistory = preferences.getBoolean(USE_HISTORY, useHistory)
ignoreConsecutiveDuplicates = preferences.getBoolean(
IGNORE_CONSECUTIVE_DUPLICATES,
ignoreConsecutiveDuplicates
)
copyImmediately = preferences.getBoolean(
COPY_IMMEDIATELY,
copyImmediately
)
openImmediately = preferences.getBoolean(
OPEN_IMMEDIATELY,
openImmediately
)
showMetaData = preferences.getBoolean(SHOW_META_DATA, showMetaData)
showHexDump = preferences.getBoolean(SHOW_HEX_DUMP, showHexDump)
closeAutomatically = preferences.getBoolean(
CLOSE_AUTOMATICALLY,
closeAutomatically
)
preferences.getString(DEFAULT_SEARCH_URL, defaultSearchUrl)?.also {
defaultSearchUrl = it
}
preferences.getString(OPEN_WITH_URL, openWithUrl)?.also {
openWithUrl = it
}
preferences.getString(SEND_SCAN_URL, sendScanUrl)?.also {
sendScanUrl = it
}
preferences.getString(SEND_SCAN_TYPE, sendScanType)?.also {
sendScanType = it
}
preferences.getString(CUSTOM_LOCALE, customLocale)?.also {
customLocale = it
}
indexOfLastSelectedFormat = preferences.getInt(
INDEX_OF_LAST_SELECTED_FORMAT,
indexOfLastSelectedFormat
@@ -115,15 +224,28 @@ class Preferences {
}
companion object {
const val CROP_HANDLE_X = "crop_handle_x"
const val CROP_HANDLE_Y = "crop_handle_y"
const val CROP_HANDLE_ORIENTATION = "crop_handle_orientation"
const val SHOW_CROP_HANDLE = "show_crop_handle"
const val ZOOM_BY_SWIPING = "zoom_by_swiping"
const val AUTO_ROTATE = "auto_rotate"
const val TRY_HARDER = "try_harder"
const val BULK_MODE = "bulk_mode"
const val SHOW_TOAST_IN_BULK_MODE = "show_toast_in_bulk_mode"
const val VIBRATE = "vibrate"
const val USE_HISTORY = "use_history"
const val IGNORE_CONSECUTIVE_DUPLICATES = "ignore_consecutive_duplicates"
const val OPEN_IMMEDIATELY = "open_immediately"
const val SHOW_HEX_DUMP = "show_hex_dump"
const val COPY_IMMEDIATELY = "copy_immediately"
const val SHOW_META_DATA = "show_meta_data"
const val SHOW_HEX_DUMP = "show_hex_dump"
const val CLOSE_AUTOMATICALLY = "close_automatically"
const val DEFAULT_SEARCH_URL = "default_search_url"
const val OPEN_WITH_URL = "open_with_url"
const val SEND_SCAN_URL = "send_scan_url"
const val SEND_SCAN_TYPE = "send_scan_type"
const val CUSTOM_LOCALE = "custom_locale"
const val INDEX_OF_LAST_SELECTED_FORMAT = "index_of_last_selected_format"
const val FORCE_COMPAT = "force_compat"
}
@@ -0,0 +1,37 @@
package de.markusfisch.android.binaryeye.preference
import android.content.Context
import android.support.v7.preference.DialogPreference
import android.util.AttributeSet
import de.markusfisch.android.binaryeye.R
class UrlPreference(
context: Context?,
attrs: AttributeSet?
) : DialogPreference(context, attrs) {
private var url: String? = null
init {
dialogLayoutResource = R.layout.dialog_url
}
fun getUrl() = url
fun setUrl(url: String) {
this.url = url
persistString(url)
}
override fun onSetInitialValue(
restorePersistedValue: Boolean,
defaultValue: Any?
) {
setUrl(
if (restorePersistedValue) {
getPersistedString(url)
} else {
defaultValue as String
}
)
}
}
@@ -0,0 +1,42 @@
package de.markusfisch.android.binaryeye.rs
import android.graphics.Bitmap
import android.graphics.Canvas
import android.support.v8.renderscript.Allocation
import android.support.v8.renderscript.RenderScript
fun fixTransparency(rs: RenderScript, bitmap: Bitmap?): Bitmap? {
bitmap ?: return null
val result = analyze(rs, bitmap)
return if (result.hasTransparentPixels) {
val copy = Bitmap.createBitmap(
bitmap.width,
bitmap.height,
bitmap.config
)
Canvas(copy).apply {
drawColor(result.backgroundColor)
drawBitmap(bitmap, 0f, 0f, null)
}
copy
} else {
bitmap
}
}
private fun analyze(rs: RenderScript, bitmap: Bitmap): Result {
val analyzeScript = ScriptC_analyze(rs)
val bitmapAllocation = Allocation.createFromBitmap(rs, bitmap)
val result = analyzeScript.reduce_analyze(bitmapAllocation).get()
bitmapAllocation.destroy()
analyzeScript.destroy()
return Result(
result.x > 0,
if (result.y > 0) 0xff000000.toInt() else 0xffffffff.toInt()
)
}
private data class Result(
val hasTransparentPixels: Boolean,
val backgroundColor: Int
)
@@ -1,25 +1,28 @@
package de.markusfisch.android.binaryeye.rs
import android.content.Context
import android.graphics.Rect
import android.support.v8.renderscript.*
import de.markusfisch.android.binaryeye.renderscript.ScriptC_rotator
import kotlin.math.max
import kotlin.math.roundToInt
private const val SCALE_FACTOR = .75f
class Preprocessor(
context: Context,
rs: RenderScript,
width: Int,
height: Int,
private val orientation: Int
private val roi: Rect?
) {
var outWidth = 0
var outHeight = 0
private val rs = RenderScript.create(context)
private val resizeScript = ScriptIntrinsicResize.create(rs)
private val rotatorScript = ScriptC_rotator(rs)
private val rotateScript = ScriptC_rotate(rs)
private var yuvType: Type? = null
private var yuvAlloc: Allocation? = null
private var roiType: Type? = null
private var roiAlloc: Allocation? = null
private var resizedType: Type? = null
private var resizedAlloc: Allocation? = null
private var rotatedType: Type? = null
@@ -38,15 +41,42 @@ class Preprocessor(
Allocation.USAGE_SCRIPT
)
val f = .75f
var w = (width * f).roundToInt()
var h = (height * f).roundToInt()
if (roi != null) {
val roiWidth = roi.width()
val roiHeight = roi.height()
roiType = Type.createXY(
rs,
Element.U8(rs),
roiWidth,
roiHeight
)
roiAlloc = Allocation.createTyped(
rs,
roiType,
Allocation.USAGE_SCRIPT
)
outWidth = (roiWidth * SCALE_FACTOR).roundToInt()
outHeight = (roiHeight * SCALE_FACTOR).roundToInt()
// Make sure the dimensions are always a multiple of 4.
outWidth -= outWidth % 4
outHeight -= outHeight % 4
// Make sure the dimensions are always greater than 4.
outWidth = max(4, outWidth)
outHeight = max(4, outHeight)
} else {
outWidth = (width * SCALE_FACTOR).roundToInt()
outHeight = (height * SCALE_FACTOR).roundToInt()
}
resizedType = Type.createXY(
rs,
Element.U8(rs),
w,
h
outWidth,
outHeight
)
resizedAlloc = Allocation.createTyped(
rs,
@@ -54,28 +84,17 @@ class Preprocessor(
Allocation.USAGE_SCRIPT
)
if (orientation == 90 || orientation == 270) {
val tmp = w
w = h
h = tmp
}
outWidth = w
outHeight = h
if (orientation != 0) {
rotatedType = Type.createXY(
rs,
Element.U8(rs),
w,
h
)
rotatedAlloc = Allocation.createTyped(
rs,
rotatedType,
Allocation.USAGE_SCRIPT
)
}
rotatedType = Type.createXY(
rs,
Element.U8(rs),
outHeight,
outWidth
)
rotatedAlloc = Allocation.createTyped(
rs,
rotatedType,
Allocation.USAGE_SCRIPT
)
}
fun destroy() {
@@ -83,6 +102,10 @@ class Preprocessor(
yuvType = null
yuvAlloc?.destroy()
yuvAlloc = null
roiType?.destroy()
roiType = null
roiAlloc?.destroy()
roiAlloc = null
resizedType?.destroy()
resizedType = null
resizedAlloc?.destroy()
@@ -92,38 +115,42 @@ class Preprocessor(
rotatedAlloc?.destroy()
rotatedAlloc = null
resizeScript.destroy()
rotatorScript.destroy()
rs.destroy()
rotateScript.destroy()
}
fun process(frame: ByteArray) {
fun resizeOnly(frame: ByteArray) {
resize(frame)
resizedAlloc?.copyTo(frame)
}
fun resizeAndRotate(frame: ByteArray) {
resize(frame)
val t = resizedType ?: return
rotateScript._inImage = resizedAlloc
rotateScript._inWidth = t.x
rotateScript._inHeight = t.y
rotateScript.forEach_rotate90(
rotatedAlloc, // Ignored in kernel, just to satisfy forEach.
rotatedAlloc
)
rotatedAlloc?.copyTo(frame)
}
private fun resize(frame: ByteArray) {
yuvAlloc?.copyFrom(frame)
resizeScript.setInput(yuvAlloc)
resizeScript.forEach_bicubic(resizedAlloc)
val t = resizedType
if (t == null || orientation == 0) {
resizedAlloc?.copyTo(frame)
} else {
rotatorScript._inImage = resizedAlloc
rotatorScript._inWidth = t.x
rotatorScript._inHeight = t.y
when (orientation) {
90 -> rotatorScript.forEach_rotate90(
rotatedAlloc,
rotatedAlloc
)
180 -> rotatorScript.forEach_rotate180(
rotatedAlloc,
rotatedAlloc
)
270 -> rotatorScript.forEach_rotate270(
rotatedAlloc,
rotatedAlloc
resizeScript.setInput(
if (roi != null) {
roiAlloc?.copy2DRangeFrom(
0, 0,
roi.width(), roi.height(),
yuvAlloc,
max(0, roi.left), max(0, roi.top)
)
roiAlloc
} else {
yuvAlloc
}
rotatedAlloc?.copyTo(frame)
}
)
resizeScript.forEach_bicubic(resizedAlloc)
}
}
@@ -0,0 +1,18 @@
package de.markusfisch.android.binaryeye.service
import android.content.Intent
import android.os.Build
import android.service.quicksettings.TileService
import android.support.annotation.RequiresApi
import de.markusfisch.android.binaryeye.activity.CameraActivity
@RequiresApi(Build.VERSION_CODES.N)
class ScanTileService : TileService() {
@RequiresApi(Build.VERSION_CODES.N)
override fun onClick() {
super.onClick()
val intent = Intent(applicationContext, CameraActivity::class.java)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
startActivityAndCollapse(intent)
}
}
@@ -1,4 +1,4 @@
package de.markusfisch.android.binaryeye.app
package de.markusfisch.android.binaryeye.view
import android.content.Context
import android.view.View
@@ -1,4 +1,4 @@
package de.markusfisch.android.binaryeye.app
package de.markusfisch.android.binaryeye.view
import android.content.Context
import android.content.ContextWrapper
@@ -6,35 +6,44 @@ import android.graphics.drawable.ColorDrawable
import android.os.Build
import android.support.v4.content.ContextCompat
import android.support.v7.app.AppCompatActivity
import android.support.v7.widget.LinearLayoutManager
import android.support.v7.widget.RecyclerView
import android.view.View
import android.view.WindowManager
import android.widget.AbsListView
import de.markusfisch.android.binaryeye.R
val systemBarScrollListener = object : AbsListView.OnScrollListener {
val systemBarListViewScrollListener = object : AbsListView.OnScrollListener {
override fun onScroll(
view: AbsListView,
firstVisibleItem: Int,
visibleItemCount: Int,
totalItemCount: Int
) {
// give Android some time to settle down before running this,
// not putting it on the queue makes it only work sometimes
// Give Android some time to settle down before running this,
// not putting it on the queue makes it only work sometimes.
view.post {
val scrolled = firstVisibleItem > 0 ||
(totalItemCount > 0 && firstChildScrolled(view))
val scrollable = if (scrolled) {
true
} else {
totalItemCount > 0 && lastChildOutOfView(view)
}
val scrollable = scrolled || totalItemCount > 0 && lastChildOutOfView(view)
colorSystemAndToolBars(view.context, scrolled, scrollable)
}
}
override fun onScrollStateChanged(
view: AbsListView,
scrollState: Int
) {
override fun onScrollStateChanged(view: AbsListView, scrollState: Int) {
}
}
val systemBarRecyclerViewScrollListener = object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
val layoutManager = recyclerView.layoutManager as LinearLayoutManager
val scrolled = layoutManager.findFirstCompletelyVisibleItemPosition() != 0
val scrollable = scrolled || layoutManager.findLastVisibleItemPosition() <
recyclerView.adapter.itemCount - 1
colorSystemAndToolBars(recyclerView.context, scrolled, scrollable)
}
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
}
}
@@ -45,12 +54,17 @@ private fun firstChildScrolled(listView: AbsListView): Boolean {
private fun lastChildOutOfView(listView: AbsListView): Boolean {
val child = listView.getChildAt(listView.lastVisiblePosition)
return child.bottom >= listView.height
return child != null && child.bottom >= listView.height
}
fun initSystemBars(activity: AppCompatActivity) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
activity.window.decorView.systemUiVisibility =
// Keeps the soft keyboard from repositioning the layout.
val window = activity.window
window.setSoftInputMode(
WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE
)
window.decorView.systemUiVisibility =
View.SYSTEM_UI_FLAG_LAYOUT_STABLE or
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
@@ -95,11 +109,11 @@ fun colorSystemAndToolBars(
}
activity.supportActionBar?.setBackgroundDrawable(
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
// avoid allocation on Honeycomb and better
// Avoid allocation on Honeycomb and better.
actionBarBackground.color = topColor
actionBarBackground
} else {
// ColorDrawable.setColor() doesn't exist pre Honeycomb
// ColorDrawable.setColor() doesn't exist pre Honeycomb.
ColorDrawable(topColor)
}
)
@@ -29,10 +29,10 @@ fun View.doOnApplyWindowInsets(f: (View, Rect) -> Unit) {
f(v, insetsWithToolbar(insets))
insets
}
// it's important to explicitly request the insets (again) in
// It's important to explicitly request the insets (again) in
// case the view was created in Fragment.onCreateView() because
// setOnApplyWindowInsetsListener() won't fire when the view
// isn't attached
// isn't attached.
requestApplyInsetsWhenAttached()
}
}
@@ -4,7 +4,7 @@ import android.content.Context
import android.graphics.Canvas
import android.graphics.Rect
import android.util.AttributeSet
import de.markusfisch.android.binaryeye.app.colorSystemAndToolBars
import de.markusfisch.android.binaryeye.view.colorSystemAndToolBars
import de.markusfisch.android.scalingimageview.widget.ScalingImageView
open class ConfinedScalingImageView : ScalingImageView {
@@ -3,7 +3,7 @@ package de.markusfisch.android.binaryeye.widget
import android.content.Context
import android.util.AttributeSet
import android.widget.ScrollView
import de.markusfisch.android.binaryeye.app.colorSystemAndToolBars
import de.markusfisch.android.binaryeye.view.colorSystemAndToolBars
class ConfinedScrollView : ScrollView {
private var scrollable = false
@@ -23,8 +23,8 @@ class ConfinedScrollView : ScrollView {
) {
super.onLayout(changed, left, top, right, bottom)
if (changed) {
// give Android some time to settle down before running this,
// not putting it on the queue makes it only work sometimes
// Give Android some time to settle down before running this,
// not putting it on the queue makes it only work sometimes.
post {
getChildAt(0)?.also { child ->
scrollable = height < child.height + paddingTop + paddingBottom
@@ -1,56 +1,18 @@
package de.markusfisch.android.binaryeye.widget
import android.content.Context
import android.graphics.*
import android.support.v4.content.ContextCompat
import android.graphics.Canvas
import android.graphics.Rect
import android.graphics.RectF
import android.util.AttributeSet
import de.markusfisch.android.binaryeye.R
import de.markusfisch.android.binaryeye.graphics.Candidates
import kotlin.math.roundToInt
class CropImageView(context: Context, attr: AttributeSet) :
ConfinedScalingImageView(context, attr) {
val windowInsets = Rect()
var onScan: (() -> Unit)? = null
var onScan: (() -> List<Point>?)? = null
private val candidates = Candidates(context)
private val boundsPaint = Paint(Paint.ANTI_ALIAS_FLAG)
private val lastMappedRect = RectF()
private val dp = context.resources.displayMetrics.density
private val padding: Int = (dp * 24f).roundToInt()
private val onScanRunnable = Runnable {
onScan?.invoke()?.also {
candidatePoints = it
invalidate()
}
}
private var candidatePoints: List<Point>? = null
init {
boundsPaint.color = ContextCompat.getColor(context, R.color.crop_bound)
boundsPaint.style = Paint.Style.STROKE
boundsPaint.strokeWidth = dp * 2f
boundsPaint.pathEffect = DashPathEffect(floatArrayOf(dp * 10f, dp * 10f), 0f)
scaleType = ScaleType.CENTER_CROP
}
override fun onLayout(
changed: Boolean,
left: Int,
top: Int,
right: Int,
bottom: Int
) {
super.onLayout(changed, left, top, right, bottom)
setBoundsWithPadding(
left + windowInsets.left,
top + windowInsets.top,
right - windowInsets.right,
bottom - windowInsets.bottom
)
}
private val onScanRunnable = Runnable { onScan?.invoke() }
fun getBoundsRect() = Rect(
bounds.left.roundToInt(),
@@ -59,40 +21,12 @@ class CropImageView(context: Context, attr: AttributeSet) :
bounds.bottom.roundToInt()
)
private fun setBoundsWithPadding(
left: Int,
top: Int,
right: Int,
bottom: Int
) {
val width = right - left
val height = bottom - top
val size = if (width < height)
width - padding * 2
else
height - padding * 2
val hpad = (width - size) / 2
val vpad = (height - size) / 2
setBounds(
left.toFloat() + hpad,
top.toFloat() + vpad,
right.toFloat() - hpad,
bottom.toFloat() - vpad
)
center(bounds)
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
canvas.drawRect(bounds, boundsPaint)
candidatePoints?.let {
candidates.draw(canvas, it)
}
candidatePoints = null
val mr = mappedRect ?: return
if (mr != lastMappedRect) {
removeCallbacks(onScanRunnable)
postDelayed(onScanRunnable, 500)
postDelayed(onScanRunnable, 300)
lastMappedRect.set(mr)
}
}
@@ -1,20 +1,70 @@
package de.markusfisch.android.binaryeye.widget
import android.annotation.SuppressLint
import android.content.Context
import android.graphics.Canvas
import android.graphics.Point
import android.graphics.PorterDuff
import android.graphics.*
import android.os.Build
import android.os.Parcel
import android.os.Parcelable
import android.util.AttributeSet
import android.view.MotionEvent
import android.view.View
import de.markusfisch.android.binaryeye.graphics.Candidates
import de.markusfisch.android.binaryeye.R
import de.markusfisch.android.binaryeye.app.prefs
import de.markusfisch.android.binaryeye.graphics.Dots
import de.markusfisch.android.binaryeye.graphics.getBitmapFromDrawable
import de.markusfisch.android.binaryeye.graphics.getDashedBorderPaint
import kotlin.math.abs
import kotlin.math.max
import kotlin.math.min
import kotlin.math.roundToInt
class DetectorView : View {
private val candidates = Candidates(context)
val currentOrientation = resources.configuration.orientation
val roi = Rect()
var onRoiChange: (() -> Unit)? = null
var onRoiChanged: (() -> Unit)? = null
private val dots = Dots(context)
private val invalidateRunnable: Runnable = Runnable {
marks = null
invalidate()
}
private val roiPaint = context.getDashedBorderPaint()
private val handleBitmap = resources.getBitmapFromDrawable(
R.drawable.button_crop
)
private val handleXRadius = handleBitmap.width / 2
private val handleYRadius = handleBitmap.height / 2
private val handlePos = Point(-1, -1)
private val handleHome = Point()
private val center = Point()
private val touchDown = Point()
private val distToFull: Int
private val minMoveThresholdSq: Int
private val cornerRadius: Int
private val fabHeight: Int
private val padding: Int
private var marks: List<Point>? = null
private var handleGrabbed = false
private var handleActive = false
private var minY = 0
private var maxY = 0
private var minDist = 0
private var shadeColor = 0
init {
val dp = context.resources.displayMetrics.density
distToFull = (24f * dp).roundToInt()
val minMoveThreshold = (8f * dp).roundToInt()
minMoveThresholdSq = minMoveThreshold * minMoveThreshold
cornerRadius = (8f * dp).roundToInt()
fabHeight = (92f * dp).roundToInt()
padding = (20f * dp).roundToInt()
isSaveEnabled = true
}
constructor(context: Context, attrs: AttributeSet) :
super(context, attrs)
@@ -22,6 +72,23 @@ class DetectorView : View {
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) :
super(context, attrs, defStyleAttr)
fun setCropHandlePos(x: Int, y: Int, orientation: Int) {
if (orientation == currentOrientation) {
handlePos.set(x, y)
} else {
handlePos.set(y, x)
}
if (x > -1) {
handleActive = true
}
}
fun getCropHandlePos() = if (handleActive) {
handlePos
} else {
Point(-1, -1)
}
fun mark(points: List<Point>) {
marks = points
invalidate()
@@ -29,10 +96,289 @@ class DetectorView : View {
postDelayed(invalidateRunnable, 500)
}
override fun onSaveInstanceState(): Parcelable? {
if (!handleActive) {
return super.onSaveInstanceState()
}
return SavedState(super.onSaveInstanceState()).apply {
savedHandlePos.set(getCropHandlePos())
savedOrientation = currentOrientation
}
}
override fun onRestoreInstanceState(state: Parcelable) {
super.onRestoreInstanceState(
if (state is SavedState) {
setCropHandlePos(
state.savedHandlePos.x,
state.savedHandlePos.y,
state.savedOrientation
)
state.superState
} else {
state
}
)
}
@SuppressLint("ClickableViewAccessibility")
override fun onTouchEvent(event: MotionEvent?): Boolean {
event ?: return super.onTouchEvent(event)
val x = event.x.roundToInt()
val y = event.y.roundToInt()
return when (event.actionMasked) {
MotionEvent.ACTION_DOWN -> {
if (prefs.showCropHandle) {
touchDown.set(x, y)
handleGrabbed = abs(x - handlePos.x) < handleXRadius &&
abs(y - handlePos.y) < handleYRadius
if (handleGrabbed) {
onRoiChange?.invoke()
}
handleGrabbed
} else {
false
}
}
MotionEvent.ACTION_MOVE -> {
if (handleGrabbed) {
handlePos.set(x, y)
handleActive =
handleActive or (distSq(handlePos, touchDown) > minMoveThresholdSq)
if (handleActive) {
updateClipRect()
invalidate()
}
true
} else {
false
}
}
MotionEvent.ACTION_CANCEL -> {
if (handleGrabbed) {
snap(x, y)
handleGrabbed = false
}
false
}
MotionEvent.ACTION_UP -> {
if (handleGrabbed) {
if (!handleActive) {
setHandleToDefaultRoi()
} else {
snap(x, y)
}
if (handleActive) {
updateClipRect()
invalidate()
}
onRoiChanged?.invoke()
handleGrabbed = false
}
false
}
else -> super.onTouchEvent(event)
}
}
private fun setHandleToDefaultRoi() {
val mn = min(center.x, center.y) * .8f
handlePos.set(
(center.x + mn).roundToInt(),
(center.y + mn).roundToInt()
)
handleActive = true
}
private fun snap(x: Int, y: Int) {
val cx = clampX(x)
val cy = clampY(y)
val dx = abs(cx - center.x)
val dy = abs(cy - center.y)
// check if handle is close to the vertical or horizontal center line
if (dx < distToFull ||
dy < distToFull ||
// check if handle is close to a screen corner
((abs(cy - minY) < distToFull || abs(maxY - cy) < distToFull) &&
abs(dx - center.x) < distToFull)
) {
reset()
invalidate()
}
}
private fun reset() {
handlePos.set(handleHome)
handleActive = false
roi.set(0, 0, 0, 0)
}
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
super.onLayout(changed, left, top, right, bottom)
val width = right - left
val height = bottom - top
center.set(
left + (width / 2),
top + (height / 2)
)
minY = padding * 2
maxY = height - minY
handleHome.set(
width - handleXRadius - paddingRight - padding,
height - handleYRadius - paddingBottom - fabHeight
)
if (handlePos.x == -2) {
setHandleToDefaultRoi()
}
if (handleActive) {
updateClipRect()
} else {
reset()
}
}
override fun onDraw(canvas: Canvas) {
canvas.drawColor(0, PorterDuff.Mode.CLEAR)
if (handleActive) {
drawClip(canvas)
}
marks?.let {
candidates.draw(canvas, it)
dots.draw(canvas, it)
}
if (prefs.showCropHandle) {
canvas.drawBitmap(
handleBitmap,
(handlePos.x - handleXRadius).toFloat(),
(handlePos.y - handleYRadius).toFloat(),
null
)
}
}
private fun drawClip(canvas: Canvas) {
if (minDist < 1) {
return
}
// canvas.clipRect() doesn't work reliably below KITKAT.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
val radius = min(minDist / 2, cornerRadius).toFloat()
canvas.save()
canvas.clipOutPathCompat(
calculateRoundedRectPath(
roi.left.toFloat(),
roi.top.toFloat(),
roi.right.toFloat(),
roi.bottom.toFloat(),
radius,
radius
)
)
canvas.drawColor(shadeColor)
canvas.restore()
} else {
canvas.drawRect(roi, roiPaint)
}
}
private fun updateClipRect() {
clampHandlePos()
val dx = abs(handlePos.x - center.x)
val dy = abs(handlePos.y - center.y)
minDist = min(dx, dy)
shadeColor = (min(
1f,
minDist.toFloat() / distToFull.toFloat()
) * 128f).toInt() shl 24
roi.set(
center.x - dx,
center.y - dy,
center.x + dx,
center.y + dy
)
}
private fun clampHandlePos() {
handlePos.x = clampX(handlePos.x)
handlePos.y = clampY(handlePos.y)
}
private fun clampX(x: Int) = min(center.x * 2, max(0, x))
private fun clampY(y: Int) = min(maxY, max(minY, y))
internal class SavedState : BaseSavedState {
val savedHandlePos = Point()
var savedOrientation = 0
constructor(superState: Parcelable?) : super(superState)
private constructor(parcel: Parcel) : super(parcel) {
savedHandlePos.set(
parcel.readInt(),
parcel.readInt()
)
savedOrientation = parcel.readInt()
}
override fun writeToParcel(out: Parcel, flags: Int) {
super.writeToParcel(out, flags)
out.writeInt(savedHandlePos.x)
out.writeInt(savedHandlePos.y)
out.writeInt(savedOrientation)
}
companion object {
@JvmField
val CREATOR = object : Parcelable.Creator<SavedState> {
override fun createFromParcel(source: Parcel) = SavedState(source)
override fun newArray(size: Int): Array<SavedState?> = arrayOfNulls(size)
}
}
}
}
private fun distSq(a: Point, b: Point): Int {
val dx = a.x - b.x
val dy = a.y - b.y
return dx * dx + dy * dy
}
private fun Point.set(point: Point) {
x = point.x
y = point.y
}
private fun Canvas.clipOutPathCompat(path: Path) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
clipOutPath(path)
} else {
@Suppress("DEPRECATION")
clipPath(path, Region.Op.DIFFERENCE)
}
}
private fun calculateRoundedRectPath(
left: Float,
top: Float,
right: Float,
bottom: Float,
rx: Float,
ry: Float
): Path {
val width = right - left
val height = bottom - top
val widthMinusCorners = width - 2 * rx
val heightMinusCorners = height - 2 * ry
return Path().apply {
moveTo(right, top + ry)
rQuadTo(0f, -ry, -rx, -ry)
rLineTo(-widthMinusCorners, 0f)
rQuadTo(-rx, 0f, -rx, ry)
rLineTo(0f, heightMinusCorners)
rQuadTo(0f, ry, rx, ry)
rLineTo(widthMinusCorners, 0f)
rQuadTo(rx, 0f, rx, -ry)
rLineTo(0f, -heightMinusCorners)
close()
}
}
@@ -4,13 +4,19 @@ import android.content.Context
import android.widget.Toast
fun Context.toast(message: Int) = Toast.makeText(
this,
applicationContext,
message,
Toast.LENGTH_LONG
).show()
fun Context.toast(message: String) = Toast.makeText(
this,
message,
applicationContext,
message.ellipsize(128),
Toast.LENGTH_LONG
).show()
private fun String.ellipsize(max: Int) = if (length < max) {
this
} else {
"${take(max)}"
}
@@ -2,6 +2,7 @@ package de.markusfisch.android.binaryeye.zxing
import android.graphics.Bitmap
import com.google.zxing.*
import com.google.zxing.common.BitMatrix
import com.google.zxing.common.HybridBinarizer
import java.util.*
@@ -10,7 +11,7 @@ class Zxing(possibleResultPoint: ResultPointCallback? = null) {
private val multiFormatReader = MultiFormatReader()
init {
val decodeFormats = EnumSet.noneOf<BarcodeFormat>(
val decodeFormats = EnumSet.noneOf(
BarcodeFormat::class.java
)
decodeFormats.addAll(
@@ -112,49 +113,110 @@ class Zxing(possibleResultPoint: ResultPointCallback? = null) {
val bitmap = BinaryBitmap(HybridBinarizer(source))
return try {
multiFormatReader.decode(bitmap, hints)
} catch (e: ReaderException) {
} catch (e: Exception) {
// Usually it's bad practice to blindly catch *all* exceptions
// because this can hide problems we want to know about. But
// since ZXing has some errors (like all software) and I don't
// want this app to break in the hands of my users, which won't
// see the stack trace anyway, it's better to just catch all
// exceptions here and live on.
null
} finally {
multiFormatReader.reset()
}
}
}
companion object {
private const val BLACK = 0xff000000.toInt()
private const val WHITE = 0xffffffff.toInt()
private const val BLACK = 0xff000000.toInt()
private const val WHITE = 0xffffffff.toInt()
fun encodeAsBitmap(
text: String,
format: BarcodeFormat,
width: Int,
height: Int
): Bitmap? {
val hints = EnumMap<EncodeHintType, Any>(EncodeHintType::class.java)
hints[EncodeHintType.CHARACTER_SET] = "utf-8"
val result = MultiFormatWriter().encode(
text,
format,
width,
height,
hints
)
val w = result.width
val h = result.height
val pixels = IntArray(w * h)
var offset = 0
for (y in 0 until h) {
for (x in 0 until w) {
pixels[offset + x] = if (result.get(x, y)) {
BLACK
} else {
WHITE
}
}
offset += w
fun encodeAsBitmap(
text: String,
format: BarcodeFormat,
width: Int,
height: Int,
hints: EnumMap<EncodeHintType, Any>? = null
): Bitmap? {
val bitMatrix = encode(text, format, hints, width, height)
val w = bitMatrix.width
val h = bitMatrix.height
val pixels = IntArray(w * h)
var offset = 0
for (y in 0 until h) {
for (x in 0 until w) {
pixels[offset + x] = if (bitMatrix.get(x, y)) {
BLACK
} else {
WHITE
}
}
offset += w
}
val bitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888)
bitmap.setPixels(pixels, 0, w, 0, 0, w, h)
return bitmap
}
fun encodeAsSvg(
text: String,
format: BarcodeFormat,
hints: EnumMap<EncodeHintType, Any>? = null
): String {
val bitMatrix = encode(text, format, hints)
val sb = StringBuilder()
val w = bitMatrix.width
var h = bitMatrix.height
val moduleHeight = if (h == 1) w / 2 else 1
for (y in 0 until h) {
for (x in 0 until w) {
if (bitMatrix.get(x, y)) {
sb.append(" M${x},${y}h1v${moduleHeight}h-1z")
}
val bitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888)
bitmap.setPixels(pixels, 0, w, 0, 0, w, h)
return bitmap
}
}
h *= moduleHeight
return """<svg width="$w" height="$h"
viewBox="0 0 $w $h"
xmlns="http://www.w3.org/2000/svg">
<path d="$sb"/>
</svg>
"""
}
fun encodeAsText(
text: String,
format: BarcodeFormat,
hints: EnumMap<EncodeHintType, Any>? = null
): String {
val bitMatrix = encode(text, format, hints)
val w = bitMatrix.width
val h = bitMatrix.height
val sb = StringBuilder()
for (y in 0 until h) {
for (x in 0 until w) {
sb.append(if (bitMatrix.get(x, y)) "" else " ")
}
sb.append("\n")
}
return sb.toString()
}
private fun encode(
text: String,
format: BarcodeFormat,
encodeHints: EnumMap<EncodeHintType, Any>? = null,
width: Int = 0,
height: Int = 0
): BitMatrix {
val hints = encodeHints ?: EnumMap<EncodeHintType, Any>(EncodeHintType::class.java)
if (!hints.contains(EncodeHintType.CHARACTER_SET)) {
hints[EncodeHintType.CHARACTER_SET] = "utf-8"
}
return MultiFormatWriter().encode(
text,
format,
width,
height,
hints
)
}
@@ -0,0 +1,4 @@
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/accent"/>
<foreground android:drawable="@drawable/ic_shortcut_decode_foreground"/>
</adaptive-icon>
@@ -0,0 +1,4 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="108dp" android:height="108dp" android:viewportWidth="108" android:viewportHeight="108">
<path android:pathData="M48.281 55.787a5.719 5.719 0 1 1 11.438 0 5.719 5.719 0 1 1-11.438 0" android:fillColor="#ffffff"/>
<path android:pathData="M48.639 37.916l-3.271 3.575h-5.665c-1.965 0-3.574 1.608-3.574 3.574v21.444c0 1.966 1.609 3.575 3.574 3.575h28.594c1.965 0 3.574-1.609 3.574-3.575V45.065c0-1.966-1.609-3.574-3.574-3.574h-5.665l-3.271-3.575zM54 64.722c-4.932 0-8.935-4.003-8.935-8.935s4.003-8.935 8.935-8.935 8.935 4.003 8.935 8.935-4.003 8.935-8.935 8.935z" android:fillColor="#ffffff"/>
</vector>
@@ -0,0 +1,4 @@
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/accent"/>
<foreground android:drawable="@drawable/ic_shortcut_encode_foreground"/>
</adaptive-icon>
@@ -0,0 +1,3 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="108dp" android:height="108dp" android:viewportWidth="108" android:viewportHeight="108">
<path android:pathData="M69.75 56.25h-13.5v13.5h-4.5v-13.5h-13.5v-4.5h13.5v-13.5h4.5v13.5h13.5z" android:fillColor="#FFFFFF"/>
</vector>
@@ -0,0 +1,4 @@
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/accent"/>
<foreground android:drawable="@drawable/ic_shortcut_preferences_foreground"/>
</adaptive-icon>
@@ -0,0 +1,3 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="108dp" android:height="108dp" android:viewportWidth="108" android:viewportHeight="108">
<path android:pathData="M70.057 56.114c0.09-0.675 0.135-1.372 0.135-2.114 0-0.719-0.045-1.439-0.158-2.114l4.566-3.553c0.405-0.315 0.517-0.922 0.27-1.372l-4.318-7.466C70.282 39 69.72 38.843 69.225 39l-5.375 2.159c-1.124-0.854-2.316-1.574-3.643-2.114l-0.81-5.712c-0.09-0.54-0.539-0.922-1.079-0.922h-8.636c-0.54 0-0.967 0.382-1.057 0.922l-0.809 5.712c-1.327 0.54-2.542 1.282-3.644 2.114L38.798 39c-0.495-0.18-1.057 0-1.327 0.495l-4.296 7.466c-0.269 0.472-0.179 1.057 0.27 1.372l4.566 3.553c-0.113 0.675-0.203 1.417-0.203 2.114 0 0.697 0.045 1.439 0.158 2.114L33.4 59.667c-0.404 0.315-0.517 0.922-0.27 1.372l4.318 7.466C37.718 69 38.28 69.158 38.775 69l5.375-2.159c1.124 0.855 2.316 1.574 3.643 2.114l0.81 5.712c0.112 0.54 0.539 0.922 1.079 0.922h8.636c0.54 0 0.989-0.382 1.057-0.922l0.809-5.712c1.327-0.54 2.542-1.259 3.644-2.114L69.202 69c0.495 0.18 1.057 0 1.327-0.495l4.318-7.466c0.27-0.495 0.158-1.057-0.27-1.372zM54 62.096c-4.453 0-8.096-3.643-8.096-8.096s3.643-8.096 8.096-8.096 8.096 3.643 8.096 8.096-3.643 8.096-8.096 8.096z" android:fillColor="#ffffff"/>
</vector>
+12
View File
@@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" xmlns:aapt="http://schemas.android.com/aapt" android:width="56dp" android:height="56dp" android:viewportWidth="56" android:viewportHeight="56">
<path android:pathData="M4 32a24 24 0 1 1 48 0 24 24 0 1 1-48 0">
<aapt:attr name="android:fillColor">
<gradient android:gradientRadius="24" android:centerX="28" android:centerY="32" android:type="radial">
<item android:offset="0" android:color="#FF000000"/>
<item android:offset="1" android:color="#00000000"/>
</gradient>
</aapt:attr>
</path>
<path android:pathData="M4 28a24 24 0 1 1 48 0 24 24 0 1 1-48 0" android:fillColor="#B6D46F"/>
<path android:pathData="M33 31h2v-8c0-1.1-0.9-2-2-2h-8v2h8v8zm-10 2V17h-2v4h-4v2h4v10c0 1.1 0.9 2 2 2h10v4h2v-4h4v-2H23z" android:fillColor="#FFFFFF"/>
</vector>
@@ -0,0 +1,3 @@
<vector android:height="24dp" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FFFFFF" android:pathData="M14 10H2v2h12v-2zm0-4H2v2h12V6zm4 8v-4h-2v4h-4v2h4v4h2v-4h4v-2h-4zM2 16h8v-2H2v2z"/>
</vector>
@@ -0,0 +1,3 @@
<vector android:height="24dp" android:tint="#FFFFFF" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FFFFFF" android:pathData="M12.65 10C11.83 7.67 9.61 6 7 6c-3.31 0-6 2.69-6 6s2.69 6 6 6c2.61 0 4.83-1.67 5.65-4H17v4h4v-4h2v-4H12.65zM7 14c-1.1 0-2-0.9-2-2s0.9-2 2-2 2 0.9 2 2-0.9 2-2 2z"/>
</vector>
+2 -2
View File
@@ -1,3 +1,3 @@
<vector android:height="24dp" android:tint="#FFFFFF" android:viewportHeight="24.0" android:viewportWidth="24.0" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M15.5 14h-0.79l-0.28-0.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-0.59 4.23-1.57L14 14.71v0.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"/>
<vector android:height="24dp" android:tint="#FFFFFF" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M3 11h8V3H3v8zm2-6h4v4H5V5zM3 21h8v-8H3v8zm2-6h4v4H5v-4zm8-12v8h8V3h-8zm6 6h-4V5h4v4zm0 10h2v2h-2zm-6-6h2v2h-2zm2 2h2v2h-2zm-2 2h2v2h-2zm2 2h2v2h-2zm2-2h2v2h-2zm0-4h2v2h-2zm2 2h2v2h-2z"/>
</vector>
@@ -1,3 +0,0 @@
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="#b6d46f"/>
</shape>
+5 -1
View File
@@ -4,6 +4,10 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:contentDescription="@string/view_barcode"/>
<de.markusfisch.android.binaryeye.widget.DetectorView
android:id="@+id/detector_view"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
<android.support.design.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
@@ -13,6 +17,6 @@
style="@style/FAB"
android:id="@+id/scan"
android:contentDescription="@string/scan_code"
android:src="@drawable/ic_action_scan"/>
android:src="@drawable/ic_action_search"/>
</android.support.design.widget.CoordinatorLayout>
</merge>
+21
View File
@@ -0,0 +1,21 @@
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<EditText
tools:targetApi="o"
android:id="@+id/url"
android:inputType="textUri"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/url_hint"
android:importantForAutofill="no"/>
<Button
android:id="@+id/test_url"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/test_url"/>
</LinearLayout>
@@ -23,6 +23,18 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"/>
<TextView
android:id="@+id/error_correction_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:text="@string/error_correction_level"/>
<Spinner
android:id="@+id/error_correction_level"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:entries="@array/error_correction_levels"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
@@ -1,78 +0,0 @@
<de.markusfisch.android.binaryeye.widget.ConfinedScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/scroll_view"
android:clipToPadding="false"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="16dp"
android:orientation="vertical">
<android.support.v7.widget.SwitchCompat
android:id="@+id/zoom_by_swiping"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="32dp"
android:text="@string/zoom_by_swiping"/>
<android.support.v7.widget.SwitchCompat
android:id="@+id/try_harder"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="32dp"
android:text="@string/try_harder"/>
<android.support.v7.widget.SwitchCompat
android:id="@+id/vibrate"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="32dp"
android:text="@string/vibrate"/>
<android.support.v7.widget.SwitchCompat
android:id="@+id/use_history"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="32dp"
android:text="@string/use_history"/>
<android.support.v7.widget.SwitchCompat
android:id="@+id/ignore_consecutive_duplicates"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="32dp"
android:text="@string/ignore_consecutive_duplicates"/>
<android.support.v7.widget.SwitchCompat
android:id="@+id/open_immediately"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="32dp"
android:text="@string/open_immediately"/>
<android.support.v7.widget.SwitchCompat
android:id="@+id/show_meta_data"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="32dp"
android:text="@string/show_meta_data"/>
<android.support.v7.widget.SwitchCompat
android:id="@+id/show_hex_dump"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="32dp"
android:text="@string/show_hex_dump"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/open_with_url"/>
<EditText
tools:targetApi="o"
android:id="@+id/open_with_url"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="32dp"
android:gravity="start|top"
android:typeface="monospace"
android:imeOptions="flagNoExtractUi"
android:inputType="textUri"
android:hint="@string/open_with_url_hint"
android:importantForAutofill="no"/>
</LinearLayout>
</de.markusfisch.android.binaryeye.widget.ConfinedScrollView>
@@ -21,6 +21,12 @@
android:title="@string/switch_camera"
android:icon="@drawable/ic_action_switch_camera"
material:showAsAction="ifRoom"/>
<item
android:id="@+id/bulk_mode"
android:title="@string/bulk_mode"
android:icon="@drawable/ic_action_bulk_mode"
android:checkable="true"
material:showAsAction="ifRoom"/>
<item
android:id="@+id/preferences"
android:title="@string/preferences"
+6 -1
View File
@@ -2,7 +2,12 @@
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:material="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/save"
android:id="@+id/copy_to_clipboard"
android:title="@string/copy_to_clipboard"
android:icon="@drawable/ic_action_copy"
material:showAsAction="ifRoom"/>
<item
android:id="@+id/export_to_file"
android:title="@string/export_to_file"
android:icon="@drawable/ic_action_save"
material:showAsAction="ifRoom"/>
+12
View File
@@ -1,6 +1,18 @@
<menu
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:material="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/remove"
android:title="@string/remove_scan"
android:icon="@drawable/ic_action_remove"
android:visible="false"
material:showAsAction="ifRoom"/>
<item
android:id="@+id/copy_password"
android:title="@string/copy_password"
android:icon="@drawable/ic_action_copy_password"
android:visible="false"
material:showAsAction="ifRoom"/>
<item
android:id="@+id/copy_to_clipboard"
android:title="@string/copy_to_clipboard"
@@ -1,9 +0,0 @@
<menu
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:material="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/next"
android:title="@string/encode"
android:icon="@drawable/ic_action_next"
material:showAsAction="ifRoom"/>
</menu>
@@ -1,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/copy_scan"
android:title="@string/copy_scan"
android:icon="@drawable/ic_action_copy"
material:showAsAction="ifRoom"/>
<item
android:id="@+id/edit_scan"
android:title="@string/edit_scan"
@@ -1,4 +1,4 @@
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background"/>
<background android:drawable="@color/accent"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon>
+142
View File
@@ -0,0 +1,142 @@
<resources>
<string name="no_camera_no_fun">এই অ্যাপ ক্যামেরা অনুমতি ছাড়া কোনো কাজের না। বিদায়।</string>
<string name="camera_error">ক্যামেরা পাওয়া যাচ্ছে না, পরে চেষ্টা করো</string>
<string name="scan_code">সংকেতের পাঠোদ্ধার</string>
<string name="compose_barcode">বারকোড বানাও</string>
<string name="decode_barcode">বারকোডের পাঠোদ্ধার</string>
<string name="content">তথ্য</string>
<string name="binary_data">(বাইনারি তথ্য)</string>
<plurals name="barcode_info">
<item quantity="one">%2$dটি অক্ষর, %1$s</item>
<item quantity="other">%2$dটি অক্ষর, %1$s</item>
</plurals>
<string name="error_correction_level">ত্রুটি ঠিককরণ পরিমাণ</string>
<string name="error_correction_level_l" formatted="false">কম (~&#37; ঠিককরণ)</string>
<string name="error_correction_level_m" formatted="false">মধ্যম (~১৫&#37; ঠিককরণ)</string>
<string name="error_correction_level_q" formatted="false">বেশি (~২৫&#37; ঠিককরণ)</string>
<string name="error_correction_level_h" formatted="false">অনেক বেশি (~৩০&#37; ঠিককরণ)</string>
<string name="issue_number">সমস্যা ক্রম</string>
<string name="orientation">দিক</string>
<string name="other_meta_data">মেটাতথ্য</string>
<string name="pdf417_extra_metadata">পিডিএফ৪১৭ মেটাতথ্য</string>
<string name="possible_country">তৈরির সম্ভাব্য দেশ</string>
<string name="suggested_price">প্রস্তসবিত দাম</string>
<string name="upc_ean_extension">ইউপিসি ইএএন বর্ধিত</string>
<string name="toggle_flash">আলো</string>
<string name="share">ভাগ করো</string>
<string name="share_as">ভাগ করার মাধ্যম</string>
<string name="copy_to_clipboard">ক্লিপবোর্ডে অনুলিপি</string>
<string name="copied_to_clipboard">ক্লিপবোর্ডে অনুলিপিত</string>
<string name="copy_password">ক্লিপবোর্ডে পাসওয়ার্ড অনুলিপি করো</string>
<string name="copied_password_to_clipboard">ক্লিপবোর্ডে পাসওয়ার্ড অনুলিপিত</string>
<string name="open_url">ইউআরএল খুলো</string>
<string name="cannot_resolve_action">খোলার মতো অ্যাপ নেই</string>
<string name="pick_search_engine">একটা সার্চ ইঞ্জিন নাও</string>
<string name="format">ধরণ</string>
<string name="size">পিক্সেলে আকার</string>
<string name="width_by_height">%1$d×%2$d</string>
<string name="input_content_here">এখানে তথ্য দাও</string>
<string name="encode">সংকেতকরণ</string>
<string name="error_no_content">তথ্য নেই</string>
<string name="error_encoding_barcode">বারকোড বানানো যায়নি</string>
<string name="view_barcode">বারকোড দেখো</string>
<string name="info">তথ্য</string>
<string name="switch_camera">ক্যামেরা পাল্টাও</string>
<string name="bulk_mode">ধারাবাহিকভাবে খুঁজো</string>
<string name="bulk_mode_summary">শুরুতে সবসময় ধারাবাহিকভাবে খুঁজো</string>
<string name="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="zoom_by_swiping">ওপরে/নিচে টানো বড়ো করার জন্য</string>
<string name="zoom_by_swiping_summary">ক্যামেরার ছোটো-বড় নিয়ন্ত্রণের জন্য উপরে-নিচে টানো।</string>
<string name="auto_rotate">খাড়া ১ডি বারকোড সনাক্ত করো</string>
<string name="auto_rotate_summary">১ডি বারকোড সনাক্ত করতে স্বয়ংক্রিয়ভাবে ঘুরাও। যন্ত্রনির্ভরে এটি কার্যসম্পাদনের হার কমাতে পারে।</string>
<string name="try_harder">পাঠকের নির্ভুলতা বাড়াও</string>
<string name="try_harder_summary">অনেক কঠিন সংকেতের জন্য এটা চালু করো। এতে কার্যসম্পাদনার হার কমতে পারে।</string>
<string name="show_toast_in_bulk_mode">ধারাবাহিকভাবে তথ্য দেখাও</string>
<string name="show_toast_in_bulk_mode_summary">ধারাবাহিকভাবে তথ্য দেখানোর সময় মুহূর্তের জন্য তথ্য দেখাও।</string>
<string name="vibrate">চিনলে কাঁপো</string>
<string name="vibrate_summary">এটা সক্ষম রাখলে সংকেত চিনার সাথে সাথে কাঁপবে।</string>
<string name="use_history">ইতিহাস সংরক্ষণ করো।</string>
<string name="use_history_summary">সনাক্তকৃত সংকেত যন্ত্রে সংরক্ষণ করো।</string>
<string name="ignore_consecutive_duplicates">দ্বিত্ব উপেক্ষা করো</string>
<string name="ignore_consecutive_duplicates_summary">দুইটা ইতিহাস সংরক্ষণ করবে না।</string>
<string name="open_immediately">সাথে সাথে খুলো</string>
<string name="open_immediately_summary">না দেখে সাথে সাথে খুলো। সক্রিয় থাকলে ক্ষতিকারক তথ্য খুলতে পারে।</string>
<string name="copy_immediately">ক্লিপবোর্ডে রাখো</string>
<string name="copy_immediately_summary">স্বয়ংক্রিয়ভাবে ক্লিপবোর্ডে অনুলিপি করো। বিশেষ দ্রষ্টব্য অন্য অ্যাপ পটভূমিতে এই তথ্য দেখে ফেলতে পারে।</string>
<string name="show_meta_data">মেটাতথ্য দেখাও</string>
<string name="show_meta_data_summary">সনাক্তকৃত বারকোড নিয়ে অতিরিক্ত তথ্য দেখাও।</string>
<string name="show_hex_dump">ষড়(hex) সংকেত দেখাও</string>
<string name="show_hex_dump_summary">পড়া তথ্যের ষড় সংকেত দেখাও</string>
<string name="close_automatically">অনুলিপি/ভাগ করার পরে পিছনে যাও</string>
<string name="close_automatically_summary">তথ্য অনুলিপি/ভাগ করার পরে স্বয়ংক্রিয়ভাবে পিছনে যাও।</string>
<string name="default_search_engine">Open unknown data in</string>
<string name="always_ask">Always ask</string>
<string name="open_with_url">অজানা ইউআরএল খুলো</string>
<string name="clear_network_suggestions">Clear network suggestions</string>
<string name="clear_network_suggestions_summary">Remove all of the network suggestions that were previously provided by this app.</string>
<string name="clear_network_suggestions_success">Network suggestions cleared</string>
<string name="clear_network_suggestions_nothing_to_remove">Nothing to remove</string>
<string name="send_category">অন্যকে পাঠাও</string>
<string name="send_scan_url">প্রতিটি পাঠোদ্ধার ইউআরএল-এ পাঠাও</string>
<string name="send_scan_type">প্রতি পাঠোদ্ধারের জন্য অনুরোধ</string>
<string name="send_type_get_add_content">গেট(GET) দিয়ে তথ্য যোগ করো</string>
<string name="send_type_get_query_string">সম্পূর্ণ কুয়েরি স্ট্রিং দিয়ে গেট(GET) করো</string>
<string name="send_type_post_form">POST application/x-www-form-urlencoded</string>
<string name="send_type_post_json">POST application/json</string>
<string name="test_url">পরীক্ষার জন্য ইউআরএল</string>
<string name="really_remove_scan">আসলেই এটা মুছবে?</string>
<string name="really_remove_all_scans">আসলেই সব মুছবে?</string>
<string name="really_remove_selected_scans">আসলেই নির্বাচিত সব মুছবে?</string>
<string name="clear_history">ইতিহাস মুছো</string>
<string name="copy_scan">পাঠোদ্ধার অনুলিপি করো</string>
<string name="edit_scan">চিহ্ন সম্পাদনা করো</string>
<string name="remove_scan">পাঠোদ্ধার মুছো</string>
<string name="enter_name">পাঠোদ্ধার ব্যাখ্যা করতে একটা নাম দাও</string>
<string name="enter_name_hint">পাঠোদ্ধারের নাম</string>
<string name="no_barcode_found">বারকোড পাওয়া যায়নি</string>
<string name="pick_list_separator">তালিকার ভুক্তি কিভাবে আলাদা করবো?</string>
<string name="separator_line_break">বাক্য বিরতি</string>
<string name="separator_ruler">মাপকাঠি</string>
<string name="save_as_file_name">ডাউনলোডে নথি হিসেবে সংরক্ষণ করবে?</string>
<string name="file_name">নথির নাম</string>
<string name="error_saving_file">নথি সংরক্ষণ করা যায়নি</string>
<string name="error_file_exists">নথি আগে থেকে আছে</string>
<string name="connect_to_wifi">ওয়াইফাইর সাথে যুক্ত করো</string>
<string name="wifi_config_failed">ওয়াইফাইয়ে ঠিক করে যুক্ত হওয়া যায়নি</string>
<string name="wifi_added">ওয়াইফাই যোগকৃত</string>
<string name="sms_send">এসএমএস পাঠাও</string>
<string name="sms_error">এসএমএস পাঠানো যায়নি</string>
<string name="tel_dial">নম্বরে ডাকো</string>
<string name="tel_error">নম্বর ডাকা যায়নি</string>
<string name="mail_send">চিঠি পাঠাও</string>
<string name="mail_error">চিঠি পাঠাতে ব্যর্থ</string>
<string name="vcard_add">যোগাযোগে যুক্ত</string>
<string name="vcard_failed">ষোগাযোগে যোগ করা যায়নি</string>
<string name="vevent_add">বর্ষপঞ্জিকায় যোগ করো</string>
<string name="vevent_failed">বর্ষপঞ্জিকায় যোগ করা যায়নি</string>
<string name="otpauth_add">২এফএ যোগ করো</string>
<string name="search_web">ওয়েবে খুঁজো</string>
<string name="search_scan">আগেরগুলোতে খুঁজো</string>
<string name="export_to_file">নথিতে রপ্তানি করো</string>
<string name="export_as">রপ্তানির ধরণ?</string>
<string name="export_csv_comma">কমাসহ সিএসভি(.csv) নথি</string>
<string name="export_csv_semicolon">সেমিকোলনসহ সিএসভি(.csv) নথি</string>
<string name="export_json">জেসন নথি</string>
<string name="export_database">এসকিউলাইট তথ্যভিত্তি</string>
<string name="saved_in_downloads">ডাউনলোডে সংরক্ষিত</string>
<string name="rotate_image_cw">ছবি ঘড়ির দিকে ঘুরাও</string>
<string name="pick_file">ছবি নাও</string>
<string name="pick_code_to_scan">খুঁজার জন্য কোড নাও</string>
<string name="shortcut_decode">বারকোড খুজো</string>
<string name="shortcut_encode">বারকোড বানাও</string>
<string name="shortcut_preferences">পছন্দসমূহ</string>
<string name="background_request_failed">পটভূমির অনুরোধ ব্যর্থ</string>
</resources>
+143
View File
@@ -0,0 +1,143 @@
<resources>
<string name="no_camera_no_fun">Bez oprávnění k fotoaparátu tato aplikace nemá smysl. Na viděnou.</string>
<string name="camera_error">Nepodařilo se otevřít fotoaparát, prosím zkuste to znovu</string>
<string name="scan_code">Skenovat kód</string>
<string name="compose_barcode">Vytvořit čárový kód</string>
<string name="decode_barcode">Dekódovat čárový kód</string>
<string name="content">Obsah</string>
<string name="binary_data">(binární data)</string>
<plurals name="barcode_info">
<item quantity="one">%2$d znak, %1$s</item>
<item quantity="few">%2$d znaky, %1$s</item>
<item quantity="other">%2$d znaků, %1$s</item>
</plurals>
<string name="error_correction_level">Stupeň korekce chyb</string>
<string name="error_correction_level_l" formatted="false">Nízký (~7&#37; korekce)</string>
<string name="error_correction_level_m" formatted="false">Střední (~15&#37; korekce)</string>
<string name="error_correction_level_q" formatted="false">Čtvrtinový (~25&#37; korekce)</string>
<string name="error_correction_level_h" formatted="false">Vysoký (~30&#37; korekce)</string>
<string name="issue_number">Issue number</string>
<string name="orientation">Orientace</string>
<string name="other_meta_data">Metadata</string>
<string name="pdf417_extra_metadata">PDF417 metadata</string>
<string name="possible_country">Možná země původu</string>
<string name="suggested_price">Doporučená cena</string>
<string name="upc_ean_extension">rozšíření UPC EAN</string>
<string name="toggle_flash">Zap/vyp blesk</string>
<string name="share">Sdílet</string>
<string name="share_as">Sdílet jako</string>
<string name="copy_to_clipboard">Zkopírovat do schránky</string>
<string name="copied_to_clipboard">Zkopírováno do schránky</string>
<string name="copy_password">Zkopírovat heslo do schránky</string>
<string name="copied_password_to_clipboard">Heslo zkopírováno do schránky</string>
<string name="open_url">Otevřít URL</string>
<string name="cannot_resolve_action">Nemáte aplikaci, která by to uměla otevřít</string>
<string name="pick_search_engine">Zvolte vyhledávač</string>
<string name="format">Formát</string>
<string name="size">Velikost v pixelech</string>
<string name="width_by_height">%1$d×%2$d</string>
<string name="input_content_here">Sem vložte obsah</string>
<string name="encode">ZAKÓDOVAT</string>
<string name="error_no_content">Chybí obsah</string>
<string name="error_encoding_barcode">Nepodařilo se vygenerovat čárový kód</string>
<string name="view_barcode">Zobrazit čárový kód</string>
<string name="info">Info</string>
<string name="switch_camera">Přepnout kameru</string>
<string name="bulk_mode">Skenovat nepřetržitě</string>
<string name="bulk_mode_summary">Always start scanning continuously.</string>
<string name="history">Historie</string>
<string name="preferences">Nastavení</string>
<string name="scan_category">Skenování</string>
<string name="content_category">Obsah</string>
<string name="locale_category">Jazyk</string>
<string name="custom_locale">Zvolit jazyk</string>
<string name="follow_system_settings">Řídit se nastavením systému</string>
<string name="show_crop_handle">Zobrazovat hrany výřezu</string>
<string name="show_crop_handle_summary">Omezí skenování na zvolenou oblast.</string>
<string name="zoom_by_swiping">Potáhnutím nahoru/dolů přiblížit</string>
<string name="zoom_by_swiping_summary">Přetáhnutím nahoru nebo dolů se zvětší / zmenší úroveň přiblížení.</string>
<string name="auto_rotate">Rozpoznávat otočené 1D čárové kódy</string>
<string name="auto_rotate_summary">Automaticky otočí obraz a tak detekuje 1D čárové kódy na výšku. Na některých zařízeních může snížit výkonnost.</string>
<string name="try_harder">Optimalizovat pro přesnost</string>
<string name="try_harder_summary">Použijte pro velmi nečitelné kódy. Sníží výkonnost.</string>
<string name="show_toast_in_bulk_mode">Show data in continuous mode</string>
<string name="show_toast_in_bulk_mode_summary">Briefly show the scanned data when scanning continuously.</string>
<string name="vibrate">Zavibrovat při detekování</string>
<string name="vibrate_summary">Zařízení zavibruje, když rozpozná čárový kód.</string>
<string name="use_history">Ukládat historii skenování</string>
<string name="use_history_summary">Rozpoznané kódy se uloží do zařízení.</string>
<string name="ignore_consecutive_duplicates">Ignorovat duplikáty</string>
<string name="ignore_consecutive_duplicates_summary">Další skeny stejného kódu se nebudou ukládat.</string>
<string name="open_immediately">Otevírat okamžitě</string>
<string name="open_immediately_summary">Přeskočí kontrolu a otevře obsah okamžitě. Může otevřít škodlivý obsah.</string>
<string name="copy_immediately">Vložit do schránky</string>
<string name="copy_immediately_summary">Automaticky zkopírovat naskenovaný obsah do schránky. Pozor, jiné aplikace jej mohou ze schránky přečíst, i když neběží.</string>
<string name="show_meta_data">Zobrazit metadata</string>
<string name="show_meta_data_summary">Zobrazí další data o naskenovaném čárovém kódu.</string>
<string name="show_hex_dump">Zobrazit hex dump</string>
<string name="show_hex_dump_summary">Zobrazí hex dump naskenovaného obsahu.</string>
<string name="close_automatically">Vrátit se po zkopírování/sdílení</string>
<string name="close_automatically_summary">Po zkopírovaní či sdílení se automaticky vrátí na skenovací obrazovku.</string>
<string name="default_search_engine">Open unknown data in</string>
<string name="always_ask">Always ask</string>
<string name="open_with_url">Otevřít neznámá data pomocí URL</string>
<string name="clear_network_suggestions">Clear network suggestions</string>
<string name="clear_network_suggestions_summary">Remove all of the network suggestions that were previously provided by this app.</string>
<string name="clear_network_suggestions_success">Network suggestions cleared</string>
<string name="clear_network_suggestions_nothing_to_remove">Nothing to remove</string>
<string name="send_category">Přesměrování</string>
<string name="send_scan_url">Posílat každý sken na URL</string>
<string name="send_scan_type">Typ requestu pro každý sken</string>
<string name="send_type_get_add_content">GET a simply add content</string>
<string name="send_type_get_query_string">GET with compelete query string</string>
<string name="send_type_post_form">POST application/x-www-form-urlencoded</string>
<string name="send_type_post_json">POST application/json</string>
<string name="test_url">Otestovat URL</string>
<string name="really_remove_scan">Opravdu odstranit sken?</string>
<string name="really_remove_all_scans">Opravdu odstranit všechny skeny?</string>
<string name="really_remove_selected_scans">Opravdu odstranit zvolené skeny?</string>
<string name="clear_history">Vymazat historii</string>
<string name="copy_scan">Zkopírovat sken</string>
<string name="edit_scan">Upravit popisek</string>
<string name="remove_scan">Odstranit sken</string>
<string name="enter_name">Napište název popisující sken</string>
<string name="enter_name_hint">Název skenu</string>
<string name="no_barcode_found">Čárový kód nenalezen</string>
<string name="pick_list_separator">Jak mají být odděleny položky?</string>
<string name="separator_line_break">Nový řádek</string>
<string name="separator_ruler">Svislice</string>
<string name="save_as_file_name">Uložit jako do složky Stahování</string>
<string name="file_name">Název souboru</string>
<string name="error_saving_file">Soubor se nepodařilo uložit</string>
<string name="error_file_exists">Soubor již existuje</string>
<string name="connect_to_wifi">Připojit k WiFi</string>
<string name="wifi_config_failed">Nepodařilo se nastavit WiFi</string>
<string name="wifi_added">WiFi přidána</string>
<string name="sms_send">Poslat SMS</string>
<string name="sms_error">Nepodařilo se poslat SMS</string>
<string name="tel_dial">Vytočit číslo</string>
<string name="tel_error">Nepodařilo se vytočit číslo</string>
<string name="mail_send">Poslat email</string>
<string name="mail_error">Nepodařilo se poslat email</string>
<string name="vcard_add">Přidat do kontaktů</string>
<string name="vcard_failed">Nepodařilo se přidat do kontaktů</string>
<string name="vevent_add">Vložit do kalendáře</string>
<string name="vevent_failed">Nepodařilo se vložit do kalendáře</string>
<string name="otpauth_add">Přidat 2FA</string>
<string name="search_web">Vyhledat na webu</string>
<string name="search_scan">Vyhledat sken</string>
<string name="export_to_file">Exportovat do souboru</string>
<string name="export_as">Exportovat jako</string>
<string name="export_csv_comma">CSV oddělené čárkami</string>
<string name="export_csv_semicolon">CSV oddělené středníky</string>
<string name="export_json">soubor JSON</string>
<string name="export_database">SQLite databáze</string>
<string name="saved_in_downloads">Uloženo do složky Stahování</string>
<string name="rotate_image_cw">Otočit obrázek po směru hodinových ručiček</string>
<string name="pick_file">Zvolte obrázek</string>
<string name="pick_code_to_scan">Zvolte kód k naskenování</string>
<string name="shortcut_decode">Skenovat čárový kód</string>
<string name="shortcut_encode">Vytvořit čárový kód</string>
<string name="shortcut_preferences">Nastavení</string>
<string name="background_request_failed">Požadavek na pozadí selhal</string>
</resources>
+57 -9
View File
@@ -11,6 +11,10 @@
<item quantity="other">%2$d Zeichen, %1$s</item>
</plurals>
<string name="error_correction_level">Fehlerkorrektur-Level</string>
<string name="error_correction_level_l" formatted="false">Low (~7&#37; wiederherstellbar)</string>
<string name="error_correction_level_m" formatted="false">Medium (~15&#37; wiederherstellbar)</string>
<string name="error_correction_level_q" formatted="false">Quartile (~25&#37; wiederherstellbar)</string>
<string name="error_correction_level_h" formatted="false">High (~30&#37; wiederherstellbar)</string>
<string name="issue_number">Ausgabe Nummer</string>
<string name="orientation">Orientierung</string>
<string name="other_meta_data">Metadaten</string>
@@ -20,8 +24,10 @@
<string name="upc_ean_extension">UPC EAN Erweiterung</string>
<string name="toggle_flash">Licht an/ausschalten</string>
<string name="share">Teilen</string>
<string name="share_as">Teilen als?</string>
<string name="copy_to_clipboard">In die Zwischenablage kopieren</string>
<string name="copied_to_clipboard">Inhalt wurde in die Zwischenablage kopiert</string>
<string name="copy_password">Passwort in die Zwischenablage kopieren</string>
<string name="copied_password_to_clipboard">Passwort wurde in die Zwischenablage kopiert</string>
<string name="open_url">URL öffnen</string>
<string name="cannot_resolve_action">Keine App kann das öffnen</string>
@@ -36,20 +42,61 @@
<string name="view_barcode">Barcode ansehen</string>
<string name="info">Info</string>
<string name="switch_camera">Kamera wechseln</string>
<string name="bulk_mode">Fortlaufend Scannen</string>
<string name="bulk_mode_summary">Immer mit fortlaufendem Scannen starten.</string>
<string name="history">Gespeicherte Codes</string>
<string name="preferences">Einstellungen</string>
<string name="zoom_by_swiping">Hoch/Runter streichen um zu zoomen</string>
<string name="try_harder">Suche länger und gründlicher nach Barcodes</string>
<string name="scan_category">Scannen</string>
<string name="content_category">Inhalt</string>
<string name="locale_category">Sprache</string>
<string name="custom_locale">Spracheinstellung</string>
<string name="follow_system_settings">Systemeinstellung übernehmen</string>
<string name="show_crop_handle">Ausschnitt verkleinern anzeigen</string>
<string name="show_crop_handle_summary">Scannen auf einen anpassbaren Bereich einschränken.</string>
<string name="zoom_by_swiping">Geste für Kamerazoom</string>
<string name="zoom_by_swiping_summary">Im Kamerabildschirm nach oben oder unten streichen, um den Kamerazoom zu steuern.</string>
<string name="auto_rotate">Vertikale Erkennung</string>
<string name="auto_rotate_summary">Das Kamerabild automatisch drehen um vertikale 1D Barcodes zu erkennen. Je nach Gerät kann dies die Leistung beeinträchtigen.</string>
<string name="try_harder">Gründliche Suche</string>
<string name="try_harder_summary">Aktivieren Sie diese Option für sehr schwer lesbare Codes. Beeinträchtigt die Leistung.</string>
<string name="show_toast_in_bulk_mode">Daten einblenden</string>
<string name="show_toast_in_bulk_mode_summary">Die gescannten Daten werden kurz eingeblenet, wenn fortlaufend gescannt wird.</string>
<string name="vibrate">Bei Erkennung vibrieren</string>
<string name="vibrate_summary">Das Gerät soll vibrieren wenn ein Code erkannt wird.</string>
<string name="use_history">Codes speichern</string>
<string name="ignore_consecutive_duplicates">Keine aufeinanderfolgenden Duplikate speichern</string>
<string name="open_immediately">Inspektion überspringen und Inhalte sofort öffnen</string>
<string name="use_history_summary">Eingelesene Barcodes auf dem Gerät speichern.</string>
<string name="ignore_consecutive_duplicates">Duplikate ignorieren</string>
<string name="ignore_consecutive_duplicates_summary">Aufeinanderfolgende Duplikate werden nicht in der Scan-Historie gespeichert.</string>
<string name="open_immediately">Inhalte sofort öffnen</string>
<string name="open_immediately_summary">Inspektion überspringen und Inhalt sofort öffnen. Kann schädliche Inhalte öffnen, wenn aktiviert.</string>
<string name="copy_immediately">Automatisch in Zwischenablage</string>
<string name="copy_immediately_summary">Eingelesene Inhalte werden automatisch in die Zwischenablage kopiert. Bitte beachten Sie, dass andere Anwendungen die Zwischenablage im Hintergrund auslesen können.</string>
<string name="show_meta_data">Meta Daten anzeigen</string>
<string name="show_meta_data_summary">Zusätzliche Daten über den eingelesenen Barcode anzeigen.</string>
<string name="show_hex_dump">Hex Dump anzeigen</string>
<string name="show_hex_dump_summary">Zeigt einen Hexdump des eingelesenen Inhalts an.</string>
<string name="close_automatically">Automatisch zurückkehren</string>
<string name="close_automatically_summary">Nach dem Kopieren/Teilen automatisch zum Scanbildschirm zurückkehren.</string>
<string name="default_search_engine">Unbekannte Daten öffnen mit</string>
<string name="always_ask">Immer fragen</string>
<string name="open_with_url">URL zum Öffnen unbekannter Daten</string>
<string name="clear_network_suggestions">WiFi Netzwerkvorschläge entfernen</string>
<string name="clear_network_suggestions_summary">Betrifft alle WiFi Netzwerke, die zuvor mit dieser App bereitgestellt wurden.</string>
<string name="clear_network_suggestions_success">Netzwerkvorschläge entfernt</string>
<string name="clear_network_suggestions_nothing_to_remove">Nichts zu entfernen</string>
<string name="send_category">Weiterleitung</string>
<string name="send_scan_url">URL die für jeden Scan aufgerufen werden soll</string>
<string name="send_scan_type">Typ des Requests für jeden Scan</string>
<string name="send_type_get_add_content">GET mit an die URL angehängtem Inhalt</string>
<string name="send_type_get_query_string">GET mit vollständigem Query-String</string>
<string name="send_type_post_form">POST application/x-www-form-urlencoded</string>
<string name="send_type_post_json">POST application/json</string>
<string name="test_url">Test URL</string>
<string name="really_remove_scan">Code wirklich entfernen?</string>
<string name="really_remove_all_scans">Alle Codes entfernen?</string>
<string name="really_remove_selected_scans">Alle ausgewählten Codes entfernen?</string>
<string name="clear_history">Gespeicherte Codes löschen</string>
<string name="copy_scan">Code kopieren</string>
<string name="edit_scan">Bezeichnung ändern</string>
<string name="remove_scan">Code löschen</string>
<string name="enter_name">Geben Sie eine Bezeichnung für den Code ein</string>
@@ -80,15 +127,16 @@
<string name="search_scan">Code suchen</string>
<string name="export_to_file">In Datei exportieren</string>
<string name="export_as">Exportieren als?</string>
<string name="export_csv_comma">CSV mit Kommas</string>
<string name="export_csv_semicolon">CSV mit Semikolons</string>
<string name="export_json">Als JSON exportieren</string>
<string name="export_database">SQLite Datenbank exportieren</string>
<string name="export_csv_comma">CSV Datei mit Kommas</string>
<string name="export_csv_semicolon">CSV Datei mit Semikolons</string>
<string name="export_json">JSON Datei</string>
<string name="export_database">SQLite Datenbank</string>
<string name="saved_in_downloads">In den Downloads gespeichert</string>
<string name="rotate_image_cw">Bild im Uhrzeigersinn rotieren</string>
<string name="pick_file">Bilddatei auswählen</string>
<string name="pick_code_to_scan">Code im Bild auswählen</string>
<string name="shortcut_decode">Barcode einlesen</string>
<string name="shortcut_decode">Barcode lesen</string>
<string name="shortcut_encode">Barcode erstellen</string>
<string name="shortcut_preferences">Einstellungen</string>
<string name="background_request_failed">Hintergrundanfrage fehlgeschlagen</string>
</resources>
+142
View File
@@ -0,0 +1,142 @@
<resources>
<string name="no_camera_no_fun">Esta aplicación no tiene sentido usar sin acceso a la cámara. Adiós.</string>
<string name="camera_error">No se puede acceder a la cámara, por favor, inténtelo de nuevo</string>
<string name="scan_code">Escanear código de barras</string>
<string name="compose_barcode">Componer código de barras</string>
<string name="decode_barcode">Decodificar código de barras</string>
<string name="content">Contenido</string>
<string name="binary_data">(datos binarios)</string>
<plurals name="barcode_info">
<item quantity="one">%2$d carácter, %1$s</item>
<item quantity="other">%2$d caracteres, %1$s</item>
</plurals>
<string name="error_correction_level">Nivel de corrección de errores</string>
<string name="error_correction_level_l" formatted="false">Bajo (~7&#37; de correción)</string>
<string name="error_correction_level_m" formatted="false">Medio (~15&#37; de correción)</string>
<string name="error_correction_level_q" formatted="false">Cuartil (~25&#37; de correción)</string>
<string name="error_correction_level_h" formatted="false">Alto (~30&#37; de correción)</string>
<string name="issue_number">Número de emisión</string>
<string name="orientation">Orientación</string>
<string name="other_meta_data">Metadatos</string>
<string name="pdf417_extra_metadata">Metadatos PDF417</string>
<string name="possible_country">Posible país de fabricación</string>
<string name="suggested_price">Precio sugerido</string>
<string name="upc_ean_extension">Extensión UPC EAN</string>
<string name="toggle_flash">Activar la linterna</string>
<string name="share">Compartir</string>
<string name="share_as">¿Compartir como?</string>
<string name="copy_to_clipboard">Copiar al portapapeles</string>
<string name="copied_to_clipboard">Copiado al portapapeles</string>
<string name="copy_password">Copiar contraseña al portapapeles</string>
<string name="copied_password_to_clipboard">Contraseña copiada al portapapeles</string>
<string name="open_url">Abrir URL</string>
<string name="cannot_resolve_action">Ninguna aplicación puede abrir eso</string>
<string name="pick_search_engine">Elija un motor de búsqueda</string>
<string name="format">Formato</string>
<string name="size">Tamaño en píxeles</string>
<string name="width_by_height">%1$d×%2$d</string>
<string name="input_content_here">Introduzca el contenido aquí</string>
<string name="encode">CODIFICAR</string>
<string name="error_no_content">Contenido no disponible</string>
<string name="error_encoding_barcode">No pudo generar el código de barras</string>
<string name="view_barcode">Ver código de barras</string>
<string name="info">Información</string>
<string name="switch_camera">Cambiar cámara</string>
<string name="bulk_mode">Escanear constantemente</string>
<string name="bulk_mode_summary">Always start scanning continuously.</string>
<string name="history">Historial</string>
<string name="preferences">Configuración</string>
<string name="scan_category">Escanear</string>
<string name="content_category">Contenido</string>
<string name="locale_category">Idioma</string>
<string name="custom_locale">Idioma personalizado</string>
<string name="follow_system_settings">Seguir los ajustes del sistema</string>
<string name="show_crop_handle">Mostrar el delimitador de recorte</string>
<string name="show_crop_handle_summary">Restringir el escaneo a una región personalizable.</string>
<string name="zoom_by_swiping">Deslice hacia arriba/abajo para ampliar</string>
<string name="zoom_by_swiping_summary">Deslice su dedo hacia arriba o abajo en la pantalla para controlar el aumento de la cámara.</string>
<string name="auto_rotate">Reconocer los códigos de barras 1D verticalmente</string>
<string name="auto_rotate_summary">Rotar automáticamente el marco de la cámara para reconocer los códigos de barras 1D verticales. Dependiendo del dispositivo, esto puede afectar al rendimiento.</string>
<string name="try_harder">Optimizar la precisión del lector</string>
<string name="try_harder_summary">Active esta opción para códigos muy difíciles de leer. Esto afectará al rendimiento.</string>
<string name="show_toast_in_bulk_mode">Show data in continuous mode</string>
<string name="show_toast_in_bulk_mode_summary">Briefly show the scanned data when scanning continuously.</string>
<string name="vibrate">Vibrar al detectar</string>
<string name="vibrate_summary">Active esta opción si desea que su dispositivo vibre cuando se reconozca un código.</string>
<string name="use_history">Guardar historial de escaneo</string>
<string name="use_history_summary">Guardar códigos reconocidos en su dispositivo.</string>
<string name="ignore_consecutive_duplicates">Ignorar duplicados</string>
<string name="ignore_consecutive_duplicates_summary">No guarde duplicados consecutivos en el historial de escaneo.</string>
<string name="open_immediately">Abrir inmediatamente</string>
<string name="open_immediately_summary">Omite la inspección y abre el contenido inmediatamente. Puede abrir contenido dañino si está activado.</string>
<string name="copy_immediately">Poner en el portapapeles</string>
<string name="copy_immediately_summary">Copiar contenido al portapapeles automáticamente. Ten en cuenta que otras aplicaciones pueden husmear en tu portapapeles en segundo plano.</string>
<string name="show_meta_data">Mostrar metadatos</string>
<string name="show_meta_data_summary">Mostrar datos adicionales del código escaneado.</string>
<string name="show_hex_dump">Mostrar dump hexadecimal</string>
<string name="show_hex_dump_summary">Mostrar un dump hexadecimal del contenido escaneado.</string>
<string name="close_automatically">Retroceder luego de Copiar/Compartir</string>
<string name="close_automatically_summary">Regresar automáticamente a la pantalla de escaneo luego de copiar o compartir el contenido leído.</string>
<string name="default_search_engine">Open unknown data in</string>
<string name="always_ask">Always ask</string>
<string name="open_with_url">Abrir datos desconocidos con la URL</string>
<string name="clear_network_suggestions">Clear network suggestions</string>
<string name="clear_network_suggestions_summary">Remove all of the network suggestions that were previously provided by this app.</string>
<string name="clear_network_suggestions_success">Network suggestions cleared</string>
<string name="clear_network_suggestions_nothing_to_remove">Nothing to remove</string>
<string name="send_category">Reenvío</string>
<string name="send_scan_url">Enviar todo escaneo a la URL</string>
<string name="send_scan_type">Tipo de solicitud para cada escaneo</string>
<string name="send_type_get_add_content">GET y solo añadir contenido</string>
<string name="send_type_get_query_string">GET con consulta completa</string>
<string name="send_type_post_form">POST application/x-www-form-urlencoded</string>
<string name="send_type_post_json">POST application/json</string>
<string name="test_url">URL de prueba</string>
<string name="really_remove_scan">¿Realmente desea borrar el escaneo seleccionado?</string>
<string name="really_remove_all_scans">¿Realmente desea borrar todo lo escaneado?</string>
<string name="really_remove_selected_scans">¿Realmente desea borrar los escaneos seleccionados?</string>
<string name="clear_history">Borrar el historial</string>
<string name="copy_scan">Copiar escaneo</string>
<string name="edit_scan">Editar etiqueta</string>
<string name="remove_scan">Borrar escaneo</string>
<string name="enter_name">Introduzca un nombre para describir el escaneo</string>
<string name="enter_name_hint">Nombre del escaneo</string>
<string name="no_barcode_found">No se encontró ningún código de barras</string>
<string name="pick_list_separator">¿Cómo separar los elementos de la lista?</string>
<string name="separator_line_break">Línea separadora</string>
<string name="separator_ruler">Regla</string>
<string name="save_as_file_name">¿Desea guardar el archivo en Descargas?</string>
<string name="file_name">Nombre del archivo</string>
<string name="error_saving_file">No se puede guardar el archivo</string>
<string name="error_file_exists">El archivo ya existe</string>
<string name="connect_to_wifi">Conectar al Wifi</string>
<string name="wifi_config_failed">No se pudo configurar el Wifi</string>
<string name="wifi_added">Wifi añadido</string>
<string name="sms_send">Enviar SMS</string>
<string name="sms_error">No se puedo enviar SMS</string>
<string name="tel_dial">Marcar número</string>
<string name="tel_error">No se pudo marcar el número</string>
<string name="mail_send">Enviar correo</string>
<string name="mail_error">No se pudo enviar el correo</string>
<string name="vcard_add">Añadir a contactos</string>
<string name="vcard_failed">No se pudo añadir a los contactos</string>
<string name="vevent_add">Añadir al calendario</string>
<string name="vevent_failed">No se pudo añadir al calendario</string>
<string name="otpauth_add">Añadir 2FA</string>
<string name="search_web">Buscar en la web</string>
<string name="search_scan">Buscar escaneo</string>
<string name="export_to_file">Exportar a archivo</string>
<string name="export_as">¿Exportar como?</string>
<string name="export_csv_comma">Archivo CSV con comas</string>
<string name="export_csv_semicolon">Archivo CSV con punto y coma</string>
<string name="export_json">Archivo JSON</string>
<string name="export_database">Base de datos SQLite</string>
<string name="saved_in_downloads">Guardado en Descargas</string>
<string name="rotate_image_cw">Rotar la imagen en sentido horario</string>
<string name="pick_file">Seleccionar imagen</string>
<string name="pick_code_to_scan">Seleccione el código a escanear</string>
<string name="shortcut_decode">Escanear un código</string>
<string name="shortcut_encode">Crear un código</string>
<string name="shortcut_preferences">Ajustes</string>
<string name="background_request_failed">Solicitud de fondo fallida</string>
</resources>
+120 -71
View File
@@ -1,94 +1,143 @@
<resources>
<string name="no_camera_no_fun">Cette application n\'a aucun sens sans accès possible à la caméra. Au revoir.</string>
<string name="no_camera_no_fun">Cette application ne peut pas fonctionner sans accès à la caméra. Au revoir.</string>
<string name="camera_error">Impossible d\'accéder à la caméra, veuillez réessayer</string>
<string name="scan_code">Scanner le code</string>
<string name="compose_barcode">Créer un code-barres</string>
<string name="decode_barcode">Décoder le code-barres</string>
<string name="content">Contenu</string>
<string name="binary_data">(binary data)</string>
<string name="binary_data">(données binaires)</string>
<plurals name="barcode_info">
<item quantity="one">%2$d characters, %1$s</item>
<item quantity="other">%2$d characters, %1$s</item>
<item quantity="one">%2$d caractère, %1$s</item>
<item quantity="other">%2$d caractères, %1$s</item>
</plurals>
<string name="error_correction_level">Error correction level</string>
<string name="error_correction_level">Niveau de correction d\'erreur</string>
<string name="error_correction_level_l" formatted="false">Faible (~7% de correction)</string>
<string name="error_correction_level_m" formatted="false">Moyen (~15% de correction)</string>
<string name="error_correction_level_q" formatted="false">Quartile (~25% de correction)</string>
<string name="error_correction_level_h" formatted="false">Élevé (~30% de correction)</string>
<string name="issue_number">Issue number</string>
<string name="orientation">Orientation</string>
<string name="other_meta_data">Metadata</string>
<string name="pdf417_extra_metadata">PDF417 metadata</string>
<string name="possible_country">Possible country of manufacture</string>
<string name="suggested_price">Suggested price</string>
<string name="other_meta_data">Métadonnées</string>
<string name="pdf417_extra_metadata">PDF417 métadonnées</string>
<string name="possible_country">Pays de fabrication possible</string>
<string name="suggested_price">Prix suggéré</string>
<string name="upc_ean_extension">UPC EAN extension</string>
<string name="toggle_flash">Toggle flash</string>
<string name="share">Share</string>
<string name="copy_to_clipboard">Copier dans le presse-papier</string>
<string name="copied_to_clipboard">Mis dans le presse-papiers</string>
<string name="copied_password_to_clipboard">Password copied to clipboard</string>
<string name="toggle_flash">Activer le flash</string>
<string name="share">Partager</string>
<string name="share_as">Partager comme ?</string>
<string name="copy_to_clipboard">Copier dans le presse-papiers</string>
<string name="copied_to_clipboard">Copié dans le presse-papiers</string>
<string name="copy_password">Copier le mot de passe dans le presse-papiers</string>
<string name="copied_password_to_clipboard">Mot de passe copié dans le presse-papiers</string>
<string name="open_url">Ouvrir l\'url</string>
<string name="cannot_resolve_action">Aucune application ne peut ouvrir ça</string>
<string name="pick_search_engine">Pick a search engine</string>
<string name="cannot_resolve_action">Aucune application ne peut ouvrir cela</string>
<string name="pick_search_engine">Choisir un moteur de recherche</string>
<string name="format">Format</string>
<string name="size">Taille en pixels</string>
<string name="width_by_height">%1$d×%2$d</string>
<string name="input_content_here">Entrez votre contenu ici</string>
<string name="input_content_here">Votre contenu ici</string>
<string name="encode">ENCODER</string>
<string name="error_no_content">Contenu manquant</string>
<string name="error_encoding_barcode">Barcode cannot be generated</string>
<string name="error_encoding_barcode">Le code-barres ne peut pas être généré</string>
<string name="view_barcode">Voir le code barre</string>
<string name="info">Info</string>
<string name="switch_camera">Changer de camera</string>
<string name="bulk_mode">Scan continu</string>
<string name="bulk_mode_summary">Always start scanning continuously.</string>
<string name="history">Historique</string>
<string name="preferences">Preferences</string>
<string name="zoom_by_swiping">Zoom camera by swiping up/down</string>
<string name="try_harder">Optimize reader for accuracy, not speed</string>
<string name="vibrate">Vibrate on detection</string>
<string name="use_history">Sauvegarder l\'histoire des scans</string>
<string name="ignore_consecutive_duplicates">Do not save consecutive duplicates</string>
<string name="open_immediately">Skip inspection and open contents immediately</string>
<string name="show_meta_data">Show metadata</string>
<string name="show_hex_dump">Show hex dump</string>
<string name="open_with_url">Open unknown data with URL</string>
<string name="really_remove_scan">Supprimer le scan?</string>
<string name="really_remove_all_scans">Supprimer tous les scans?</string>
<string name="clear_history">Clear history</string>
<string name="edit_scan">Edit label</string>
<string name="remove_scan">Remove scan</string>
<string name="enter_name">Enter a name to describe the scan</string>
<string name="enter_name_hint">Name of the scan</string>
<string name="preferences">Préférences</string>
<string name="scan_category">Scan</string>
<string name="content_category">Contenu</string>
<string name="locale_category">Langue</string>
<string name="custom_locale">Langue personnalisée</string>
<string name="follow_system_settings">Suivre les réglages du système</string>
<string name="show_crop_handle">Afficher le bouton de rognage</string>
<string name="show_crop_handle_summary">Restreindre le scan à une région personnalisée.</string>
<string name="zoom_by_swiping">Balayage haut/bas pour zoomer</string>
<string name="zoom_by_swiping_summary">Balayez vers le haut ou vers le bas dans l’écran de la caméra pour contrôler le zoom de la caméra.</string>
<string name="auto_rotate">Reconnaître les codes-barres 1D verticaux</string>
<string name="auto_rotate_summary">Tourner automatiquement le cadre de la caméra pour reconnaître les codes-barres 1D verticaux. En fonction de l\'appareil, cela peut affecter les performances.</string>
<string name="try_harder">Optimiser la précision du lecteur</string>
<string name="try_harder_summary">Activez cette option pour les codes très difficiles à lire. Affectera les performances.</string>
<string name="show_toast_in_bulk_mode">Show data in continuous mode</string>
<string name="show_toast_in_bulk_mode_summary">Briefly show the scanned data when scanning continuously.</string>
<string name="vibrate">Vibrer à la détection</string>
<string name="vibrate_summary">Activez ceci si vous voulez que votre appareil vibre lorsquun code est reconnu.</string>
<string name="use_history">Sauvegarder l\'historique des scans</string>
<string name="use_history_summary">Sauvegarder les codes reconnus sur votre appareil.</string>
<string name="ignore_consecutive_duplicates">Ignorer les doublons</string>
<string name="ignore_consecutive_duplicates_summary">Ne pas sauvegarder les doublons dans l\'historique des scans.</string>
<string name="open_immediately">Ouvrir immédiatement</string>
<string name="open_immediately_summary">Sauter l\'inspection et ouvrir le contenu immédiatement. Si activé, cela peut ouvrir un contenu nocif.</string>
<string name="copy_immediately">Mettre dans le presse-papiers</string>
<string name="copy_immediately_summary">Copier automatiquement les contenus scannés dans le presse-papiers. Veuillez noter que les autres applications peuvent fouiner dans votre presse-papiers en arrière-plan.</string>
<string name="show_meta_data">Afficher les méta-données</string>
<string name="show_meta_data_summary">Afficher les données supplémentaires sur le code-barres scanné.</string>
<string name="show_hex_dump">Afficher le dump hexa</string>
<string name="show_hex_dump_summary">Affiche un dump hexadécimal du contenu scanné.</string>
<string name="close_automatically">Revenir après une copie/partage</string>
<string name="close_automatically_summary">Revient automatiquement sur l\'écran de scan après avoir copié ou partagé le contenu lu.</string>
<string name="default_search_engine">Open unknown data in</string>
<string name="always_ask">Always ask</string>
<string name="open_with_url">Ouvrir des données inconnues avec lURL</string>
<string name="clear_network_suggestions">Clear network suggestions</string>
<string name="clear_network_suggestions_summary">Remove all of the network suggestions that were previously provided by this app.</string>
<string name="clear_network_suggestions_success">Network suggestions cleared</string>
<string name="clear_network_suggestions_nothing_to_remove">Nothing to remove</string>
<string name="send_category">Transfert</string>
<string name="send_scan_url">Envoyer chaque scan vers une URL</string>
<string name="send_scan_type">Type de requête pour chaque scan</string>
<string name="send_type_get_add_content">GET et ajouter simplement le contenu</string>
<string name="send_type_get_query_string">GET avec chaîne de requête complète</string>
<string name="send_type_post_form">POST application/x-www-form-urlencoded</string>
<string name="send_type_post_json">POST application/json</string>
<string name="test_url">Tester l\'URL</string>
<string name="really_remove_scan">Supprimer le scan ?</string>
<string name="really_remove_all_scans">Supprimer tous les scans ?</string>
<string name="really_remove_selected_scans">Supprimer les scans sélectionnés ?</string>
<string name="clear_history">Effacer l\'historique</string>
<string name="copy_scan">Copier le scan</string>
<string name="edit_scan">Éditer l\'étiquette</string>
<string name="remove_scan">Supprimer le scan</string>
<string name="enter_name">Saisir un nom décrivant le scan</string>
<string name="enter_name_hint">Nom du scan</string>
<string name="no_barcode_found">Aucun code-barres trouvé</string>
<string name="pick_list_separator">How to separate list items?</string>
<string name="separator_line_break">Line break</string>
<string name="separator_ruler">Ruler</string>
<string name="save_as_file_name">Save in Downloads as file?</string>
<string name="file_name">File name</string>
<string name="error_saving_file">Cannot save file</string>
<string name="error_file_exists">File already exists</string>
<string name="connect_to_wifi">Connect to WiFi</string>
<string name="wifi_config_failed">Could not configure WiFi</string>
<string name="wifi_added">WiFi added</string>
<string name="sms_send">Send SMS</string>
<string name="sms_error">Could not send SMS</string>
<string name="tel_dial">Dial number</string>
<string name="tel_error">Could not dial number</string>
<string name="mail_send">Send mail</string>
<string name="mail_error">Could not send mail</string>
<string name="vcard_add">Add to contacts</string>
<string name="vcard_failed">Could not add to contacts</string>
<string name="vevent_add">Add to calendar</string>
<string name="vevent_failed">Could not add to calendar</string>
<string name="otpauth_add">Add 2FA</string>
<string name="search_web">Search the web</string>
<string name="search_scan">Search scan</string>
<string name="export_to_file">Export to file</string>
<string name="export_as">Export as?</string>
<string name="export_csv_comma">CSV with commas</string>
<string name="export_csv_semicolon">CSV with semicolons</string>
<string name="export_json">Export as JSON</string>
<string name="export_database">Export SQLite database</string>
<string name="saved_in_downloads">Saved in downloads</string>
<string name="rotate_image_cw">Rotate image clockwise</string>
<string name="pick_file">Pick image file</string>
<string name="pick_code_to_scan">Pick code to scan</string>
<string name="shortcut_decode">Scan a barcode</string>
<string name="shortcut_encode">Create a barcode</string>
<string name="shortcut_preferences">Settings</string>
<string name="pick_list_separator">Comment séparer les éléments listés ?</string>
<string name="separator_line_break">Saut de ligne</string>
<string name="separator_ruler">Règle</string>
<string name="save_as_file_name">Sauvegarder dans les Téléchargements ?</string>
<string name="file_name">Nom du fichier</string>
<string name="error_saving_file">Impossible de sauvegarder le fichier</string>
<string name="error_file_exists">Fichier déjà existant</string>
<string name="connect_to_wifi">Connecter au WiFi</string>
<string name="wifi_config_failed">Impossible de configurer le WiFi</string>
<string name="wifi_added">WiFi ajouté</string>
<string name="sms_send">Envoyer SMS</string>
<string name="sms_error">Impossible d\'envoyer le SMS</string>
<string name="tel_dial">Numéro de téléphone</string>
<string name="tel_error">Impossible de composer le numéro</string>
<string name="mail_send">Envoyer le mail</string>
<string name="mail_error">Impossible d\'envoyer le mail</string>
<string name="vcard_add">Ajouter aux contacts</string>
<string name="vcard_failed">Impossible d\'ajouter aux contacts</string>
<string name="vevent_add">Ajouter à l\'agenda</string>
<string name="vevent_failed">Impossible d\'ajouter à l\'agenda</string>
<string name="otpauth_add">Ajouter 2FA</string>
<string name="search_web">Rechercher sur internet</string>
<string name="search_scan">Rechercher un scan</string>
<string name="export_to_file">Exporter comme fichier</string>
<string name="export_as">Exporter comme ?</string>
<string name="export_csv_comma">CSV avec virgules</string>
<string name="export_csv_semicolon">CSV avec points-virgules</string>
<string name="export_json">Exporter comme JSON</string>
<string name="export_database">Exporter la base de données SQLite</string>
<string name="saved_in_downloads">Sauvegardé dans les Téléchargements</string>
<string name="rotate_image_cw">Tourner l\'image dans le sens horaire</string>
<string name="pick_file">Choisir un fichier image</string>
<string name="pick_code_to_scan">Choisissez le code à scanner</string>
<string name="shortcut_decode">Scanner un code-barres</string>
<string name="shortcut_encode">Créer un code-barres</string>
<string name="shortcut_preferences">Réglages</string>
<string name="background_request_failed">Échec de la requête d\'arrière-plan</string>
</resources>

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