Compare commits

...
479 Commits
Author SHA1 Message Date
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
Markus Fisch ae422d9a02 Advance version number to 1.28.0 2020-05-15 10:14:50 +02:00
zmniandGitHub ace841814a Update Indonesian translation 2020-05-15 09:55:16 +02:00
Markus Fisch 21c3206464 Reset preprocessor to fit current orientation
Important for landscape to landscape orientation changes.
2020-05-14 20:28:05 +02:00
Markus Fisch f392baca8b Remove special RenderScript handling for LineageOS
Always running `RenderScript.forceCompat()` interferes with the
`forceCompat` flag that is set/unset if there is a RSRuntimeException.

Now that the app is restarting automatically for a RSRuntimeException
this special handling is no longer required anyway.
2020-05-14 14:08:28 +02:00
Markus Fisch 4cd0fb6418 Restart the app a fixed number of times only
To avoid being stuck in a restart loop if there is always a
RSRuntimeException not matter if forceCompat() is called or
not.
2020-05-14 14:05:06 +02:00
Markus Fisch 5b948a4c8f Add a commit() method to preferences too
Removes the doubled code for putting a Boolean into preferences.
2020-05-14 14:02:25 +02:00
Markus Fisch a75aff2e64 Add a note about RenderScript to the README
To save others from stumbling over the same problems.
2020-05-14 11:58:25 +02:00
Markus Fisch a49a49f1d2 Save preference onStop() instead of onPause()
There's no need to save preference in onPause() when there's
onStop().
2020-05-13 13:42:07 +02:00
Markus Fisch 6db46f1d0d Automatically restart the app after a RSRuntimeExc
And toggle the forceCompat flag instead of setting it to true
so it works the other way round too.

Of course, this makes much more sense if there was a switch to
manually enable/disable forceCompat so a user can play with it.

On the other hand, I don't want users to bother with this.
So it's probably best to try and fix this behind the curtain.
2020-05-13 13:35:46 +02:00
Markus Fisch 1c0062df8b Share history as CSV and JSON too 2020-05-12 12:17:45 +02:00
Markus Fisch d776688d26 Refactor string resource name
error_saving_binary_data should really be just error_saving_file.
2020-05-12 11:58:39 +02:00
Markus Fisch 9856aa9efa Move showing result toast to top most call
So toasts are not scattered all over export functions.
2020-05-12 11:53:58 +02:00
Markus Fisch 27ce6cb12b Export history in JSON format 2020-05-12 10:48:11 +02:00
Markus Fisch 9d3a2b8479 Simplify writing external files
And refactor getExternalOutputStream() to openExternalOutputStream().
2020-05-12 10:44:20 +02:00
Markus Fisch 74f0c050c0 Advance version number to 1.27.1 2020-05-11 20:15:32 +02:00
Markus Fisch af92d1db17 Refactor preference setting functions
It's much simpler that way and you know it's going to use
`apply()` and not `commit()`.
2020-05-11 11:40:40 +02:00
Markus Fisch 307401087e Run RenderScript.forceCompat() after RSRuntimeExc
Because there are more systems than just Lineage that need
`RenderScript.forceCompat()`.

To not hold back everyone else, the app now silently switches to
`forceCompat()` once there was a RSRuntimeException. Thats's very
crude because there may be other reasons for RSRuntimeException's
*and* the app will still crash even if `forceCompat()` is the fix
because `forceCompat()` must be called before every other
RenderScript function.

But that's probably the best I can do right now.
2020-05-11 11:31:16 +02:00
Ptilopsis LeucotisandGitHub a36fd668a9 Update Russion translation
And mark untranslatable string as `translatable="false"` for Android Studio
Translation Editor that doesn't recognize `donottranslate.xml`.
2020-05-07 14:22:00 +02:00
Markus Fisch 7e022249aa Advance version number to 1.27.0 2020-05-04 21:01:01 +02:00
Markus Fisch d464cc26b0 Remove Leak Canary from debug build
Because specifying a different minSdkVersion for the debug version
results in a higher minSdkVersion in the release build too. So

	buildTypes {
		debug {
			defaultConfig.minSdkVersion 14
			...
		}
		...
	}

will mean the release build will have minSdkVersion 14 too.
WTF, Gradle.

This reverts commit 45eb24036f.
2020-05-04 20:47:56 +02:00
Markus Fisch 45eb24036f Add Leak Canary to debug build
And increase minSdk for the debug build to 14 for Leak Canary.
2020-05-04 13:42:32 +02:00
Markus Fisch ea0212c58e Remove special treatment for macOS Catalina
Since 64bit Linux systems can build with build tools 29.0.3 too
and RenderScript.forceCompat() is only executed for Lineage OS
now.

Still, do NOT build release versions with macOS Catalina or the
build will break on Lineage OS!
2020-05-04 13:25:55 +02:00
Markus Fisch 170a9944ef Replace static inset callback for view callbacks
Using a common static callback function to set the paddings from
WindowInsets creates memory leaks because the function holds
references to the views and so to the Activity or Fragment.

Every view needs to have its own WindowInsetsListener that cannot
hold any references from its Activity or Fragment.

So I'm finally biting the bullet and do it like Chris Banes
described here:
https://medium.com/androiddevelopers/windowinsets-listeners-to-layouts-8f9ccc8fa4d1
2020-05-02 21:13:51 +02:00
unbranchedandGitHub ccfd356c67 Update Italian translation 2020-05-01 13:31:15 +02:00
Markus Fisch 91aa1d343e Put metadata into fastlane folder
Apparently the this data needs to be in a fastlane
folder so fastlane supply can work.
2020-04-29 22:02:26 +02:00
Markus Fisch b64b3c5c01 Add a note about the lacking MAXICODE support
And remove it from the metadata.

ZXing can read MAXICODEs only in PURE mode what means the
source image must be monochrome, unrotated and unskewed.
Of course, this isn't the case for a camera frame and this
is why Binary Eye cannot read MAXICODEs.
2020-04-29 10:01:36 +02:00
Markus Fisch dfb6c07930 Always check activity for null before use 2020-04-29 09:39:18 +02:00
Markus Fisch 6ca93f2502 Remove unused code 2020-04-29 09:37:22 +02:00
Markus Fisch e867651acd Add metadata for F-Droid and Google Play
And use these screenshots instead of the ones in gh-page in
the README.

It's probably better to put those screenshots in the master
branch anyway. This way, the images can always be displayed
even when offline.
2020-04-27 20:48:25 +02:00
Markus Fisch 607e1324ed Export SQLite database 2020-04-27 20:47:20 +02:00
Markus Fisch 256183b4aa Fix version headlines in CHANGELOG 2020-04-27 10:10:56 +02:00
Markus Fisch 733b6bb123 Improve order of preferences
Arrange preferences in the order a user would encounter them.
2020-04-27 09:31:33 +02:00
Markus Fisch 27a382f2cb Advance version number to 1.26.0 2020-04-26 20:32:44 +02:00
Markus Fisch b0eb601e5a Add a setting for swipe to zoom feature 2020-04-25 19:37:37 +02:00
Markus Fisch 3a3e743575 Stop calling focusTo() as soon as it returns false
To avoid throwing and catching future RuntimeExceptions.
2020-04-25 19:22:20 +02:00
Markus Fisch f08efb8238 Zoom in & out by swiping vertically on camera view
Alternative way to zoom in/out.
Probably a bit more convenient than using the slider.
2020-04-24 20:27:04 +02:00
Markus Fisch 723362dd67 Update build tools 2020-04-24 19:02:27 +02:00
zmniandGitHub dde039a299 Update Indonesian translation 2020-04-23 14:40:14 +02:00
Markus Fisch f2a056c1d0 Add avocado rule to Makefile
To make this a regular task.
2020-04-21 09:19:57 +02:00
Markus Fisch 0defe263ad Optimize vector drawables with avocado
See:
https://github.com/alexjlockwood/avocado
2020-04-20 21:01:25 +02:00
Markus Fisch 9d8d33de06 Update Kotlin version 2020-04-17 14:19:23 +02:00
Markus Fisch 738b4d0f7b Advance version number to 1.25.1 2020-04-15 12:22:05 +02:00
Markus Fisch 80378afa84 Explicitly keep support.v7.widget.SearchView
Somehow it's required to name this class explicitly so ProGuard
won't remove it.
2020-04-15 11:06:32 +02:00
Markus Fisch ec07e10208 Advance version number to 1.25.0 2020-04-14 21:45:16 +02:00
Markus Fisch 2a6cd169aa Remove unused otpauth_error resource string 2020-04-14 21:23:43 +02:00
Markus Fisch 051454988a Keep selection when item is scrolled out of view 2020-04-14 21:21:05 +02:00
Markus Fisch a388d7cbec Update Kotlin version 2020-04-14 21:19:32 +02:00
Markus Fisch fa45d9e088 Show a label next to renamed entries
So a user can easily tell what entries are labeled.
2020-04-14 17:54:50 +02:00
Markus Fisch 41e538fa97 Remove unused function to show soft keyboard 2020-04-14 12:31:09 +02:00
Markus Fisch 1753438288 Handle null in resultPoints array
Apparently ZXing returns an array that may contain null references
for some barcodes.
2020-04-14 12:20:34 +02:00
Markus Fisch 4135dfbd3f Add app shortcuts for launchers that support them
So a user can go directly to the preferences for example.
2020-04-14 12:06:30 +02:00
Markus Fisch 4de7d5d5d7 Remove context argument from update()
update() is tied to the fragment and can never be pure so
it's simpler to just get the context/activity inside the
function.
2020-04-14 09:32:13 +02:00
Markus Fisch c4856f97ef Use the same icon to remove all or just one scan 2020-04-13 17:04:29 +02:00
Markus Fisch aa885b8379 Add an action to search history 2020-04-13 17:02:13 +02:00
Markus Fisch eab9639242 Remove DatabaseRepository and export binary as hex
Well, it was nice seeing Flow in action but I think it's slowly
becoming more of a burden than a benefit.

The traditional Cursor approach sure isn't as nice, but it's much
more simple, does use less resources (because we never need to put
things into an object) and it's less code too.

Also, scans that contain binary data are now always exported as
a hex string.
2020-04-12 21:42:05 +02:00
Markus Fisch a199d82539 Color status bar in contextual mode to match CAB 2020-04-11 19:06:56 +02:00
Markus Fisch 469fc2ff5f Remove irrelevant one quantity for IN and CH
Apparently, in Chinese and Indonesian, only the other quantity
is ever used.
2020-04-11 14:00:25 +02:00
Markus Fisch b4261d8571 Give scans an individual name in history listing
So we can add some information about the scan.
2020-04-11 13:55:45 +02:00
Markus Fisch 725817151d Reuse ColorDrawable for ActionBar only for API 11+
ColorDrawable.setColor() doesn't exist pre Honeycomb.
2020-04-11 13:54:02 +02:00
Markus Fisch e434300eda Define nav bar color only if it's required 2020-04-10 19:40:28 +02:00
Markus Fisch 6b08c422cb Simplify loading translucent primary color
For the translucent system bars.
2020-04-10 19:14:57 +02:00
Markus Fisch 4bfa7b3582 Remove saving barcodes for intents from other apps
Since the app shouldn't save contents meant for other apps.
2020-04-08 18:43:52 +02:00
Markus Fisch d6f902ae02 Improve coloring system bars
Avoid constant (re)allocation of ColorDrawable for the action
bar background.

Also load the transparent primary color only once from Resources.

It's a tiny bit more efficient although nobody will be able to
actually feel a difference ;)
2020-04-07 23:19:30 +02:00
Markus Fisch 94023b0671 Separate Mapping into its own file
Makes it easier to find.
2020-04-07 23:12:28 +02:00
Markus Fisch 16a9428bc1 Separate handling window insets into its own file
Window insets have nothing to do with coloring system bars.
2020-04-07 23:11:40 +02:00
Markus Fisch 3db5891e0f Set padding on history instead of header/footer
Moves the scroll bar initially below the action bar where
it should be. Also, it's simpler this way.
2020-04-05 20:19:41 +02:00
Markus Fisch d03fd72b95 Use import/export icon for export history listing
It's less about saving and more about exporting something.
2020-04-05 14:44:36 +02:00
Markus Fisch 97640c2a1f Update tools version 2020-04-03 18:27:28 +02:00
Markus Fisch 3bba6cf10d Make sure to handle a null ResultPoint correctly
Never trust ZXing to deliver a non-null object.
Also make sure to not use an empty array of ResultPoint's.
2020-04-03 18:11:45 +02:00
Markus Fisch aca97afc78 Ignore consecutive duplicates by default
For new installations that is, of course.
2020-04-03 15:50:19 +02:00
Markus Fisch 3b46da5571 Add a message for copying the WiFi password 2020-04-03 15:46:34 +02:00
Markus Fisch df62881ee6 Refactor put_into_clipboard to copied_to_clipboard
To make it more clear that this is the message that is shown
*after* something has been copied into the clipboard.

Also update the german and english translations accordingly.
2020-04-03 15:38:33 +02:00
Markus Fisch 966eea4f3f Always put parsed WiFi password into clipboard
So if adding the network automatically fails, it's easier
for to manually connect to the WiFi.
2020-04-03 15:34:23 +02:00
Markus Fisch eb2ed05b6d Move clipboard handling into it's own file
So we can use from elsewhere too.
2020-04-03 15:33:44 +02:00
Markus Fisch f31c1eee0e Only request ACCESS_FINE_LOCATION below Android Q
Not required to suggest a network for Android Q.
2020-04-03 15:31:57 +02:00
Markus Fisch c61db34d27 Check return values when adding a WiFi
Only before Android Q.

Also refactor connect() to addNetwork() because on Android Q,
we can only suggest new networks but cannot directly connect
to them.
2020-04-03 15:29:20 +02:00
Markus Fisch 3d85cef8a0 Move vibration setting above history
use_history and ignore_consecutive_duplicates should be
directly next to each other.
2020-04-03 12:37:15 +02:00
Markus Fisch 276fcccc46 Request ACCESS_FINE_LOCATION for adding a WiFi
configuredNetworks requires ACCESS_FINE_LOCATION.
2020-04-03 12:34:50 +02:00
Markus Fisch caa075f422 Add a setting to control vibration on detection 2020-04-03 11:59:35 +02:00
Markus Fisch cc629314ca Update german translation for TRY_HARDER 2020-04-03 11:51:45 +02:00
Markus Fisch c9242c3475 Make test rule run only unit tests
And add an explicit "cat" rule to do test on a connected
Android device.
2020-04-03 11:49:28 +02:00
Markus Fisch 3a87416cdb Add missing annotations for experimental stuff 2020-04-03 11:43:03 +02:00
Markus Fisch 4c4f60bddc Add a setting to enable TRY_HARDER
And always enable it when scanning from an image.
2020-04-03 11:41:42 +02:00
Markus Fisch 557652b18e Remove test for hex strings in WiFi codes
Because SSID/password are no longer quoted in the HashMap.

Instead, both will be quoted when they're used in a WifiConfiguration
since Android Q's WifiNetworkSuggestion.Builder never needs those
values quoted.

Also, since it is fundamentally impossible to tell whether a string
is a hex representation or happens to just consist of characters
from 0 to f, the quotes are now only omitted for a password string
that is exactly 64 characters long and contains only 0-f (raw PSK).
2020-04-03 09:37:06 +02:00
Markus Fisch a1094626c4 Fix some style issues
Apply auto format.
2020-04-02 22:11:58 +02:00
Markus Fisch e45dd7ff18 Remove hex detection for SSID
Unfortunately, we can never know whether a string consisting only
of 0-f is a hex string. So we just make this one assumption that
a string of 0-f that is exactly 64 characters long, is _probably_
a hex string for raw PSK. Of course, this may fail for any string
that just happens to be 64 characters long and contains just 0-f.
2020-04-02 22:02:02 +02:00
Markus Fisch 1c1625b6fd Simplify toasting with an extension function
All these Toast.makeText().show() blocks mean too many
unnecessary repetitions.
2020-04-02 21:34:39 +02:00
Markus Fisch 9c4cb0daad Suggest a WiFi network on Android Q
Because WifiConfiguration and WifiManager.addNetwork() (and all
related functions) were deprecated in Android Q.

Because we target Android Q, WifiManager.addNetwork() will always
fail and return -1.

On Android Q, we can only suggest the system (and user) a WiFi
network but cannot add it permanently ourselves.
2020-04-02 21:28:55 +02:00
Markus Fisch 024dd379e8 Run RenderScript.forceCompat() for LineageOS only
Since forceCompat() disables the native implementation, what is
a drawback for all other devices whose native implementation is
fully functional.
2020-04-02 21:23:55 +02:00
Markus Fisch 16c4957c79 Simplify saving a generated barcode image 2020-04-01 22:38:09 +02:00
Markus Fisch 01741b83cd Fix vibrating for Android O and higher
The amplitude was too small.
2020-04-01 22:30:55 +02:00
Markus Fisch 4200b3274a Fix typo in german translation 2020-04-01 22:27:16 +02:00
Markus Fisch eabd14eb60 Use ContentResolver to save external files on Q+
To support Android Q and future Android versions.
2020-04-01 22:21:37 +02:00
Markus Fisch 778c087930 Clean up unnecessary/unused code
According to Android Studio's recommendations.
2020-04-01 15:16:09 +02:00
Markus Fisch cd646a2e90 Add annotations for deprecated code
Because this code is not going to change anytime soon.
So it's better to just annotate them to keep those warnings
out of the way.
2020-04-01 15:13:48 +02:00
Markus Fisch be6576ac74 Add annotations to mark experimental/preview stuff 2020-04-01 15:13:20 +02:00
Markus Fisch 6460500a4d Use new ClipboardManager for HoneyComb and better 2020-04-01 14:52:24 +02:00
Markus Fisch 02474dfad2 Move getting fragment for intent into own function
Improves readability and needs less code.
2020-04-01 13:57:52 +02:00
Markus Fisch d78fd109ff Use VibrationEffect in Android O or better
Instead of the deprecated `Vibrator.vibrate(int)`.
2020-04-01 13:54:23 +02:00
Markus Fisch 85e3f49270 Fix path to latest Inkscape on macOS 2020-04-01 10:23:52 +02:00
Markus Fisch 59d40b944b Show latest possible result point in camera view 2020-03-30 19:57:57 +02:00
Markus Fisch 1e82e8bd53 Use a sorted map to enlist metadata items
A HashMap is unsorted of course what means the order of the
metadata items was random.
2020-03-27 18:12:20 +01:00
Markus Fisch ab5e9b6e9b Advance version number to 1.24.0 2020-03-24 21:29:48 +01:00
Markus Fisch 7336d8d12b Try to resolve URL intent before starting it
Because, apparently, there are devices where this intent cannot
be resolved.

Also put the project URL into a resource so we can at least show
it in a Toast if there's no browser installed. Might as well put
it in the clipboard some day.
2020-03-24 21:09:42 +01:00
Markus Fisch 1c9dd73d9a Check resultPoints for null before using it
ZXing's Result.getResultPoints() may as well return null.
2020-03-24 20:55:07 +01:00
Markus Fisch 5892002a89 Use legacy storage permission model for Android Q
Just simply use `requestLegacyExternalStorage="true"` to opt out
of the new Scoped storage model for the time being.

As soon as we set Android 11 as target version, this will no longer
work!

Details on the issue:
https://developer.android.com/preview/privacy/storage
2020-03-24 20:34:06 +01:00
Markus Fisch 540290e97c Include meta data in CSV export 2020-03-24 20:26:00 +01:00
Markus Fisch f315916fb8 Add a dot before csv suffix
The suffix is not an extension but just a string that
gets appended to the file name.
2020-03-24 20:25:02 +01:00
Markus Fisch e75bb48360 Add a preference switch to show/hide metadata
If there's one for the hex dump, there should be a switch
for the metadata as well.
2020-03-24 18:28:36 +01:00
Markus Fisch 2b1d711168 Show ZXing metadata after decoding
Also save the metadata in the history so it's still available
for past scans.
2020-03-24 18:21:30 +01:00
unbranchedandGitHub 2af5a89430 Update italian translation 2020-03-21 14:32:13 +01:00
Markus Fisch b640fa1372 Reset flash flag when camera is closed
Closing the camera will also shut off the flash.

This fixes the bug that when you scan a barcode with the flash on and
immediately go back (to the previous instance of CameraActivity), the
flash flag will still be true when the flash is off.

Fix for https://github.com/markusfisch/BinaryEye/issues/101
2020-03-19 18:08:54 +01:00
Markus Fisch 8e24673d08 Advance version number to 1.23.0 2020-03-14 20:14:12 +01:00
Markus Fisch ae54e62ec3 Update comments about building on macOS Catalina 2020-03-14 20:05:14 +01:00
Markus Fisch 420a4cbad3 Limit history items to one line
And ellipsize content if longer.
2020-03-14 19:47:45 +01:00
Markus Fisch 05ec77da27 Add a setting to show/hide hex dump 2020-03-14 19:39:17 +01:00
Markus Fisch c03abf1148 Add a setting to not save consecutive double scans 2020-03-14 16:11:35 +01:00
Markus Fisch b929df4848 Always set window inset listener
In case there is a configuration (orientation) change later on.

And always return previously initialized window insets what is
required when Fragments change and the listener won't fire (again).

Also use an explicit variable to know when the window insets rect
has been initialized. `Rect.isEmpty()` doesn't cut the mustard.
2020-03-14 15:56:53 +01:00
Markus Fisch ec729b6624 Move infos about supported barcodes down
Probably the least interesting part.
2020-03-14 14:31:52 +01:00
Markus Fisch 67471ee141 Advance version number to 1.22.3 2020-03-12 10:07:30 +01:00
Markus Fisch 6999f0a863 Advance version number to 1.22.2 2020-03-11 21:52:33 +01:00
Markus Fisch 6574a5d651 Fix getting window insets for Lollipop and above
Don't use the static `windowInsets` when `setWindowInsetListener()`
is called because this is just for pre-Lollipop devices where
`ViewCompat.setOnApplyWindowInsetsListener(view)` *never* fires.
2020-03-11 21:42:55 +01:00
Markus Fisch 1639e73738 Advance version number to 1.22.1 2020-03-10 19:32:06 +01:00
Markus Fisch 68fa67f54f Save non-printable content as raw data 2020-03-09 17:05:42 +01:00
Markus Fisch 326c34b6c1 Clean up some style issues 2020-03-09 17:04:31 +01:00
Markus Fisch a43dca3644 Advance version number to 1.22.0 2020-03-04 21:51:06 +01:00
Markus Fisch 168ccf7d10 Remove using a special tool version for Catalina
But keep using a special version for `buildToolsVersion` since
RenderScript requires a minimum of 29.0.3 on Catalina what
doesn't work on Linux yet.
2020-03-02 13:59:23 +01:00
Markus Fisch a9630b4d53 Optimize imports 2020-02-28 21:58:59 +01:00
Markus Fisch 4a138bd425 Highlight detected barcodes in camera view too
Although it's just visible before the result screen
takes over.
2020-02-28 21:48:54 +01:00
Markus Fisch 339ff59270 Update build tools 2020-02-27 22:22:21 +01:00
yzqzssandGitHub d208ec12bc Add bing and yandex search engines 2020-02-23 14:06:54 +01:00
yzqzssandGitHub 893ddefa71 Update Chinese Translation 2020-02-22 19:35:36 +01:00
Markus Fisch 5d56365e79 Skip forceCompat() when building on Catalina
Calling RenderScript.forceCompat() on a build that was made with the
new build tools (which are required to make RenderScript build again
on macOS Catalina), results in a RSRuntimeException (Loading of ScriptC
script failed).

RenderScript.forceCompat() was introduced to fix RenderScript for
Lineage 16.0. Maybe this is no longer required with those new build
tools?

Yes, this means this setup is producing platform independent builds
that have different features depending on what platform they were
built on. And yes, that's as bad as it sounds.

So, for the time being, it's probably better to _NOT_ use Catalina
to make release builds.
2020-02-20 15:08:52 +01:00
Markus Fisch 3237467503 Check for macOS Catalina and use appropriate tools
Since RenderScript can no longer be compiled on macOS Catalina
because Catalina cannot run 32bit binaries and Google failed
to provide a 64bit compiler in time. Now, as if that wasn't
bad enough, the new build tools that provide a 64bit compiler
don't work on any other system than macOS unfortunately.

For details on the issue see:
https://issuetracker.google.com/issues/142590626
2020-02-20 15:08:06 +01:00
Markus Fisch 346529469c Revert "Update build tools for macOS Catalina"
This reverts commit cf4020c613.

Those new build tools don't work on other systems than macOS
Catalina and even there produce broken builds :(

Also, it looks like the only solution is to remove
`RenderScript.forceCompat()` what's probably no longer
required to support Lineage 16.0 with those new build tools.
Confirmation pending.
2020-02-18 16:23:39 +01:00
zmniandGitHub 9f4e420dbb Update Indonesian translation 2020-02-17 15:09:33 +01:00
Markus Fisch cf4020c613 Update build tools for macOS Catalina
To make RenderScript compile again on Catalina.

For details see:
https://issuetracker.google.com/issues/142590626
2020-02-17 10:43:10 +01:00
Markus Fisch 93edde7ed1 Specify full backup rules
"true" isn't a valid value for android:fullBackupContent.
2020-02-17 10:36:19 +01:00
unbranchedandGitHub 362e462c2a Update Italian translation 2020-02-09 18:51:08 +01:00
Markus Fisch a7241bc0fb Fix typo in remark about commit messages 2020-02-08 12:43:57 +01:00
Markus Fisch f26046f34b Add links for all supported barcode formats
And put them into a list.
2020-02-08 12:39:44 +01:00
zmniandMarkus Fisch a70813ea5c Update Indonesian translation 2020-01-12 13:46:32 +01:00
Markus Fisch 3f251fdb66 Advance version number to 1.21.0 2019-12-29 17:03:28 +01:00
Markus Fisch b76cf329dd Ignore PickActivity for Firebase App Indexing
Because Lint/Firebase can't handle the VIEW intent filter
without a http URL. What doesn't make any sense here.
2019-12-29 17:00:18 +01:00
Markus Fisch e0ad177a62 Improve loading big images
Load a scaled down bitmap instead of trying to load the
whole image and then scaling it down. If the image is too
large, it cannot be loaded completely in the first place.
2019-12-29 13:57:35 +01:00
Markus Fisch 1223bd3679 Remove file scheme for VIEW intent
Because newer Androids use the "content://" scheme instead of the
"file://" scheme. So remove this restriction.
2019-12-29 12:44:33 +01:00
Markus Fisch b4e24fa328 Save and restore history list state
If we're going back to the list. Makes it far easier to
look through a long list.
2019-12-25 18:14:08 +01:00
Markus Fisch 0368ab5f68 Advance version number to 1.20.1 2019-12-22 13:50:05 +01:00
Markus Fisch c44ee8f063 Directly invoke insets listener below Lollipop
Because ViewCompat.setOnApplyWindowInsetsListener() will never
fire on devices below Lollipop.
2019-12-22 13:45:33 +01:00
Markus Fisch 3bfa6345a0 Update description in README 2019-12-21 18:31:56 +01:00
Markus Fisch 77d87ba995 Advance version number to 1.20.0 2019-12-21 18:13:48 +01:00
Markus Fisch a2ac68028f Put all FABs in a CoordinatorLayout 2019-12-21 17:49:56 +01:00
Markus Fisch aeb945f145 Make system bars a bit less transparent
So the foreground text can be seen better.
2019-12-21 17:49:28 +01:00
Markus Fisch aadde397ea Color nav bar when list is as high as the view too
Because there's now a list footer, the navigation bar at
the bottom of the screen must be translucent as soon as
the last item touches the end of the screen.
2019-12-21 17:49:19 +01:00
Markus Fisch 063fea913e Add additional next action item to encode fragment
Because android:windowSoftInputMode="adjustResize" doesn't go
well with android:windowFullscreen="true" and the latter one
is needed to draw below the system bars.
2019-12-21 17:26:07 +01:00
Markus Fisch 9d57889b81 Fix returning result for SCAN intents again
Actually use the flag to return the result and it's
working like it should.
2019-12-21 17:01:07 +01:00
Markus Fisch 233b69623f Revert "Fix returning result for SCAN intents"
SCAN intents are still processed in CameraActivity - not in
PickActivity.

This reverts commit 7416037762.
2019-12-21 16:59:42 +01:00
Markus Fisch c7d6e60efb Scroll content below tool and system bars 2019-12-21 16:11:23 +01:00
Markus Fisch 63688a9994 Refactor format to resolve variable shadowing 2019-12-20 10:53:07 +01:00
Markus Fisch dc74e33b8b Remove unnecessary <merge/> tag 2019-12-19 16:45:04 +01:00
Markus Fisch 81f2620a69 Add a style for secondary text
To better distinguish between content and secondary
information.
2019-12-19 16:44:29 +01:00
Markus Fisch 1c95550f5d Remove bottom margin after content on decode
Looks better with content length and format on one line
below it.
2019-12-19 16:43:19 +01:00
Markus Fisch 37a821818d Put character count and format on one line 2019-12-19 16:42:40 +01:00
Markus Fisch 29e0bdfbc3 Increase font size of barcode contents in history 2019-12-17 14:30:52 +01:00
Markus Fisch d4bbcef011 Fix transfering barcode format from history 2019-12-17 14:29:44 +01:00
Markus Fisch 81fc41ea81 Tag all activities as resizable 2019-12-17 14:11:42 +01:00
Markus Fisch 8cadf42c86 Convert encode button to a FAB
And make sure it's always reachable and above the
soft keyboard.
2019-12-17 14:09:10 +01:00
Markus Fisch e29ec2cc40 Fix some style issues
Apply auto format.
2019-12-17 13:57:54 +01:00
Markus Fisch 05952e7f7f Use explict locale when converting to lower case 2019-12-17 13:57:17 +01:00
Markus Fisch 0e8abb4607 Use a common style for all FABs 2019-12-17 13:51:22 +01:00
Markus Fisch cede9186a2 Use just 0 instead of an elaborate variable 2019-12-17 13:35:22 +01:00
Markus Fisch c1c9fa050b Fix casting context to AppCompatActivity
On Android 4.x, since support library 23.3.0, View.getContext()
returns a TintContextWrapper object instead of an Activity.
2019-12-17 13:30:27 +01:00
Markus Fisch 76d86288cc Preset file name to save with format and content
Saves typing it out again when saving a barcode as a file.
2019-12-17 11:34:42 +01:00
Markus Fisch c0902f8830 Update build tools 2019-12-17 11:11:22 +01:00
Markus Fisch 1d3546d676 Update Kotlin version 2019-12-09 19:48:50 +01:00
Balázs ÚrandMarkus Fisch 58baab6ffd Update Hungarian translation 2019-12-09 19:46:28 +01:00
zmniandMarkus Fisch 3bafa60d59 Update Indonesian translation 2019-11-28 13:25:57 +01:00
Markus Fisch 825062ba78 Advance version number to 1.19.0 2019-11-24 17:32:23 +01:00
Markus Fisch d128e3cc24 Use property access syntax
And remove explicit types.
2019-11-24 17:21:46 +01:00
Markus Fisch 83619834a8 Remember last selected barcode format
When encoding a new barcode.

Makes it easier to use the same format multiple times.
2019-11-24 17:11:54 +01:00
Markus Fisch cb16220ebb Update Kotlin and SDK version to 29 2019-11-22 10:55:20 +01:00
Markus Fisch 7f3af23773 Allow scaling a new barcode to half its size
Apparently some barcodes aren't created with enough "quiet space"
inside the generated image so scanning them from a screen can be
a little tough.
2019-11-19 21:52:20 +01:00
Markus Fisch 9898a14dd3 Advance version number to 1.18.0 2019-11-16 15:42:19 +01:00
Markus Fisch 85fc475118 Highlight codes in pick activity 2019-11-16 15:31:44 +01:00
Markus Fisch 987e8531e6 Add a setting to open contents without inspection 2019-11-16 14:46:25 +01:00
Markus Fisch 4d9a537bef Mention Downloads in Save-as-file dialog
To give the user an idea where the file will get saved.
2019-11-16 14:28:24 +01:00
Markus Fisch 05de15488e Fix cropping rotated images 2019-11-16 14:19:18 +01:00
Markus Fisch 721a3ee801 Improve visability of crop border 2019-11-16 14:16:33 +01:00
Markus Fisch bca000c21b Improve title of PickActivity 2019-11-16 13:20:20 +01:00
Markus Fisch 80afd0c5b7 Remove up navigation from PickActivity
There's no up - just back.
2019-11-16 13:10:09 +01:00
Markus Fisch 7416037762 Fix returning result for SCAN intents
Set parameter for `com.google.zxing.client.android.SCAN`
intents so the result is returned instead of displayed.
2019-11-16 13:00:57 +01:00
Markus Fisch 325aa00bf8 Make open URL default action on decode fragment
I think most users would rather want to open an URL from a
QR code (or something like that) than sharing the contents
of a barcode to another app.
2019-11-14 16:14:27 +01:00
Markus Fisch 40c2cc9b60 Improve handling of nullable types
And update a few other Kotlin improvements along the way.
2019-11-14 13:32:12 +01:00
Markus Fisch 1bf16a2fb7 Set explicit locale for case conversions
To make Lint happy. Shouldn't make a difference.
2019-11-14 13:31:12 +01:00
Markus Fisch 2687ce031a Add gradle.properties to repository
Since SonarQube is removed, this file can be part of the
repository again.
2019-11-14 12:58:43 +01:00
Markus Fisch 3213169ea1 Move PickActivity to the right directory
Misplaced by mistake.
2019-11-14 12:53:27 +01:00
Markus Fisch 5c3d6c4b31 Advance version number to 1.17.1 2019-11-13 22:07:06 +01:00
Markus Fisch 336f596d08 Add a warning to not upgrade ZXing
Do NOT upgrade ZXing because newer versions won't work on API < 24.
For details see: https://github.com/zxing/zxing/issues/1170
2019-11-13 15:39:34 +01:00
Markus Fisch 930656fbe5 Update tools version 2019-11-13 15:39:16 +01:00
Markus Fisch 3b45061974 Check if data intent of onActivityResult is null
Can be null if the user doesn't actually choose an image and
goes back instead.
2019-11-13 15:11:46 +01:00
Markus Fisch 983c3e3c38 Advance version number to 1.17.0 2019-11-12 23:38:24 +01:00
Markus Fisch 691fe05322 Add a save-as-file menu item to the barcode view
Because many people don't use the share menu.
2019-11-12 17:14:39 +01:00
Markus Fisch 569389bb12 Add some line breaks to improve readability 2019-11-12 15:22:29 +01:00
Markus Fisch da886291cf Add a menu item to pick an image file
Because many people don't use the share menu.
2019-11-12 14:42:06 +01:00
Markus Fisch 05b219b3ea Update bar transparency on draw in zoom/pinch view
It's almost the same as updating it on every touch because
every touch triggers a draw.
2019-11-12 13:34:47 +01:00
Markus Fisch 7cfefe7791 Add a manual picking activity for shared images
So the user can choose what barcode to scan if there are multiple
barcodes in the shared image. Also it's possible to focus a small
barcode in a shared image to make it readable.
2019-11-11 16:57:53 +01:00
Markus Fisch a2fe94285d Fix line break in PRIVACY 2019-11-11 16:53:34 +01:00
Markus Fisch 6429a1ba52 Apply auto format 2019-11-09 16:57:26 +01:00
Markus Fisch 477ae25fc7 Use discrete style for barcode size seekbar
Makes the steps visible.
2019-11-09 14:46:27 +01:00
Markus Fisch a2646ffdb3 Remove SonarQube integration
More trouble than it's worth.
2019-11-09 14:44:45 +01:00
Markus Fisch 7a93172876 Advance version number to 1.16.0 2019-11-07 21:52:59 +01:00
Markus Fisch 327cbe55d7 Update system bar transparency for double tap too
Double tapping the barcode (image) magnifies it so the system
bars need to be updated accordingly.
2019-11-06 15:10:31 +01:00
Markus Fisch 35ec26868c Show a toast if creating file to share fails
Give the user feedback if it's not possible to save the
generated barcode.
2019-11-06 15:00:55 +01:00
Balázs ÚrandMarkus Fisch 0c7317f09e Update Hungarian translation 2019-11-01 19:09:58 +01:00
karottenbaumandMarkus Fisch a049db588b Improve Dutch translations 2019-10-31 10:35:05 +01:00
zmniandMarkus Fisch 840691451a Update indonesian translation 2019-10-30 17:10:46 +01:00
Markus Fisch b50c956a21 Remove unused resources from rasterization script
Along with some stray tab characters and a superfluous
type annotation.
2019-10-26 13:23:21 +02:00
molikunerandMarkus Fisch 3820d647e0 Use Flows for DB access
* Replaced List with Flow to keep the memory usage low while exporting
* Updated kotlin to 1.3.50
* Updated kotlinx.coroutines to 1.3.2 to have stable Flow support
* Cleaned CSVBuilder#buildWith to simplify code
2019-10-13 16:25:27 +02:00
Markus Fisch ce234381c8 Add a comment why a post() is necessary
Apparently it's necessary to give Android some time before
coloring the system bars or it won't work always.
2019-10-13 13:16:19 +02:00
Ludovico de NittisandMarkus Fisch 2abeccdd1b Complete Italian translation 2019-10-13 12:47:14 +02:00
Markus Fisch ae4d6f8e2a Increase version code for Play upload 2019-10-12 18:24:41 +02:00
Markus Fisch 78c3253979 Remove Leak Canary
Because it's necessary to increase the minSDK for the
release build too. This makes no sense.
2019-10-12 18:20:17 +02:00
Markus Fisch 8026478052 Advance version number to 1.15.0 2019-10-12 17:45:14 +02:00
Markus Fisch 0d2ac7751e Color tool/system bars when barcode is zoomed 2019-10-12 16:55:05 +02:00
Markus Fisch 47cb298317 Add Leak Canary to debug build 2019-10-11 20:12:27 +02:00
Markus Fisch c1d0b98990 Move lambdas out of parenthesis
And some other optimizations.
2019-10-11 14:31:03 +02:00
Markus Fisch 798c10d420 Only color tool/statusbar when content is scrolled 2019-10-11 11:44:47 +02:00
Markus Fisch 0d65f41bb5 Color tool/system bars when a view is scrollable
According to Material Design, scrollable content should either
scroll to the edges of the screen or behind another surface.

So to avoid clipping contents against an invisible edge, it's
necessary to show the (usually transparent) tool and system
bars for a visible edge.
2019-10-11 10:17:29 +02:00
Markus Fisch eb32af9cda Apply auto format 2019-10-06 17:16:16 +02:00
Markus Fisch cb82374712 Update tools version 2019-10-06 17:06:28 +02:00
Markus Fisch c4f7189435 Use underscore for unused variable 2019-10-06 17:06:02 +02:00
molikunerandMarkus Fisch 2c037a6322 Add history export feature
* Added new action item in HistoryFragment to export the history
* Removed some GlobalScope to get more structured concurrency safety
* Replaced all usage of Database with calls to new DatabaseRepository
* Added alertDialog function to simplify usage as function
* Added information for successfully saved file
* Added CSVBuilder
2019-10-04 17:03:03 +02:00
MonochromeandMarkus Fisch 6b9e6ee5b4 Add an intent-filter for action VIEW
So it's possible to decode image by opening them, too.
2019-10-01 19:41:58 +02:00
molikunerandMarkus Fisch adecdf7e27 Remove parsing of OTPs
We should not check the content of the OTP code, instead let the receiver handle the
content and give appropriate errors, if needed.
2019-09-30 16:07:52 +02:00
Markus Fisch d925e136ef Fix Russian translation
Remove app_name (which is the same for all languages) and use
tabs instead of spaces for indentation.
2019-09-30 13:46:18 +02:00
molikunerandMarkus Fisch 3ae7e561c4 Standardise HTTP action
* Rename IAction uses to simplify code

* Renamed SimpleIntentIAction to IntentAction
* Renamed IntentAction#executeForIntent to createIntent
* Added suspend modifier to IAction#execute to simplify later work
* Added parseAndNormalizeUri
* Optimized usage of Coroutines in WifiAction
* Replaced one java Pattern instance with Kotlin Regex

* Simplify DecodeFragment#openUrl by moving stuff into it's own action

* Added OpenOrSearchAction
* Added WebAction
* Added DEFAULT_ACTION

* IgnoreCase in SchemeAction
2019-09-30 13:36:18 +02:00
Eugene KotsareuandMarkus Fisch ca619efa14 Add Russian translation 2019-09-24 22:23:01 +02:00
Markus Fisch 47d5923a04 Advance version number to 1.14.0 2019-09-08 21:19:52 +02:00
Markus Fisch 8c22a8ab00 Apply auto formatting 2019-09-08 21:15:12 +02:00
molikunerandMarkus Fisch 8152cb6a24 Add OTP support 2019-09-08 21:06:05 +02:00
zmniandMarkus Fisch ffed4fc5cf Update Indonesian translation 2019-09-04 16:47:56 +02:00
Markus Fisch d8bd3c1176 Advance version number to 1.13.1 2019-08-31 10:45:02 +02:00
molikunerandMarkus Fisch 3b12aa4b7b Fix crash caused by missing \R support for Regex (#62)
* Fix crash caused by missing `\R` support for Regex
* Replaced `\R` usage in VType actions with official equivalent
* Replace `\R`, which was overlooked
2019-08-31 10:38:08 +02:00
Markus Fisch 099b522d35 Advance version number to 1.13.0 2019-08-23 18:32:03 +02:00
Markus Fisch 166d181469 Optimize imports according to Android Studio
Order of imports doesn't matter so I should simply let
Android Studio have its way.
2019-08-23 17:57:17 +02:00
Markus Fisch a4a2a67177 Add missing extras for ZXing result intents
Turns out, some apps do actually use them.
2019-08-23 17:55:39 +02:00
Markus Fisch 7fe3de78af Add screenshots to README 2019-08-09 14:05:28 +02:00
Markus Fisch e75d658092 Advance version number to 1.12.0 2019-08-05 20:29:16 +02:00
Markus Fisch c840462fa2 Fix folder name for Brazilian Portuguese
According to lint, it must read `values-pt-rBR`.
2019-08-05 20:25:51 +02:00
Ivan AmaroandMarkus Fisch c3475a37d2 Add Brazilian Portuguese translation 2019-08-05 14:03:19 +02:00
yzqzssandMarkus Fisch 183fe2c6f2 Add Simplified Chinese 2019-08-04 23:58:00 +02:00
ThomasandMarkus Fisch 056f5e263e Add Chinese translation
This set of Chinese translation is good for zh, zh-Hant, zh-TW, zh-HK and other zh variants. Some English nouns are kept as they are well known in common conversations.

Many app handle Chinese localization pickings differently (with a ton of political dicisions), but in general label this as "中文"/"Chinese" or "正體中文"/“traditional Chinese”.
2019-08-04 23:57:08 +02:00
Markus Fisch 386c7a6056 Encode text using UTF-8 encoding 2019-08-04 23:55:15 +02:00
Markus Fisch 80c876f35d Update description in README
With all the new features it's probably no longer fitting to
say that it doesn't do _anything_ else but decoding a barcode.
2019-07-31 13:19:07 +02:00
Markus Fisch 30ae51d945 Suppress lint warning about null root layout
Because dialogs don't have a parent layout and must
therefore be inflated with a null root layout.
2019-07-31 13:15:28 +02:00
Markus Fisch 95b54ed4ab Add some line breaks
Improves readability (for me).
2019-07-31 13:05:10 +02:00
Markus Fisch 3650bda107 Save preferences when leaving fragment only
Much simpler.
2019-07-30 13:48:19 +02:00
Markus Fisch 2bdf13761e Add preferences and the option to use a custom URL
For opening unknown barcodes. Should close #48.
2019-07-28 13:59:42 +02:00
Markus Fisch ee4ee1f778 Advance version number to 1.11.2 2019-07-24 22:30:27 +02:00
Markus Fisch dbb72747e9 Downgrade ZXing to version 3.3.3
Apparently 3.4.0 is crashing on some devices.
Further investigation required.

This reverts commit b888e37556.
2019-07-24 22:23:30 +02:00
Markus Fisch 8e577c0924 Use WindowInsets to get system bar metrics
Since reading system resources can go wrong and WindowInsets
are the way to go.

Watch this:
https://www.youtube.com/watch?v=_mGDMVRO3iE
2019-07-24 15:12:55 +02:00
Markus Fisch 6ce160447c Advance version number to 1.11.1 2019-07-22 11:06:14 +02:00
Markus Fisch b94a47ccb4 Update CameraView library to catch exception
Catch Camera.autoFocus() exceptions since some devices
throw a RuntimeException when it should work.
2019-07-22 11:00:40 +02:00
Markus Fisch ec34b33ce7 Advance version number to 1.11.0 2019-07-18 20:52:12 +02:00
Markus Fisch 024b95c12e Reapply auto format 2019-07-18 09:57:02 +02:00
molikunerandMarkus Fisch cf4dc2aa4a Add VTypes (VCard and VEvent)
* Ignoring case on SMS and TEL actions
* Added VCard action
* Added VEvent action
* Added abstract VTypeParser
2019-07-18 09:51:01 +02:00
Markus Fisch 1919ce2935 Make generated barcodes zoom/pinch-able
I'm not sure if this has any additional value but sometimes
I like to inspect the generated codes a little closer.
2019-07-16 14:42:16 +02:00
Markus Fisch 1e3dfb64ae Fix building PDF417 barcode
Use the dimensions from the generated barcode for the
stride. ZXing adds quite some margin around a PDF417
and that breaks Bitmap.setPixels() when stride is set
to the target width.
2019-07-16 14:09:24 +02:00
Markus Fisch f87eaa82dd Advance version number to 1.10.0 2019-07-10 21:47:58 +02:00
Markus Fisch 13b90186d3 Include JUnit tests in Makefile test rule too 2019-07-10 16:21:30 +02:00
Markus Fisch 2c4e329663 Make tool and system bars completely transparent
Results in a lighter, more modern look.
2019-07-10 16:12:30 +02:00
Markus Fisch 624e129ef7 Remove duplicated code when executing share intent
And use the new function `execShareIntent()` in IAction too.
2019-07-10 16:07:19 +02:00
Markus Fisch 2f19bd7fc9 Add some line breaks to improve readability
And clean up imports which is a silly thing to do, of course.
I just can't help it.
2019-07-10 16:05:59 +02:00
Markus Fisch da48539585 Update build tools version 2019-07-10 16:02:09 +02:00
Balázs ÚrandMarkus Fisch a1d263837a Update Hungarian translation 2019-07-08 23:09:09 +02:00
molikunerandMarkus Fisch 188824ecaa Optimize code and regex
* Simplified code of WiFi parsing
* Optimized regex of Mail, SMS, and WiFi
* Fixed possible crash on Gingerbread or lower because of missing value
* Resolved unexpected behavior when no application was found to share sth
* Used more Kotlin way of getting ViewHolder variable of View tag
* Used more Kotlin way of getting the current content in DecodeFragment
2019-07-08 20:38:21 +02:00
Markus Fisch f5376290ab Convert text to bytes if byte segments are shorter
Because Zxing cuts off content prefixes like "WIFI:" from
byte segments what renders the bytes segments useless.

Since byte segments are important for binary content only,
it's probably to safe to ignore them for text content.
2019-07-05 21:10:29 +02:00
Markus Fisch b888e37556 Update ZXing to latest version 2019-07-05 21:05:00 +02:00
Markus Fisch cb43160902 Apply Kotlin Coding Conventions
Apply Android Studio's Reformat Code using Kotlin Coding Conventions,
see https://kotlinlang.org/docs/reference/coding-conventions.html
2019-07-05 15:27:15 +02:00
molikunerandMarkus Fisch a3ad6aa4c5 Add Mail, SMS and Telephone action
* Added `SimpleIntentIAction` to simplify code
* Change actions registry Array to Set
2019-07-05 15:21:37 +02:00
Markus Fisch d0f0bedae2 Restore tools:text for hex view 2019-07-05 12:58:09 +02:00
Markus Fisch 3f58349ce8 Fix indents in resources and new code
Replace spaces with tabs in string resources for new
resources.

Fix indenting according to Kotlin Coding Conventions.

Add a few line breaks and braces where it improves
readability for me.
2019-07-05 11:18:55 +02:00
molikunerandMarkus Fisch e90d7d5f0e Fix WiFi parsing and remove extra button
* Do some stuff in background to keep UI thread smooth
* Replaced WifiNetworkInfoFactory with WifiConfigurationFactory
* Added utility functions and titleResId to IAction
* Removed unused button from layout
2019-07-05 10:50:41 +02:00
Markus Fisch b39e5d2721 Replace space indents with tabs
To make the new code match to what's there.
We don't want to mix tabs with spaces.
2019-07-04 18:43:08 +02:00
Markus Fisch 3ede2713b6 Add translations for new WiFi button
Necessary or lint will fail.
2019-07-04 18:36:05 +02:00
Joe SteinandMarkus Fisch dc2fc292ce Add ability to connect to WiFi via button
* Added some tools:text for designing in Android Studio
* Add Connect to Wifi button
* Background for wifi button
* Added Action framework and WifiAction
* Unit tests for WifiAction
* Separate WiFi parsing and connecting code
* Remove network name escaping
* Make button slightly lighter when pressed
2019-07-04 18:31:43 +02:00
zmniandMarkus Fisch 3dc50d2d7a Add Indonesian translation 2019-07-01 20:38:13 +02:00
Balázs ÚrandMarkus Fisch 3d15425460 Update Hungarian translation 2019-06-28 10:36:28 +02:00
Markus Fisch 412e27ee81 Advance version number to 1.9.1 2019-06-20 14:20:00 +02:00
Markus Fisch d73ea341c0 Ask for search engine when searching for content
Instead of opening Google when searching for decoded content,
there's now a dialog to pick a suitable search engine from.

Especially useful if the user already knows what category
the scanned item belongs to.
2019-06-20 14:08:59 +02:00
Markus Fisch 694ebf83ed Advance version number to 1.9.0 2019-06-16 19:20:51 +02:00
Markus Fisch 78bb03fabf Try Google when opening content as URL fails
Better than showing an error and doing nothing.
2019-06-16 19:10:34 +02:00
Markus Fisch 902215f064 Show generic error message if Exception is empty
Because Zxing throws empty Exceptions for some barcodes.
2019-06-16 18:58:18 +02:00
Markus Fisch eaf38729bd Advance version number to 1.8.1 2019-05-26 17:04:54 +02:00
Markus Fisch bdd89ca539 Add contribution guidelines 2019-05-26 16:33:11 +02:00
Markus Fisch 39ecd31455 Advance version number to 1.8.0 2019-05-26 16:15:52 +02:00
Markus Fisch c96a2cf3ec Assume binary data if content is empty 2019-05-26 16:08:52 +02:00
Markus Fisch ea9e29b98c Support for saving binary data into external file
Since sharing binary data doesn't make much sense.
2019-05-26 15:57:55 +02:00
Markus Fisch 96e1ef5a52 Save binary data as blob
Required to restore it correctly.
2019-05-26 15:55:02 +02:00
Markus Fisch ed4b0a75dd Remove horizontal orientation line from scanner
Looks cleaner without that boring line.
2019-05-24 10:18:29 +02:00
Markus Fisch 37354338cb Increase button height for API < 21
48dp is the recommended touch height.
2019-05-21 10:48:27 +02:00
Markus Fisch 3cd2eecc22 Advance version number to 1.7.3 2019-05-20 20:17:04 +02:00
Markus Fisch 0e40d3f7c3 Use idiomatic Kotlin syntax 2019-05-20 20:12:18 +02:00
Markus Fisch c8fa01116d Hide binary data and text-only actions
Show binary data in hex dump only.
2019-05-20 20:10:42 +02:00
Markus Fisch 04539b4c37 Remove RenderScript fallback
Isn't needed anymore now that RenderScript works on Lineage 16.0.
2019-05-20 20:09:25 +02:00
Markus Fisch b9e8f4cfca Invoke forceCompat() to fix crashes on Lineage
For some reason invoking RenderScript.forceCompat() makes
RenderScript finally work on Lineage 16.0. Without that,
RenderScript will crash on (at least a Samsung Galaxy S5
running) Lineage 16.0 as soon as it's initialized.
Seems to be a device/system bug.

The solution was found by https://github.com/nonnymoose
Thanks a lot! :D

For more details about this please see:
https://github.com/markusfisch/BinaryEye/issues/21
2019-05-20 10:57:38 +02:00
Markus Fisch ad06c2bf9e Update build tools 2019-05-17 21:03:35 +02:00
Markus Fisch 2c7ec74b1b Add rs_fp_relaxed to rotator RenderScript
The docs recommend using rs_fp_relaxed because it "may be
beneficial on some architectures due to additional optimizations
only available with relaxed precision (such as SIMD CPU
instructions)."
2019-05-15 22:10:31 +02:00
Markus Fisch b34f29c95f Catch RSRuntimeException earlier
Turns out ScriptC_rotator() can raise a RSRuntimeException
so instantiating the Preprocessor object needs to be
guarded in the try/catch block.
2019-05-15 22:05:56 +02:00
Markus Fisch e11518c3f5 Use metadata byte segments for hex dump
Some barcodes can contain binary data (e.g. QR code in 8bit mode).
But Result.getText() returns an already translated string.
2019-05-15 13:41:27 +02:00
Markus Fisch 57c4fad307 Advance version number to 1.7.2 2019-05-14 19:27:43 +02:00
Markus Fisch 856b8e37e1 Add signing config to release target 2019-05-14 19:27:27 +02:00
Markus Fisch 6b8dd8e864 Add a fallback for RenderScript
Since RenderScript fails on some devices/ROMs for unknown
reasons (e.g. Samsung Galaxy S5 with Lineage OS 16.0).

I have confirmed it's not this app by trying the official
Google sample for RenderScript which crashed too.
So I believe it's a system issue.

Now, instead of crashing, the app provides a fallback for
YUV conversion and image rotation. Of course, it's much
slower than the RenderScript implementation but still
better than crashing the app.
2019-05-14 19:14:59 +02:00
Markus Fisch c993cd53f1 Add italian translation 2019-05-12 21:23:32 +02:00
Markus Fisch 934983f668 Add shadow to Google Play icon 2019-05-09 23:02:13 +02:00
Markus Fisch ccf425faad Add new Google Play icon
According to new icon specification.
2019-05-09 22:34:45 +02:00
Markus Fisch a244763109 Move signing configuration into app/build.gradle
And add a rule to build an app bundle.
2019-05-09 20:38:36 +02:00
Markus Fisch 7ab712e040 Advance version number to 1.7.1 2019-05-08 11:09:25 +02:00
Markus Fisch cb7219b290 Update to RenderScript API 24 to build 64bit libs
Building against RenderScript API 18 doesn't produce
librs.rotator.so for arm64-v8a and x86_64 architectures.
2019-05-08 10:58:55 +02:00
Markus Fisch 7f4ba50ee4 Update gradle, build tools and remove Error Prone
I liked Error Prone. But the old version is incompatible
with Gradle 5 now and using a newer one requires an amount
of work I'm not willing to invest.
2019-05-07 17:05:50 +02:00
Markus Fisch b1f00f0592 Remove unused member variable 2019-05-07 10:51:54 +02:00
Markus Fisch 80f8f908d2 Advance version number to 1.7.0 2019-05-01 13:44:51 +02:00
Markus Fisch 506308ece9 Remove now unused remove_scan string resource 2019-05-01 13:04:08 +02:00
Markus Fisch baecded9d5 Pick list separator on sharing history listing
Depending on the use case, it may be better to have
a dividing line or not.
2019-05-01 12:31:54 +02:00
Markus Fisch ceefee397f Remove extra decoding thread
Unnecessary since CameraView 1.8.2 because Camera.PreviewCallback
is called from a separate thread already.
2019-05-01 12:11:18 +02:00
Markus Fisch e45804be31 Update URL to F-Droid badge 2019-04-30 10:08:54 +02:00
Balázs ÚrandMarkus Fisch edef1acbfb Update Hungarian translation (#28) 2019-04-24 22:08:34 +02:00
Markus Fisch 742939182c Advance version number to 1.6.0 2019-04-24 17:52:46 +02:00
Markus Fisch a21bfcdea6 Share history listing as plain text
Feature request.
2019-04-24 17:36:05 +02:00
Markus Fisch bd281e449b Add missing barcode_info plural for french 2019-04-24 16:49:49 +02:00
Markus Fisch 0e1464a15f Add separator line between history items
Makes it easier to separate list items.
2019-04-24 16:48:47 +02:00
Markus Fisch 8a6441ddf3 Add content description for share FABs
Improve accessibility.
2019-04-24 16:47:48 +02:00
Markus Fisch 94874c0a9e Add content description for flash FAB
Improve accessibility.
2019-04-24 16:46:38 +02:00
Markus Fisch b9a6410959 Advance version number to 1.5.1 2019-04-12 20:46:26 +02:00
Markus Fisch 512f5e00dd Show content bytes instead of chars in hex dump 2019-04-12 20:42:21 +02:00
301 changed files with 10130 additions and 2363 deletions
+1 -1
View File
@@ -1,8 +1,8 @@
*.DS_Store
*.swp
.gradle
.idea
build
local.properties
gradle.properties
infer-out
*.iml
+269
View File
@@ -1,5 +1,274 @@
# Change Log
## 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
* Update Indonesian translation
* Fix initializing of RenderScript automatically
* Fix highlighting after landscape to landscape rotations
## 1.27.1
* Update Russian translation
* Force compat mode if native RenderScript crashed
## 1.27.0
* Export SQLite database
* Improve order of preferences
* Update Italian translation
## 1.26.0
* Zoom in & out by swiping vertically in camera view
* Add a setting for swipe to zoom feature
* Optimize vector drawables
* Update Indonesian translation
## 1.25.1
* Fix Proguard configuration
## 1.25.0
* Add giving scans individual names in history listing
* Add searching the history
* Add a setting to search harder for barcodes
* Add a setting to control vibration on detection
* Add app shortcuts for launchers that support them
* Remove saving barcodes for intents from other apps
* Show latest possible result point in camera view
* Suggest a WiFi network on Android Q+
* Always put parsed WiFi password into clipboard
* Use native image processing where available
* Ignore consecutive duplicates by default
* Fix saving external files on Android Q+
* Fix order of metadata items
## 1.24.0
* Show barcode metadata
* Add a setting to show/hide metadata
* Include metadata in CSV export
* Update italian translation
* Fix resetting flash button
* Fix CSV export for Android Q
## 1.23.0
* Add a setting to not save consecutive double scans
* Add a setting to show/hide hex dump
* Limit history items to just one line
* Fix window insets for consecutive screens
## 1.22.3
* Fix broken RenderScript built
## 1.22.2
* Fix getting window insets for Android Lollipop and above
## 1.22.1
* Ensure non-printable content is saved as raw data
## 1.22.0
* Highlight detected barcodes in camera view
* Add bing and yandex search engines
* Update Chinese Translation
* Update Italian translation
* Update Indonesian translation
## 1.21.0
* Save and restore history list state
* Improve scanning from large images
* Fix opening image files by "Open with"
## 1.20.1
* Fix window insets below Android Lollipop
## 1.20.0
* Scroll content below tool and system bars
* Preset file name to save with format and content
* Add encode menu item to make encoding easier when keyboard is visible
* Increase font size of barcode contents in history
* Update Hungarian translation
* Update Indonesian translation
* Fix encoding for old Androids
* Fix returning result for ZXing's SCAN intent again
* Fix transfering barcode format from history
## 1.19.0
* Allow scaling a new barcode to half its size
* Remember last selected barcode format
## 1.18.0
* Make open URL default action after decoding a barcode
* Add a setting to open contents without inspection
* Highlight codes for picking in shared/loaded images
* Improve visability of crop border
* Fix returning result for ZXing's SCAN intent
## 1.17.1
* Fix navigating back without choosing an image
## 1.17.0
* Pinch/Zoom shared/loaded images to scan just a section
* Add a load file menu item to load an image file
* Add a save file menu item to the barcode view
* Use discrete style for size seek bar
* Fix bar transparency when zooming/pinching
## 1.16.0
* Improve displaying scanned barcodes
* Show a message if a barcode cannot be shared
* Fix setting system bar transparency when double taping a barcode
* Update Dutch translation
* Update Hungarian translation
* Update Indonesian translation
* Update Italian translation
## 1.15.0
* Additionally export history as CSV file
* Color tool and system bars when a view is scrolled
* Add an intent filter for opening images
* Add Russian translation
## 1.14.0
* Add OTP support
* Update Indonesian translation
## 1.13.1
* Fix crash caused by missing support for Regex on Android < 6
## 1.13.0
* Add missing extras for ZXing result intents
## 1.12.0
* Add preferences dialog and the option to use a custom URL with read contents
* Add Brazilian Portuguese translation
* Add Chinese translation
* Encode text using UTF-8 encoding when creating a barcode
## 1.11.2
* Downgrade ZXing version
* Improve getting system UI metrics
## 1.11.1
* Fix tap to focus on some devices
## 1.11.0
* Add support for VCard and VEvent barcodes contents
* Generated barcodes are now zoomable
* Fix creating PDF417 barcodes
## 1.10.0
* Add support for special barcode contents (WiFi, SMS, phone, E-mail)
* Add Indonesian translation
* Update Hungarian translation
* Make system bars completely transparent
## 1.9.1
* Pick search engine for content that is not an URL
## 1.9.0
* Try Google when opening content as URL fails
* Show generic error message if barcode generation fails
## 1.8.1
* Distribute APK instead of App Bundle
## 1.8.0
* Save binary data into external file
* Fix storage of binary data
* Remove horizontal orientation line from scanner
## 1.7.3
* Fix crashes on Lineage and remove RenderScript fallback
* Improve handling of binary data
## 1.7.2
* Add a fallback for RenderScript
* Add italian translation
## 1.7.1
* Fix support for 64 bit architectures
## 1.7.0
* Pick list separator on sharing history listing
* Update Hungarian translation
## 1.6.0
* Share history listing as plain text
* Add separator line between history items
* Add content description for floating action buttons
## 1.5.1
* Fix generating hex dump
## 1.5.0
* Show a hex dump of decoded contents
* Fix processing text from share menu
+20
View File
@@ -0,0 +1,20 @@
# Contribution Guidelines
Please try to keep things in good shape and comply to what's there.
This project follows Android [best practices][android_best_practices]
so please have a look if you've never heard of them.
The code is formatted according to Android Studio's standard, with the
exception of indent being tabs instead of spaces.
Use the feature branch workflow to add new features and make sure
to squash when merging into master:
$ git merge cool_feature --squash
Then write a [good commit message][commit_messages] to keep the history
meaningful and useful. One feature, one commit.
[android_best_practices]: https://developer.android.com/distribute/best-practices/develop/
[commit_messages]: https://juffalow.com/other/write-good-git-commit-message
+15 -11
View File
@@ -1,5 +1,4 @@
PACKAGE = de.markusfisch.android.binaryeye
APK = app/build/outputs/apk/debug/app-debug.apk
all: debug install start
@@ -7,26 +6,25 @@ debug:
./gradlew assembleDebug
release: lint
@./gradlew assembleRelease \
-Pandroid.injected.signing.store.file=$(ANDROID_KEYFILE) \
-Pandroid.injected.signing.store.password=$(ANDROID_STORE_PASSWORD) \
-Pandroid.injected.signing.key.alias=$(ANDROID_KEY_ALIAS) \
-Pandroid.injected.signing.key.password=$(ANDROID_KEY_PASSWORD)
./gradlew assembleRelease
bundle: lint
./gradlew bundleRelease
lint:
./gradlew lintDebug
sonarqube:
./gradlew sonarqube
infer: clean
infer -- ./gradlew assembleDebug
test:
./gradlew cAT
./gradlew test
cat:
./gradlew test cAT
install:
adb $(TARGET) install -r $(APK)
adb $(TARGET) install -r app/build/outputs/apk/debug/app-debug.apk
start:
adb $(TARGET) shell 'am start -n \
@@ -35,8 +33,14 @@ start:
uninstall:
adb $(TARGET) uninstall $(PACKAGE).debug
meminfo:
adb shell dumpsys meminfo $(PACKAGE).debug
images:
svg/update.sh
avocado:
avocado $(shell fgrep -rl '<vector' app/src/main/res)
clean:
./gradlew clean
+49 -1
View File
@@ -2,4 +2,52 @@
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.
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)
+105 -8
View File
@@ -2,18 +2,115 @@
Yet another barcode scanner for Android. As if there weren't [enough][play].
This one is totally free, open source and does use the [ZXing][zxing] barcode
scanning library.
This one is free, without any ads and open source.
Unlike many others it works in portrait and landscape orientation, can read
inverted codes, uses Material Design and doesn't do anything else but decoding
a barcode.
Works in portrait and landscape orientation, can read inverted codes,
comes in Material Design and can also generate barcodes.
<a href="https://f-droid.org/en/packages/de.markusfisch.android.binaryeye/"><img alt="Get it on F-Droid" src="https://f-droid.org/badge/get-it-on.png" height="80"/></a> <a href="https://play.google.com/store/apps/details?id=de.markusfisch.android.binaryeye"><img alt="Get it on Google Play" src="https://play.google.com/intl/en_us/badges/images/generic/en_badge_web_generic.png" height="80"/></a>
Binary Eye uses the [ZXing][zxing] ("Zebra Crossing") barcode scanning
library.
And it's written in [Kotlin][kotlin] because I wanted to explore that a
bit more.
## Screenshots
<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/screencap-scanning.png"
alt="Screenshot Gallery" width="160"/>
<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/screencap-scanning-cropped.png"
alt="Screenshot Gallery" width="160"/>
<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/screencap-decoded.png"
alt="Screenshot Theme" width="160"/>
<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/screencap-compose-barcode.png"
alt="Screenshot Editor" width="160"/>
## Download
<a href="https://f-droid.org/en/packages/de.markusfisch.android.binaryeye/"><img alt="Get it on F-Droid" src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png" height="80"/></a> <a href="https://play.google.com/store/apps/details?id=de.markusfisch.android.binaryeye"><img alt="Get it on Google Play" src="https://play.google.com/intl/en_us/badges/images/generic/en_badge_web_generic.png" height="80"/></a>
## Supported Barcode Formats
### Read
ZXing can read the following barcode formats:
* [AZTEC][aztec]
* [CODABAR][codabar]
* [CODE 39][code_39]
* [CODE 93][code_93]
* [CODE 128][code_128]
* [DATA MATRIX][data_matrix]
* [EAN 8][ean_8]
* [EAN 13][ean_13]
* [ITF][itf]
* [MAXICODE][maxicode] (only when unrotated and unskewed, see [77][77],
because of which Binary Eye *cannot* read this barcode)
* [PDF417][pdf417]
* [QR CODE][qr_code]
* [RSS 14][rss]
* [RSS EXPANDED][rss]
* [UPC A][upc_a]
* [UPC E][upc_e]
* [UPC EAN EXTENSION][upc_ean]
### Generate
ZXing can generate the following barcode formats:
* [AZTEC][aztec]
* [CODABAR][codabar]
* [CODE 39][code_39]
* [CODE 128][code_128]
* [DATA MATRIX][data_matrix]
* [EAN 8][ean_8]
* [EAN 13][ean_13]
* [ITF][itf]
* [PDF 417][pdf417]
* [QR CODE][qr_code]
* [UPC A][upc_a]
## RenderScript
This app uses [RenderScript][rs] to resize and rotate the camera image.
Unfortunately, RenderScript has some nasty gotchas.
### RenderScript.forceCompat()
It's necessary to call `RenderScript.forceCompat()` on some devices/roms.
`RenderScript.forceCompat()` needs to be run before any other RenderScript
function and unfortunately there is no way to know if invoking `forceCompat()`
is necessary or not.
If `RenderScript.forceCompat()` is necessary, a `RSRuntimeException` will
be thrown and the only option is to restart the app, this time with calling
`forceCompat()` first.
Calling `RenderScript.forceCompat()` means the processing is done in
software so you probably don't want to enable it by default.
### 2D barcodes
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.
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
[kotlin]: http://kotlinlang.org/
[aztec]: https://en.wikipedia.org/wiki/Aztec_Code
[codabar]: https://en.wikipedia.org/wiki/Codabar
[code_39]: https://en.wikipedia.org/wiki/Code_39
[code_93]: https://en.wikipedia.org/wiki/Code_93
[code_128]: https://en.wikipedia.org/wiki/Code_128
[data_matrix]: https://en.wikipedia.org/wiki/Data_Matrix
[ean_8]: https://en.wikipedia.org/wiki/EAN-8
[ean_13]: https://en.wikipedia.org/wiki/International_Article_Number
[itf]: https://en.wikipedia.org/wiki/Interleaved_2_of_5
[maxicode]: https://en.wikipedia.org/wiki/MaxiCode
[pdf417]: https://en.wikipedia.org/wiki/PDF417
[qr_code]: https://en.wikipedia.org/wiki/QR_code
[rss]: https://en.wikipedia.org/wiki/GS1_DataBar
[upc_a]: https://en.wikipedia.org/wiki/Universal_Product_Code
[upc_e]: https://en.wikipedia.org/wiki/Universal_Product_Code#UPC-E
[upc_ean]: https://en.wikipedia.org/wiki/Universal_Product_Code#EAN-13
[77]: https://github.com/markusfisch/BinaryEye/issues/77
[rs]: https://developer.android.com/guide/topics/renderscript/compute
+25 -12
View File
@@ -1,10 +1,5 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'net.ltgt.errorprone'
configurations.errorprone {
resolutionStrategy.force 'com.google.errorprone:error_prone_core:2.0.19'
}
android {
compileSdkVersion sdk_version
@@ -14,20 +9,32 @@ android {
minSdkVersion 9
targetSdkVersion sdk_version
versionCode 19
versionName '1.5.0'
versionCode 75
versionName '1.36.0'
// it's recommended to set this value to the lowest API level
// able to provide all the functionality
renderscriptTargetApi 18
renderscriptTargetApi 24
renderscriptSupportModeEnabled true
renderscriptSupportModeBlasEnabled true
testInstrumentationRunner 'android.support.test.runner.AndroidJUnitRunner'
}
signingConfigs {
config {
keyAlias System.getenv('ANDROID_KEY_ALIAS')
keyPassword System.getenv('ANDROID_KEY_PASSWORD')
storePassword System.getenv('ANDROID_STORE_PASSWORD')
def filePath = System.getenv('ANDROID_KEYFILE')
storeFile filePath ? file(filePath) : null
}
}
sourceSets {
main.java.srcDirs += 'src/main/kotlin'
androidTest.java.srcDirs += 'src/androidTest/kotlin'
test.java.srcDirs += 'src/test/kotlin'
}
buildTypes {
@@ -39,6 +46,7 @@ android {
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
minifyEnabled true
shrinkResources true
signingConfig signingConfigs.config
}
}
@@ -51,18 +59,23 @@ android {
}
dependencies {
androidTestImplementation ('com.android.support.test:runner:1.0.2') {
androidTestImplementation('com.android.support.test:runner:1.0.2') {
exclude module: 'support-annotations'
}
androidTestImplementation ('com.android.support.test:rules:1.0.2') {
androidTestImplementation('com.android.support.test:rules:1.0.2') {
exclude module: 'support-annotations'
}
testImplementation 'junit:junit:4.12'
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.0.1"
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.8.3'
implementation 'com.github.markusfisch:CameraView:1.9.1'
implementation 'com.github.markusfisch:ScalingImageView:1.3.0'
}
+2 -2
View File
@@ -3,10 +3,10 @@
<!-- Lint can't understand Kotlin sources and reports all resources
as unused -->
<issue id="UnusedResources">
<ignore path="src/main/res/"/>
<ignore path="src/main/res/" />
</issue>
<!-- Ignore RenderScript output -->
<issue id="UnusedResources">
<ignore path="*/res/rs/debug/raw/bc*"/>
<ignore path="*/res/rs/debug/raw/bc*" />
</issue>
</lint>
+2
View File
@@ -1,4 +1,6 @@
-dontwarn com.google.**
-dontwarn android.support.v7.**
-dontwarn android.support.v8.**
-keep class android.support.v7.widget.SearchView { *; }
-keep class android.support.v8.renderscript.** { *; }
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,34 +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"
)
for (file in assets.list("yuv")) {
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).toInt()
val frameHeight = m.group(2).toInt()
val frameOrientation = m.group(3).toInt()
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(),
)
val preprocessor = Preprocessor(
rs,
frameWidth,
frameHeight,
frameOrientation
)
preprocessor.process(frameData)
val result = zxing.decode(
frameData,
preprocessor.outWidth,
preprocessor.outHeight
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
+41 -7
View File
@@ -5,17 +5,34 @@
<supports-screens
android:largeScreens="true"
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="true"
android:fullBackupContent="@xml/backup_rules"
android:supportsRtl="true"
android:resizeableActivity="true"
android:label="@string/app_name"
android:icon="@mipmap/ic_launcher"
android:theme="@style/AppTheme">
@@ -27,6 +44,9 @@
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
<meta-data
android:name="android.app.shortcuts"
android:resource="@xml/shortcuts"/>
</activity>
<activity
android:name=".activity.CameraActivity"
@@ -36,11 +56,6 @@
<category android:name="android.intent.category.DEFAULT"/>
<data android:mimeType="text/plain"/>
</intent-filter>
<intent-filter android:label="@string/decode_barcode">
<action android:name="android.intent.action.SEND"/>
<category android:name="android.intent.category.DEFAULT"/>
<data android:mimeType="image/*"/>
</intent-filter>
<intent-filter>
<action android:name="com.google.zxing.client.android.SCAN"/>
<category android:name="android.intent.category.DEFAULT"/>
@@ -55,9 +70,25 @@
android:scheme="http"/>
</intent-filter>
</activity>
<activity
android:name=".activity.PickActivity"
android:label="@string/pick_code_to_scan"
tools:ignore="AppLinkUrlError">
<intent-filter android:label="@string/decode_barcode">
<action android:name="android.intent.action.SEND"/>
<category android:name="android.intent.category.DEFAULT"/>
<data android:mimeType="image/*"/>
</intent-filter>
<intent-filter android:label="@string/decode_barcode">
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<data android:mimeType="image/*"/>
</intent-filter>
</activity>
<activity
android:name=".activity.MainActivity"
android:label="@string/app_name"/>
android:label="@string/app_name"
android:exported="true"/>
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="${applicationId}.provider"
@@ -67,5 +98,8 @@
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths"/>
</provider>
<meta-data
android:name="android.app.searchable"
android:resource="@xml/searchable"/>
</application>
</manifest>
@@ -0,0 +1,30 @@
package de.markusfisch.android.binaryeye.actions
import de.markusfisch.android.binaryeye.actions.mail.MailAction
import de.markusfisch.android.binaryeye.actions.otpauth.OtpauthAction
import de.markusfisch.android.binaryeye.actions.search.OpenOrSearchAction
import de.markusfisch.android.binaryeye.actions.sms.SmsAction
import de.markusfisch.android.binaryeye.actions.tel.TelAction
import de.markusfisch.android.binaryeye.actions.vtype.vcard.VCardAction
import de.markusfisch.android.binaryeye.actions.vtype.vevent.VEventAction
import de.markusfisch.android.binaryeye.actions.web.WebAction
import de.markusfisch.android.binaryeye.actions.wifi.WifiAction
object ActionRegistry {
val DEFAULT_ACTION: IAction = OpenOrSearchAction
private val REGISTRY: Set<IAction> = setOf(
MailAction,
OtpauthAction,
SmsAction,
TelAction,
VCardAction,
VEventAction,
WebAction,
WifiAction
)
fun getAction(data: ByteArray): IAction = REGISTRY.find {
it.canExecuteOn(data)
} ?: DEFAULT_ACTION
}
@@ -0,0 +1,48 @@
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.widget.toast
interface IAction {
val iconResId: Int
val titleResId: Int
fun canExecuteOn(data: ByteArray): Boolean
suspend fun execute(context: Context, data: ByteArray)
}
abstract class IntentAction : IAction {
abstract val errorMsg: Int
final override suspend fun execute(context: Context, data: ByteArray) {
val intent = createIntent(context, data) ?: return context.toast(
errorMsg
)
execShareIntent(context, intent)
}
abstract suspend fun createIntent(context: Context, data: ByteArray): Intent?
}
abstract class SchemeAction : IAction {
abstract val scheme: String
open val intentAction: String = Intent.ACTION_VIEW
open val buildRegex: Boolean = false
final override fun canExecuteOn(data: ByteArray): Boolean {
val content = String(data)
return if (buildRegex) {
content.matches("""^$scheme://[\w\W]+$""".toRegex(RegexOption.IGNORE_CASE))
} else {
content.startsWith("$scheme://", ignoreCase = true)
}
}
final override suspend fun execute(context: Context, data: ByteArray) {
val uri = parseAndNormalizeUri(String(data))
execShareIntent(context, Intent(intentAction, uri))
}
}
@@ -0,0 +1,30 @@
package de.markusfisch.android.binaryeye.actions.mail
import android.content.Context
import android.content.Intent
import android.net.Uri
import de.markusfisch.android.binaryeye.R
import de.markusfisch.android.binaryeye.actions.IntentAction
object MailAction : IntentAction() {
private val mailRegex =
"""^mail(?:to)?:([\w.%+-]+@[A-Za-z\d.-]+\.[A-Za-z]{2,6}(?:[?&](?:subject|body)=[\S\s]*?){0,2})$""".toRegex()
override val iconResId: Int = R.drawable.ic_action_mail
override val titleResId: Int = R.string.mail_send
override val errorMsg: Int = R.string.mail_error
override fun canExecuteOn(data: ByteArray): Boolean {
return String(data).matches(mailRegex)
}
override suspend fun createIntent(context: Context, data: ByteArray): Intent? {
val mailWithMessage = mailRegex.matchEntire(
String(data)
)?.groupValues?.get(1) ?: return null
return Intent(
Intent.ACTION_SENDTO,
Uri.parse("mailto:$mailWithMessage")
)
}
}
@@ -0,0 +1,10 @@
package de.markusfisch.android.binaryeye.actions.otpauth
import de.markusfisch.android.binaryeye.R
import de.markusfisch.android.binaryeye.actions.SchemeAction
object OtpauthAction : SchemeAction() {
override val iconResId: Int = R.drawable.ic_action_otpauth
override val titleResId: Int = R.string.otpauth_add
override val scheme: String = "otpauth"
}
@@ -0,0 +1,55 @@
package de.markusfisch.android.binaryeye.actions.search
import android.content.Context
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 java.net.URLEncoder
object OpenOrSearchAction : IAction {
override val iconResId: Int = R.drawable.ic_action_search
override val titleResId: Int = R.string.search_web
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)
}
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)
}
return null
}
private suspend fun getSearchIntent(context: Context, query: String): Intent? {
val names = context.resources.getStringArray(
R.array.search_engines_names
).toMutableList()
val urls = context.resources.getStringArray(
R.array.search_engines_values
).toMutableList()
if (prefs.openWithUrl.isNotEmpty()) {
names.add(prefs.openWithUrl)
urls.add(prefs.openWithUrl)
}
val queryUri = alertDialog<String>(context) { resume ->
setTitle(R.string.pick_search_engine)
setItems(names.toTypedArray()) { _, which ->
resume(urls[which] + URLEncoder.encode(query, "utf-8"))
}
} ?: return null
return openUri(context, queryUri, false)
}
}
@@ -0,0 +1,34 @@
package de.markusfisch.android.binaryeye.actions.sms
import android.content.Context
import android.content.Intent
import android.net.Uri
import de.markusfisch.android.binaryeye.R
import de.markusfisch.android.binaryeye.actions.IntentAction
object SmsAction : IntentAction() {
private val smsRegex =
"""^sms(?:to)?:(\+?[0-9]+)(?::([\S\s]*))?$""".toRegex(RegexOption.IGNORE_CASE)
override val iconResId: Int = R.drawable.ic_action_sms
override val titleResId: Int = R.string.sms_send
override val errorMsg: Int = R.string.sms_error
override fun canExecuteOn(data: ByteArray): Boolean {
return String(data).matches(smsRegex)
}
override suspend fun createIntent(context: Context, data: ByteArray): Intent? {
val (number: String, message: String) = smsRegex.matchEntire(
String(data)
)?.let {
it.groupValues[1] to it.groupValues[2]
} ?: return null
return Intent(
Intent.ACTION_SENDTO,
Uri.parse("smsto:$number")
).apply {
if (message.isNotEmpty()) putExtra("sms_body", message)
}
}
}
@@ -0,0 +1,23 @@
package de.markusfisch.android.binaryeye.actions.tel
import android.content.Context
import android.content.Intent
import de.markusfisch.android.binaryeye.R
import de.markusfisch.android.binaryeye.actions.IntentAction
import de.markusfisch.android.binaryeye.app.parseAndNormalizeUri
object TelAction : IntentAction() {
private val telRegex = """^tel:(\+?[0-9]+)$""".toRegex(RegexOption.IGNORE_CASE)
override val iconResId: Int = R.drawable.ic_action_tel
override val titleResId: Int = R.string.tel_dial
override val errorMsg: Int = R.string.tel_error
override fun canExecuteOn(data: ByteArray): Boolean {
return String(data).matches(telRegex)
}
override suspend fun createIntent(context: Context, data: ByteArray): Intent {
return Intent(Intent.ACTION_DIAL, parseAndNormalizeUri(String(data)))
}
}
@@ -0,0 +1,44 @@
package de.markusfisch.android.binaryeye.actions.vtype
import java.util.*
object VTypeParser {
private const val BACKSLASH_R_LEGACY =
"(?:\u000D\u000A|[\u000A\u000B\u000C\u000D\u0085\u2028\u2029])"
private val vTypeRegex =
"""^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
)
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 {
it.groupValues[1].split(';').let { typeAndInfo ->
typeAndInfo[0] to VTypeProperty(
typeAndInfo.drop(1),
it.groupValues[2]
)
}
}.groupBy({ it.first.toUpperCase(Locale.US) }) { it.second }
}
data class VTypeProperty(
val info: List<String>,
val value: String
) {
val firstTypeOrFirstInfo: String?
get() = this.info.find { it.startsWith("TYPE=", true) }?.let {
typeRegex.matchEntire(it)?.groupValues?.get(1)
} ?: this.info.firstOrNull()
companion object {
private val typeRegex = """^(?:TYPE=)(.+?)[:;]?.*$""".toRegex(RegexOption.IGNORE_CASE)
}
}
@@ -0,0 +1,185 @@
package de.markusfisch.android.binaryeye.actions.vtype.vcard
import android.content.ContentValues
import android.content.Context
import android.content.Intent
import android.os.Build
import android.provider.ContactsContract
import de.markusfisch.android.binaryeye.R
import de.markusfisch.android.binaryeye.actions.IntentAction
import de.markusfisch.android.binaryeye.actions.vtype.VTypeParser
import java.util.*
import kotlin.collections.ArrayList
object VCardAction : IntentAction() {
override val iconResId: Int
get() = R.drawable.ic_action_vcard
override val titleResId: Int
get() = R.string.vcard_add
override val errorMsg: Int
get() = R.string.vcard_failed
override fun canExecuteOn(data: ByteArray): Boolean {
return VTypeParser.parseVType(String(data)) == "VCARD"
}
override suspend fun createIntent(context: Context, data: ByteArray): Intent? {
val info = VTypeParser.parseMap(String(data))
return Intent(Intent.ACTION_INSERT_OR_EDIT).apply {
type = ContactsContract.Contacts.CONTENT_ITEM_TYPE
info["FN"]?.singleOrNull()?.also { name ->
putExtra(ContactsContract.Intents.Insert.NAME, name.value)
} ?: info["N"]?.singleOrNull()?.also { name ->
putExtra(ContactsContract.Intents.Insert.NAME, name.value.nameFormat)
}
info["TEL"]?.filter { it.value.isNotEmpty() }?.forEachIndexed { index, phoneProperty ->
val (extraPhoneType, extraPhone) = when (index) {
0 -> ContactsContract.Intents.Insert.PHONE_TYPE to
ContactsContract.Intents.Insert.PHONE
1 -> ContactsContract.Intents.Insert.SECONDARY_PHONE_TYPE to
ContactsContract.Intents.Insert.SECONDARY_PHONE
2 -> ContactsContract.Intents.Insert.TERTIARY_PHONE_TYPE to
ContactsContract.Intents.Insert.TERTIARY_PHONE_TYPE
else -> return@forEachIndexed
}
phoneProperty.firstTypeOrFirstInfo?.also {
it.phoneType?.also { phoneType ->
putExtra(extraPhoneType, phoneType)
} ?: putExtra(extraPhoneType, it)
}
putExtra(
extraPhone,
phoneProperty.value.toLowerCase(Locale.US).removePrefix("tel:")
)
}
info["EMAIL"]?.forEachIndexed { index, mailProperty ->
val (extraMailType, extraMail) = when (index) {
0 -> ContactsContract.Intents.Insert.EMAIL_TYPE to
ContactsContract.Intents.Insert.EMAIL
1 -> ContactsContract.Intents.Insert.SECONDARY_EMAIL_TYPE to
ContactsContract.Intents.Insert.SECONDARY_EMAIL
2 -> ContactsContract.Intents.Insert.TERTIARY_EMAIL_TYPE to
ContactsContract.Intents.Insert.TERTIARY_EMAIL
else -> return@forEachIndexed
}
mailProperty.firstTypeOrFirstInfo?.also {
it.mailType?.also { mailType ->
putExtra(extraMailType, mailType)
} ?: putExtra(extraMailType, it)
}
putExtra(extraMail, mailProperty.value)
}
info["ORG"]?.singleOrNull()?.also { organization ->
putExtra(ContactsContract.Intents.Insert.COMPANY, organization.value)
}
info["TITLE"]?.singleOrNull()?.also { jobTitle ->
putExtra(ContactsContract.Intents.Insert.JOB_TITLE, jobTitle.value)
}
info["ADR"]?.forEach { addr ->
addr.firstTypeOrFirstInfo?.also {
it.addressType?.also { addressType ->
putExtra(ContactsContract.Intents.Insert.POSTAL_TYPE, addressType)
} ?: putExtra(ContactsContract.Intents.Insert.POSTAL_TYPE, it)
}
putExtra(ContactsContract.Intents.Insert.POSTAL, addr.value.locationFormat)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
val urls = info["URL"]?.mapNotNull {
it.value.takeIf { url -> url.isNotBlank() }?.let { url ->
ContentValues(2).apply {
put(
ContactsContract.Data.MIMETYPE,
ContactsContract.CommonDataKinds.Website.CONTENT_ITEM_TYPE
)
put(ContactsContract.CommonDataKinds.Website.URL, url)
}
}
}
val bDays = info["BDAY"]?.mapNotNull {
it.value.takeIf { day -> day.isNotBlank() }?.let { day ->
ContentValues(3).apply {
put(
ContactsContract.Data.MIMETYPE,
ContactsContract.CommonDataKinds.Event.CONTENT_ITEM_TYPE
)
put(
ContactsContract.CommonDataKinds.Event.TYPE,
ContactsContract.CommonDataKinds.Event.TYPE_BIRTHDAY
)
put(ContactsContract.CommonDataKinds.Event.START_DATE, day)
}
}
}
putParcelableArrayListExtra(
ContactsContract.Intents.Insert.DATA,
ArrayList(listOfNotNull(urls, bDays).flatten())
)
}
}
}
private val String.locationFormat: String
get() = """^([^;]*?);([^;]*?);([^;]*?);([^;]*?);([^;]*?);([^;]*?);([^;]*?)$""".toRegex()
.matchEntire(
this
)?.groupValues?.let {
listOf(
it[1],
it[2],
it[3],
"${it[4]} ${it[6]}",
"${it[5]} ${it[7]}"
).filter { line -> line.isNotBlank() }
.joinToString("\n").takeIf { location -> location.isNotBlank() }
} ?: this
private val String.nameFormat: String
get() = """^([^;]*?);([^;]*?);([^;]*?);([^;]*?);([^;]*?)$""".toRegex()
.matchEntire(this)?.groupValues?.let {
listOf(it[4], it[2], it[3], it[1], it[5]).joinToString(" ")
.takeIf { name -> name.isNotBlank() }
} ?: this
private val String.mailType: Int?
get() = when (this.toUpperCase(Locale.US)) {
"HOME" -> ContactsContract.CommonDataKinds.Email.TYPE_HOME
"WORK" -> ContactsContract.CommonDataKinds.Email.TYPE_WORK
"OTHER" -> ContactsContract.CommonDataKinds.Email.TYPE_OTHER
"MOBILE" -> ContactsContract.CommonDataKinds.Email.TYPE_MOBILE
else -> null
}
private val String.phoneType: Int?
get() = when (this.toUpperCase(Locale.US)) {
"HOME" -> ContactsContract.CommonDataKinds.Phone.TYPE_HOME
"MOBILE", "CELL" -> ContactsContract.CommonDataKinds.Phone.TYPE_MOBILE
"WORK" -> ContactsContract.CommonDataKinds.Phone.TYPE_WORK
"FAX_WORK" -> ContactsContract.CommonDataKinds.Phone.TYPE_FAX_WORK
"FAX_HOME" -> ContactsContract.CommonDataKinds.Phone.TYPE_FAX_HOME
"PAGER" -> ContactsContract.CommonDataKinds.Phone.TYPE_PAGER
"OTHER" -> ContactsContract.CommonDataKinds.Phone.TYPE_OTHER
"CALLBACK" -> ContactsContract.CommonDataKinds.Phone.TYPE_CALLBACK
"CAR" -> ContactsContract.CommonDataKinds.Phone.TYPE_CAR
"COMPANY_MAIN" -> ContactsContract.CommonDataKinds.Phone.TYPE_COMPANY_MAIN
"ISDN" -> ContactsContract.CommonDataKinds.Phone.TYPE_ISDN
"MAIN" -> ContactsContract.CommonDataKinds.Phone.TYPE_MAIN
"OTHER_FAX" -> ContactsContract.CommonDataKinds.Phone.TYPE_OTHER_FAX
"RADIO" -> ContactsContract.CommonDataKinds.Phone.TYPE_RADIO
"TELEX" -> ContactsContract.CommonDataKinds.Phone.TYPE_TELEX
"TTY_TDD" -> ContactsContract.CommonDataKinds.Phone.TYPE_TTY_TDD
"WORK_MOBILE" -> ContactsContract.CommonDataKinds.Phone.TYPE_WORK_MOBILE
"WORK_PAGER" -> ContactsContract.CommonDataKinds.Phone.TYPE_WORK_PAGER
"ASSISTANT" -> ContactsContract.CommonDataKinds.Phone.TYPE_ASSISTANT
"MMS" -> ContactsContract.CommonDataKinds.Phone.TYPE_MMS
else -> null
}
private val String.addressType: Int?
get() = when (this.toUpperCase(Locale.US)) {
"HOME" -> ContactsContract.CommonDataKinds.StructuredPostal.TYPE_HOME
"WORK" -> ContactsContract.CommonDataKinds.StructuredPostal.TYPE_WORK
"OTHER" -> ContactsContract.CommonDataKinds.StructuredPostal.TYPE_OTHER
else -> null
}
}
@@ -0,0 +1,81 @@
package de.markusfisch.android.binaryeye.actions.vtype.vevent
import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
import android.os.Build
import android.provider.CalendarContract
import android.support.annotation.RequiresApi
import de.markusfisch.android.binaryeye.R
import de.markusfisch.android.binaryeye.actions.IntentAction
import de.markusfisch.android.binaryeye.actions.vtype.VTypeParser
import java.text.SimpleDateFormat
import java.util.*
object VEventAction : IntentAction() {
override val iconResId: Int
get() = R.drawable.ic_action_vevent
override val titleResId: Int
get() = R.string.vevent_add
override val errorMsg: Int
get() = R.string.vevent_failed
override fun canExecuteOn(data: ByteArray): Boolean {
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)
override suspend fun createIntent(context: Context, data: ByteArray): Intent? {
val info = VTypeParser.parseMap(String(data))
return Intent(Intent.ACTION_EDIT).apply {
type = "vnd.android.cursor.item/event"
info["SUMMARY"]?.singleOrNull()?.also { title ->
putExtra(CalendarContract.Events.TITLE, title.value)
}
info["DESCRIPTION"]?.singleOrNull()?.also { description ->
putExtra(CalendarContract.Events.DESCRIPTION, description.value)
}
info["LOCATION"]?.singleOrNull()?.also { location ->
putExtra(CalendarContract.Events.EVENT_LOCATION, location.value)
}
info["DTSTART"]?.singleOrNull()?.also { eventStart ->
dateFormats.simpleFindParse(eventStart.value)?.also {
putExtra(CalendarContract.EXTRA_EVENT_BEGIN_TIME, it.time)
}
}
info["DTEND"]?.singleOrNull()?.also { eventEnd ->
dateFormats.simpleFindParse(eventEnd.value)?.also {
putExtra(CalendarContract.EXTRA_EVENT_END_TIME, it.time)
}
}
}
}
@SuppressLint("SimpleDateFormat") // we definitely don't wan't the local format
private val dateFormats = listOf(
SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"),
SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss"),
SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'"),
SimpleDateFormat("yyyyMMdd'T'HHmmss"),
SimpleDateFormat("yyyy-MM-dd"),
SimpleDateFormat("yyyyMMdd")
)
private fun List<SimpleDateFormat>.simpleFindParse(data: String): Date? {
for (simpleDataFormat in this) {
return simpleDataFormat.simpleParse(data) ?: continue
}
return null
}
private fun SimpleDateFormat.simpleParse(data: String): Date? = try {
this.parse(data)
} catch (e: Exception) {
null
}
}
@@ -0,0 +1,11 @@
package de.markusfisch.android.binaryeye.actions.web
import de.markusfisch.android.binaryeye.R
import de.markusfisch.android.binaryeye.actions.SchemeAction
object WebAction : SchemeAction() {
override val iconResId: Int = R.drawable.ic_action_open
override val titleResId: Int = R.string.open_url
override val scheme: String = "https?"
override val buildRegex: Boolean = true
}
@@ -0,0 +1,33 @@
package de.markusfisch.android.binaryeye.actions.wifi
import android.content.Context
import de.markusfisch.android.binaryeye.R
import de.markusfisch.android.binaryeye.actions.IAction
import de.markusfisch.android.binaryeye.widget.toast
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
object WifiAction : IAction {
override val iconResId = R.drawable.ic_action_wifi
override val titleResId = R.string.connect_to_wifi
var password: String? = null
override fun canExecuteOn(data: ByteArray): Boolean =
WifiConnector.parse(String(data)) {
password = it
} != null
override suspend fun execute(
context: Context,
data: ByteArray
) = withContext(Dispatchers.IO) {
val wifiConfig = WifiConnector.parse(
String(data)
) ?: return@withContext
val message = WifiConnector.addNetwork(context, wifiConfig)
withContext(Dispatchers.Main) {
context.toast(message)
}
}
}
@@ -0,0 +1,370 @@
package de.markusfisch.android.binaryeye.actions.wifi
import android.content.Context
import android.net.wifi.WifiConfiguration
import android.net.wifi.WifiEnterpriseConfig
import android.net.wifi.WifiManager
import android.net.wifi.WifiNetworkSuggestion
import android.os.Build
import android.support.annotation.RequiresApi
import de.markusfisch.android.binaryeye.R
import java.util.*
/**
* Normal:
* WIFI:S:[network SSID];T:<WPA|WEP|nopass|>;P:[network password];H:<true|false|>;;
*
* WPA2 enterprise (EAP):
* WIFI:S:[network SSID];T:WPA2-EAP;H:<true|false|nopass|>;E:[EAP method];PH2:[Phase 2 method];AI:[anonymous identity];I:[identity];P:[password];;
*
* EPA methods:
* "AKA", "AKA_PRIME",
* "NONE", "PEAP",
* "PWD", "SIM",
* "TLS", "TTLS",
* "UNAUTH_TLS"
* https://developer.android.com/reference/android/net/wifi/WifiEnterpriseConfig.Eap.html
*
* Phase 2 methods:
* "AKA", "AKA_PRIME",
* "GTC", "MSCHAP",
* "MSCHAPV2", "NONE",
* "PAP", "SIM"
* https://developer.android.com/reference/android/net/wifi/WifiEnterpriseConfig.Phase2.html
*
* The fields can appear in any order. Only "S:" is required.
*/
object WifiConnector {
fun parse(
input: String,
passwordBlock: ((password: String?) -> Unit)? = null
): Any? {
val inputMap = parseMap(input) ?: return null
val parsedData = SimpleDataAccessor.of(inputMap) ?: return null
passwordBlock?.apply {
invoke(parsedData.password)
}
return if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
// WifiConfiguration is deprecated in Android Q
@Suppress("DEPRECATION")
WifiConfiguration().apply(parsedData)
} else {
WifiNetworkSuggestion.Builder().apply(parsedData)
}
}
fun addNetwork(context: Context, config: Any): Int {
val wifiManager = context.applicationContext.getSystemService(
Context.WIFI_SERVICE
) as WifiManager
return if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
// WifiConfiguration is deprecated in Android Q
@Suppress("DEPRECATION")
val wifiConfig = config as WifiConfiguration
if (wifiManager.enableWifi() &&
wifiManager.removeOldNetwork(wifiConfig) &&
wifiManager.enableNewNetwork(wifiConfig)
) {
R.string.wifi_added
} else {
R.string.wifi_config_failed
}
} else {
val suggestion = (config as WifiNetworkSuggestion.Builder).build()
val suggestions = listOf(suggestion)
// remove previous conflicting network suggestion
wifiManager.removeNetworkSuggestions(suggestions)
val result = wifiManager.addNetworkSuggestions(suggestions)
if (result == WifiManager.STATUS_NETWORK_SUGGESTIONS_SUCCESS) {
R.string.wifi_added
} else {
R.string.wifi_config_failed
}
}
}
internal fun parseMap(string: String): Map<String, String>? {
// 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
val pairRegex = """(.+?):((?:[^\\;]|\\.)*);""".toRegex()
return wifiRegex.matchEntire(
string
)?.groupValues?.get(1)?.let { pairs ->
pairRegex.findAll(pairs).map { pair ->
pair.groupValues[1].toUpperCase(Locale.US) to pair.groupValues[2]
}.toMap()
}
}
internal class SimpleDataAccessor private constructor(
private val inputMap: Map<String, String>
) {
// 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
get() = inputMap["T"]?.unescape ?: ""
internal val password: String?
get() = inputMap["P"]?.unescape
internal val hidden: Boolean
get() = inputMap["H"]?.unescape == "true"
internal val anonymousIdentity: String
get() = inputMap["AI"]?.unescape ?: ""
internal val identity: String
get() = inputMap["I"]?.unescape ?: ""
internal val eapMethod: Int?
get() = if (inputMap["E"].isNullOrEmpty()) {
requireSdk(Build.VERSION_CODES.JELLY_BEAN_MR2) {
WifiEnterpriseConfig.Eap.NONE
}
} else when (inputMap["E"]) {
"AKA" -> requireSdk(Build.VERSION_CODES.LOLLIPOP) { WifiEnterpriseConfig.Eap.AKA }
"AKA_PRIME" -> requireSdk(Build.VERSION_CODES.M) { WifiEnterpriseConfig.Eap.AKA_PRIME }
"NONE" -> requireSdk(Build.VERSION_CODES.JELLY_BEAN_MR2) { WifiEnterpriseConfig.Eap.NONE }
"PEAP" -> requireSdk(Build.VERSION_CODES.JELLY_BEAN_MR2) { WifiEnterpriseConfig.Eap.PEAP }
"PWD" -> requireSdk(Build.VERSION_CODES.JELLY_BEAN_MR2) { WifiEnterpriseConfig.Eap.PWD }
"SIM" -> requireSdk(Build.VERSION_CODES.LOLLIPOP) { WifiEnterpriseConfig.Eap.SIM }
"TLS" -> requireSdk(Build.VERSION_CODES.JELLY_BEAN_MR2) { WifiEnterpriseConfig.Eap.TLS }
"TTLS" -> requireSdk(Build.VERSION_CODES.JELLY_BEAN_MR2) { WifiEnterpriseConfig.Eap.TTLS }
"UNAUTH_TLS" -> requireSdk(Build.VERSION_CODES.N) { WifiEnterpriseConfig.Eap.UNAUTH_TLS }
else -> null
}
internal val phase2Method: Int?
get() = if (inputMap["PH2"].isNullOrEmpty()) {
requireSdk(Build.VERSION_CODES.JELLY_BEAN_MR2) {
WifiEnterpriseConfig.Phase2.NONE
}
} else when (inputMap["PH2"]) {
"AKA" -> requireSdk(Build.VERSION_CODES.O) { WifiEnterpriseConfig.Phase2.AKA }
"AKA_PRIME" -> requireSdk(Build.VERSION_CODES.O) { WifiEnterpriseConfig.Phase2.AKA_PRIME }
"GTC" -> requireSdk(Build.VERSION_CODES.JELLY_BEAN_MR2) { WifiEnterpriseConfig.Phase2.GTC }
"MSCHAP" -> requireSdk(Build.VERSION_CODES.JELLY_BEAN_MR2) { WifiEnterpriseConfig.Phase2.MSCHAP }
"MSCHAPV2" -> requireSdk(Build.VERSION_CODES.JELLY_BEAN_MR2) { WifiEnterpriseConfig.Phase2.MSCHAPV2 }
"NONE" -> requireSdk(Build.VERSION_CODES.JELLY_BEAN_MR2) { WifiEnterpriseConfig.Phase2.NONE }
"PAP" -> requireSdk(Build.VERSION_CODES.JELLY_BEAN_MR2) { WifiEnterpriseConfig.Phase2.PAP }
"SIM" -> requireSdk(Build.VERSION_CODES.O) { WifiEnterpriseConfig.Phase2.SIM }
else -> null
}
internal companion object {
internal fun of(inputMap: Map<String, String>): SimpleDataAccessor? {
return SimpleDataAccessor(inputMap).takeUnless {
inputMap["S"].isNullOrEmpty()
}
}
}
}
@RequiresApi(Build.VERSION_CODES.Q)
private fun WifiNetworkSuggestion.Builder.apply(
data: SimpleDataAccessor
): WifiNetworkSuggestion.Builder? {
fun WifiNetworkSuggestion.Builder.applyCommon(
data: SimpleDataAccessor
): WifiNetworkSuggestion.Builder? {
setSsid(data.ssid)
return this
}
fun WifiNetworkSuggestion.Builder.applySecurity(
data: SimpleDataAccessor
): WifiNetworkSuggestion.Builder? {
when (data.securityType) {
"WPA", "WPA2" -> {
data.password?.let {
setWpa2Passphrase(it)
}
}
"WPA2-EAP" -> {
data.password?.let {
setWpa2Passphrase(it)
}
setWpa2EnterpriseConfig(WifiEnterpriseConfig().apply {
identity = data.identity
anonymousIdentity = data.anonymousIdentity
password = data.password
data.eapMethod?.let { eapMethod = it }
data.phase2Method?.let { phase2Method = it }
})
}
"WPA3" -> {
data.password?.let {
setWpa3Passphrase(it)
}
}
"WPA3-EAP" -> {
data.password?.let { setWpa3Passphrase(it) }
setWpa3EnterpriseConfig(WifiEnterpriseConfig().apply {
identity = data.identity
anonymousIdentity = data.anonymousIdentity
password = data.password
data.eapMethod?.let { eapMethod = it }
data.phase2Method?.let { phase2Method = it }
})
}
}
return this
}
return this.applyCommon(data)?.applySecurity(data)
}
// WifiConfiguration is deprecated in Android Q
@Suppress("DEPRECATION")
private fun WifiConfiguration.apply(
data: SimpleDataAccessor
): WifiConfiguration? {
val ssidWithQuotes = data.ssid.quote
val passwordWithQuotes = data.password?.quoteUnlessHex
fun WifiConfiguration.applyCommon(
data: SimpleDataAccessor
): WifiConfiguration? {
allowedAuthAlgorithms.clear()
allowedGroupCiphers.clear()
allowedKeyManagement.clear()
allowedPairwiseCiphers.clear()
allowedProtocols.clear()
SSID = ssidWithQuotes
hiddenSSID = data.hidden
return this
}
fun WifiConfiguration.applySecurity(
data: SimpleDataAccessor
): WifiConfiguration? {
when (data.securityType) {
"", "nopass" -> allowedKeyManagement.set(WifiConfiguration.KeyMgmt.NONE)
"WEP" -> @Suppress("DEPRECATION") /* WEP as insecure */ {
passwordWithQuotes?.let {
wepKeys[0] = it
} ?: return null
wepTxKeyIndex = 0
allowedAuthAlgorithms.set(WifiConfiguration.AuthAlgorithm.SHARED)
allowedKeyManagement.set(WifiConfiguration.KeyMgmt.NONE)
allowedGroupCiphers.set(WifiConfiguration.GroupCipher.TKIP)
allowedGroupCiphers.set(WifiConfiguration.GroupCipher.CCMP)
allowedGroupCiphers.set(WifiConfiguration.GroupCipher.WEP40)
allowedGroupCiphers.set(WifiConfiguration.GroupCipher.WEP104)
}
"WPA" -> {
passwordWithQuotes?.let {
preSharedKey = it
} ?: return null
allowedAuthAlgorithms.set(WifiConfiguration.AuthAlgorithm.OPEN)
@Suppress("DEPRECATION") // WPA 1 is insecure and has bad performance
allowedProtocols.set(WifiConfiguration.Protocol.WPA) // WPA
allowedProtocols.set(WifiConfiguration.Protocol.RSN) // WPA2
allowedKeyManagement.set(WifiConfiguration.KeyMgmt.WPA_PSK)
allowedKeyManagement.set(WifiConfiguration.KeyMgmt.WPA_EAP)
@Suppress("DEPRECATION") // TKIP is insecure and has bad performance
allowedPairwiseCiphers.set(WifiConfiguration.PairwiseCipher.TKIP)
allowedPairwiseCiphers.set(WifiConfiguration.PairwiseCipher.CCMP)
allowedGroupCiphers.set(WifiConfiguration.GroupCipher.TKIP)
allowedGroupCiphers.set(WifiConfiguration.GroupCipher.CCMP)
}
"WPA2-EAP" -> requireSdk(Build.VERSION_CODES.JELLY_BEAN_MR2) {
passwordWithQuotes?.let {
preSharedKey = it
} ?: return null
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
allowedPairwiseCiphers.set(WifiConfiguration.PairwiseCipher.TKIP)
allowedPairwiseCiphers.set(WifiConfiguration.PairwiseCipher.CCMP)
allowedGroupCiphers.set(WifiConfiguration.GroupCipher.TKIP)
allowedGroupCiphers.set(WifiConfiguration.GroupCipher.CCMP)
enterpriseConfig.identity = data.identity
enterpriseConfig.anonymousIdentity = data.anonymousIdentity
enterpriseConfig.password = data.password
enterpriseConfig.eapMethod =
data.eapMethod ?: return null // non valid eapMethod
enterpriseConfig.phase2Method =
data.phase2Method ?: return null // non valid phase2Method
} ?: return null // api isn't high enough
}
return this
}
return this.applyCommon(data)?.applySecurity(data)
}
private inline fun <T> requireSdk(version: Int, block: () -> T): T? {
return if (Build.VERSION.SDK_INT >= version) return block() else null
}
}
// 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 ->
escaped.groupValues[1]
}
private val String.quote: String
get() = if (startsWith("\"") && endsWith("\"")) this else "\"$this\""
private val String.quoteUnlessHex: String
get() = if (isHex()) this else this.quote
// Unfortunately, we can never know whether a string consisting only
// of 0-f is a hex string. So we just make this one assumption that
// a string of 0-f that is exactly 64 characters long, is _probably_
// a hex string for raw PSK. Of course, this may fail for any string
// that just happens to be 64 characters long and contains just 0-f.
private val hexRegex = """^[0-9a-f]+$""".toRegex(
RegexOption.IGNORE_CASE
)
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
@Suppress("DEPRECATION")
return isWifiEnabled || setWifiEnabled(true)
}
// WifiConfiguration is deprecated in Android Q
@Suppress("DEPRECATION")
private fun WifiManager.removeOldNetwork(
wifiConfig: WifiConfiguration
): Boolean {
try {
configuredNetworks?.firstOrNull {
it.SSID == wifiConfig.SSID &&
it.allowedKeyManagement == wifiConfig.allowedKeyManagement
}?.networkId?.also {
removeNetwork(it)
}
} catch (e: SecurityException) {
// 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
@Suppress("DEPRECATION")
private fun WifiManager.enableNewNetwork(
wifiConfig: WifiConfiguration
): Boolean {
val id = addNetwork(wifiConfig)
if (id == -1) {
return false
}
disconnect()
return if (enableNetwork(id, true)) {
reconnect()
true
} else {
false
}
}
@@ -1,68 +1,76 @@
package de.markusfisch.android.binaryeye.activity
import com.google.zxing.Result
import de.markusfisch.android.cameraview.widget.CameraView
import de.markusfisch.android.binaryeye.app.db
import de.markusfisch.android.binaryeye.app.initSystemBars
import de.markusfisch.android.binaryeye.app.prefs
import de.markusfisch.android.binaryeye.rs.Preprocessor
import de.markusfisch.android.binaryeye.zxing.Zxing
import de.markusfisch.android.binaryeye.R
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.graphics.Bitmap
import android.graphics.Rect
import android.graphics.RectF
import android.hardware.Camera
import android.net.Uri
import android.os.Bundle
import android.os.Parcelable
import android.os.Vibrator
import android.provider.MediaStore
import android.support.v4.app.ActivityCompat
import android.support.v4.content.ContextCompat
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
import android.view.View
import android.widget.SeekBar
import android.widget.Toast
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import java.io.IOException
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.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.rs.Preprocessor
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 kotlin.math.max
import kotlin.math.min
import kotlin.math.roundToInt
class CameraActivity : AppCompatActivity() {
private val zxing = Zxing()
private val decodingRunnable = Runnable {
while (!Thread.currentThread().isInterrupted) {
val result = decodeFrame()
if (result != null) {
cameraView.post { found(result) }
break
private val zxing = Zxing(ResultPointCallback { point ->
point?.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 decodingThread: Thread? = null
private var preprocessor: Preprocessor? = null
private var frameData: ByteArray? = null
private var frameWidth: Int = 0
private var frameHeight: Int = 0
private var frameOrientation: Int = 0
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 = false
private var ignoreNext: String? = null
private var fallbackBuffer: IntArray? = null
override fun onRequestPermissionsResult(
requestCode: Int,
@@ -70,65 +78,94 @@ class CameraActivity : AppCompatActivity() {
grantResults: IntArray
) {
when (requestCode) {
REQUEST_CAMERA -> if (grantResults.size > 0 &&
grantResults[0] != PackageManager.PERMISSION_GRANTED) {
Toast.makeText(
this,
R.string.no_camera_no_fun,
Toast.LENGTH_SHORT
).show()
PERMISSION_CAMERA -> if (grantResults.isNotEmpty() &&
grantResults[0] != PackageManager.PERMISSION_GRANTED
) {
toast(R.string.no_camera_no_fun)
finish()
}
}
}
override fun onActivityResult(
requestCode: Int,
resultCode: Int,
resultData: Intent?
) {
when (requestCode) {
PICK_FILE_RESULT_CODE -> {
if (resultCode == Activity.RESULT_OK && resultData != null) {
val pick = Intent(this, PickActivity::class.java)
pick.action = Intent.ACTION_VIEW
pick.setDataAndType(resultData.data, "image/*")
startActivity(pick)
}
}
}
}
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)
setSupportActionBar(findViewById(R.id.toolbar) as Toolbar)
vibrator = getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
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) {
if (intent.type == "text/plain") {
handleSendText(intent)
} else if (intent.type?.startsWith("image/") == true) {
handleSendImage(intent)
}
if (intent?.action == Intent.ACTION_SEND &&
intent.type == "text/plain"
) {
handleSendText(intent)
}
}
override fun onDestroy() {
super.onDestroy()
preprocessor?.destroy()
resetPreProcessor()
fallbackBuffer = null
saveZoom()
saveCropHandlePos()
rs.destroy()
}
override fun onResume() {
super.onResume()
System.gc()
returnResult = "com.google.zxing.client.android.SCAN".equals(
intent.action
)
if (hasCameraPermission()) {
zxing.updateHints(prefs.tryHarder)
returnResult = "com.google.zxing.client.android.SCAN" == intent.action
if (hasCameraPermission(this)) {
openCamera()
}
}
private fun resetPreProcessor() {
preprocessor?.destroy()
preprocessor = null
}
private fun openCamera() {
cameraView.openAsync(
CameraView.findCameraId(
@Suppress("DEPRECATION")
if (frontFacing) {
Camera.CameraInfo.CAMERA_FACING_FRONT
} else {
@@ -144,41 +181,77 @@ class CameraActivity : AppCompatActivity() {
}
private fun closeCamera() {
cancelDecoding()
cameraView.close()
// closing the camera will also shut off the flash
flash = false
}
override fun onRestoreInstanceState(savedState: Bundle) {
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 -> {
startActivity(MainActivity.getHistoryIntent(this))
true
}
R.id.pick_file -> {
val chooseFile = Intent(Intent.ACTION_GET_CONTENT)
chooseFile.type = "image/*"
startActivityForResult(
Intent.createChooser(
chooseFile,
getString(R.string.pick_file)
),
PICK_FILE_RESULT_CODE
)
true
}
R.id.switch_camera -> {
switchCamera()
true
}
R.id.bulk_mode -> {
bulkMode = bulkMode xor true
item.isChecked = bulkMode
true
}
R.id.preferences -> {
startActivity(MainActivity.getPreferencesIntent(this))
true
}
R.id.info -> {
openReadme()
true
@@ -187,6 +260,10 @@ class CameraActivity : AppCompatActivity() {
}
}
private fun createBarcode() {
startActivity(MainActivity.getEncodeIntent(this))
}
private fun switchCamera() {
closeCamera()
frontFacing = frontFacing xor true
@@ -194,13 +271,15 @@ class CameraActivity : AppCompatActivity() {
}
private fun openReadme() {
startActivity(
Intent(
Intent.ACTION_VIEW, Uri.parse(
"https://github.com/markusfisch/BinaryEye/blob/master/README.md"
)
)
val intent = Intent(
Intent.ACTION_VIEW,
Uri.parse(getString(R.string.project_url))
)
if (intent.resolveActivity(packageManager) != null) {
startActivity(intent)
} else {
toast(R.string.project_url)
}
}
private fun handleSendText(intent: Intent) {
@@ -211,53 +290,52 @@ class CameraActivity : AppCompatActivity() {
}
}
private fun handleSendImage(intent: Intent) {
val uri = intent.getParcelableExtra<Parcelable>(Intent.EXTRA_STREAM) as? Uri ?: return
val bitmap = try {
MediaStore.Images.Media.getBitmap(contentResolver, uri)
} catch (e: IOException) {
null
}
if (bitmap == null) {
Toast.makeText(
this,
R.string.error_no_content,
Toast.LENGTH_SHORT
).show()
return
}
val result = zxing.decode(downsizeIfBigger(bitmap, 1024))
if (result != null) {
showResult(result)
finish()
return
}
Toast.makeText(
this,
R.string.no_barcode_found,
Toast.LENGTH_SHORT
).show()
finish()
}
private fun hasCameraPermission(): Boolean {
val permission = android.Manifest.permission.CAMERA
if (ContextCompat.checkSelfPermission(this, permission) !=
PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(
this, arrayOf(permission),
REQUEST_CAMERA
)
return false
}
return true
}
private fun initCameraView() {
cameraView.setUseOrientationListener(true)
cameraView.setTapToFocus()
@Suppress("ClickableViewAccessibility")
cameraView.setOnTouchListener(object : View.OnTouchListener {
var focus = true
var offset = -1f
var progress = 0
override fun onTouch(v: View?, event: MotionEvent?): Boolean {
event ?: return false
val pos = event.y
when (event.actionMasked) {
MotionEvent.ACTION_DOWN -> {
offset = pos
progress = zoomBar.progress
return true
}
MotionEvent.ACTION_MOVE -> {
if (prefs.zoomBySwiping) {
v ?: return false
val dist = offset - pos
val maxValue = zoomBar.max
val change = maxValue / v.height.toFloat() * 2f * dist
zoomBar.progress = min(
maxValue,
max(progress + change.roundToInt(), 0)
)
return true
}
}
MotionEvent.ACTION_UP -> {
// 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) {
v?.performClick()
return true
}
}
}
}
return false
}
})
@Suppress("DEPRECATION")
cameraView.setOnCameraListener(object : CameraView.OnCameraListener {
override fun onConfigureParameters(
parameters: Camera.Parameters
@@ -287,27 +365,41 @@ class CameraActivity : AppCompatActivity() {
}
override fun onCameraError() {
Toast.makeText(
this@CameraActivity,
R.string.camera_error,
Toast.LENGTH_SHORT
).show()
this@CameraActivity.toast(R.string.camera_error)
finish()
}
override fun onCameraReady(camera: Camera) {
frameWidth = cameraView.frameWidth
frameHeight = cameraView.frameHeight
frameOrientation = cameraView.frameOrientation
camera.setPreviewCallback { data, _ -> frameData = data }
// 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
ignoreNext = null
decoding = true
camera.setPreviewCallback { frameData, _ ->
if (decoding) {
decodeFrame(
frameData,
frameWidth,
frameHeight,
frameOrientation
)?.let { result ->
if (result.text != ignoreNext) {
postResult(result)
decoding = false
}
}
}
}
}
override fun onPreviewStarted(camera: Camera) {
startDecoding()
}
override fun onCameraStopping(camera: Camera) {
cancelDecoding()
camera.setPreviewCallback(null)
}
})
@@ -328,8 +420,10 @@ class CameraActivity : AppCompatActivity() {
override fun onStopTrackingTouch(seekBar: SeekBar) {}
})
restoreZoom()
}
@Suppress("DEPRECATION")
private fun setZoom(zoom: Int) {
val camera: Camera? = cameraView.camera
camera?.let {
@@ -358,14 +452,44 @@ 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() }
}
}
@Suppress("DEPRECATION")
private fun toggleTorchMode() {
val camera = cameraView.camera
val parameters = camera?.parameters
@@ -384,105 +508,253 @@ class CameraActivity : AppCompatActivity() {
}
}
private fun startDecoding() {
frameData = null
decodingThread = Thread(decodingRunnable)
decodingThread?.start()
}
private fun cancelDecoding() {
if (decodingThread != null) {
decodingThread?.interrupt()
try {
decodingThread?.join()
} catch (e: InterruptedException) {
// parent thread was interrupted
}
decodingThread = null
private fun decodeFrame(
frameData: ByteArray?,
frameWidth: Int,
frameHeight: Int,
frameOrientation: Int
): Result? {
frameData ?: return null
rotate = if (prefs.autoRotate) {
rotate xor true
} else {
invert = invert xor true
isPortrait(frameOrientation)
}
}
private fun decodeFrame(): Result? {
val fd = frameData
fd ?: return null
if (preprocessor == null) {
preprocessor = Preprocessor(
this,
return try {
if (recreatePreprocessor) {
resetPreProcessor()
recreatePreprocessor = false
}
val pp = preprocessor ?: createPreprocessorAndMapping(
frameWidth,
frameHeight,
frameOrientation
)
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, w, h, invert)
} catch (e: RSRuntimeException) {
prefs.forceCompat = prefs.forceCompat xor true
// now the only option is to restart the app because
// RenderScript.forceCompat() needs to be called before
// RenderScript is initialized
restartApp(this)
null
}
val pp = preprocessor
pp ?: return null
pp.process(fd)
invert = invert xor true
return zxing.decode(
fd,
}
private fun createPreprocessorAndMapping(
frameWidth: Int,
frameHeight: Int,
frameOrientation: Int
): Preprocessor {
val frameRoi = calculateFrameRect(
frameWidth,
frameHeight,
frameOrientation
)
val pp = Preprocessor(
rs,
frameWidth,
frameHeight,
frameRoi
)
val viewRect = if (frameRoi != null) {
detectorView.roi
} else {
cameraView.previewRect
}
nativeMapping = frameToView(
pp.outWidth,
pp.outHeight,
invert
frameOrientation,
viewRect
)
rotatedMapping = frameToView(
pp.outHeight,
pp.outWidth,
(frameOrientation - 90 + 360) % 360,
viewRect
)
return pp
}
private fun found(result: Result) {
cancelDecoding()
vibrator.vibrate(100)
showResult(result)
}
private fun showResult(result: Result) {
if (prefs.useHistory) {
GlobalScope.launch {
db.insertScan(
System.currentTimeMillis(),
result.text,
result.barcodeFormat.toString()
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()
)
}
}
if (returnResult) {
val resultIntent = Intent()
resultIntent.putExtra("SCAN_RESULT", result.text)
setResult(RESULT_OK, resultIntent)
finish()
return
}
startActivity(
MainActivity.getDecodeIntent(
this,
result.text,
result.barcodeFormat
)
)
}
private fun postResult(result: Result) {
// get mapping for the current rotate value
val mapping = getMapping()
cameraView.post {
val rp = result.resultPoints
if (mapping != null && rp != null && rp.isNotEmpty()) {
detectorView.mark(mapping.map(rp))
}
vibrator.vibrate()
showResult(
this@CameraActivity,
result,
returnResult,
bulkMode
)
if (bulkMode) {
ignoreNext = result.text
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 downsizeIfBigger(bitmap: Bitmap, max: Int): Bitmap {
return if (Math.max(bitmap.width, bitmap.height) > max) {
var width: Int = max
var height: Int = max
if (bitmap.width > bitmap.height) {
height = Math.round(
width.toFloat() / bitmap.width.toFloat() *
bitmap.height.toFloat()
)
} else {
width = Math.round(
height.toFloat() / bitmap.height.toFloat() *
bitmap.width.toFloat()
)
fun showResult(
activity: Activity,
result: Result,
isResult: Boolean = false,
bulkMode: Boolean = false
) {
if (isResult) {
activity.setResult(Activity.RESULT_OK, getReturnIntent(result))
activity.finish()
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 (body != null && body.isNotEmpty()) {
activity.toast(body)
} else if (code == null || code > 299) {
activity.toast(R.string.background_request_failed)
}
}
Bitmap.createScaledBitmap(bitmap, width, height, true)
} else {
bitmap
}
if (prefs.useHistory) {
scan.id = db.insertScan(scan)
}
if (!bulkMode) {
activity.startActivity(
MainActivity.getDecodeIntent(activity, scan)
)
}
}
fun getReturnIntent(result: Result): Intent {
val intent = Intent()
intent.putExtra("SCAN_RESULT", result.text)
intent.putExtra(
"SCAN_RESULT_FORMAT",
result.barcodeFormat.toString()
)
if (result.rawBytes?.isNotEmpty() == true) {
intent.putExtra("SCAN_RESULT_BYTES", result.rawBytes)
}
result.resultMetadata?.let { metadata ->
metadata[ResultMetadataType.UPC_EAN_EXTENSION]?.let {
intent.putExtra(
"SCAN_RESULT_ORIENTATION",
it.toString()
)
}
metadata[ResultMetadataType.ERROR_CORRECTION_LEVEL]?.let {
intent.putExtra(
"SCAN_RESULT_ERROR_CORRECTION_LEVEL",
it.toString()
)
}
metadata[ResultMetadataType.UPC_EAN_EXTENSION]?.let {
intent.putExtra(
"SCAN_RESULT_UPC_EAN_EXTENSION",
it.toString()
)
}
metadata[ResultMetadataType.BYTE_SEGMENTS]?.let {
var i = 0
@Suppress("UNCHECKED_CAST")
for (seg in it as Iterable<ByteArray>) {
intent.putExtra("SCAN_RESULT_BYTE_SEGMENTS_$i", seg)
++i
}
}
}
return intent
}
@@ -1,21 +1,38 @@
package de.markusfisch.android.binaryeye.activity
import com.google.zxing.BarcodeFormat
import de.markusfisch.android.binaryeye.app.initSystemBars
import de.markusfisch.android.binaryeye.app.setFragment
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.R
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.*
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.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) {
@@ -26,36 +43,56 @@ 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)
initSystemBars(this)
setSupportActionBar(findViewById(R.id.toolbar) as Toolbar)
initSystemBars(this)
val toolbar = findViewById(R.id.toolbar) as Toolbar
recordToolbarHeight(toolbar)
setSupportActionBar(toolbar)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
supportFragmentManager.addOnBackStackChangedListener {
colorSystemAndToolBars(this@MainActivity)
}
if (state == null) {
setFragment(supportFragmentManager, when {
intent?.hasExtra(HISTORY) == true -> HistoryFragment()
intent?.hasExtra(ENCODE) == true -> EncodeFragment.newInstance(
intent.getStringExtra(ENCODE)
)
intent?.hasExtra(DECODE) == true -> DecodeFragment.newInstance(
intent.getStringExtra(DECODE),
intent.getSerializableExtra(
DECODE_FORMAT
) as BarcodeFormat
)
else -> DecodeFragment()
})
setFragment(supportFragmentManager, getFragmentForIntent(intent))
}
}
companion object {
private const val PREFERENCES = "preferences"
private const val HISTORY = "history"
private const val ENCODE = "encode"
private const val DECODE = "decode"
private const val DECODE_FORMAT = "decode_format"
const val DECODED = "decoded"
private fun getFragmentForIntent(intent: Intent?): Fragment {
intent ?: return PreferencesFragment()
return when {
intent.hasExtra(PREFERENCES) -> PreferencesFragment()
intent.hasExtra(HISTORY) -> HistoryFragment()
intent.hasExtra(ENCODE) -> EncodeFragment.newInstance(
intent.getStringExtra(ENCODE)!!
)
intent.hasExtra(DECODED) -> DecodeFragment.newInstance(
intent.getParcelableExtra(DECODED)!!
)
else -> PreferencesFragment()
}
}
fun getPreferencesIntent(context: Context): Intent {
val intent = Intent(context, MainActivity::class.java)
intent.putExtra(PREFERENCES, true)
return intent
}
fun getHistoryIntent(context: Context): Intent {
val intent = Intent(context, MainActivity::class.java)
@@ -71,23 +108,22 @@ class MainActivity : AppCompatActivity() {
val intent = Intent(context, MainActivity::class.java)
intent.putExtra(ENCODE, text)
if (isExternal) {
val flagActivityClearTask =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
Intent.FLAG_ACTIVITY_CLEAR_TASK
} else 0
intent.addFlags(
android.content.Intent.FLAG_ACTIVITY_NO_HISTORY or
android.content.Intent.FLAG_ACTIVITY_CLEAR_TASK or
android.content.Intent.FLAG_ACTIVITY_NEW_TASK
Intent.FLAG_ACTIVITY_NO_HISTORY or
flagActivityClearTask or
Intent.FLAG_ACTIVITY_NEW_TASK
)
}
return intent
}
fun getDecodeIntent(
context: Context,
text: String,
format: BarcodeFormat
): Intent {
fun getDecodeIntent(context: Context, scan: Scan): Intent {
val intent = Intent(context, MainActivity::class.java)
intent.putExtra(DECODE, text)
intent.putExtra(DECODE_FORMAT, format)
intent.putExtra(DECODED, scan)
return intent
}
}
@@ -0,0 +1,206 @@
package de.markusfisch.android.binaryeye.activity
import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.Rect
import android.graphics.RectF
import android.net.Uri
import android.os.Bundle
import android.os.Parcelable
import android.os.Vibrator
import android.support.v7.app.AppCompatActivity
import android.support.v7.widget.Toolbar
import android.support.v8.renderscript.RenderScript
import android.view.Menu
import android.view.MenuItem
import com.google.zxing.Result
import de.markusfisch.android.binaryeye.R
import de.markusfisch.android.binaryeye.app.*
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.rs.fixTransparency
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)
val toolbar = findViewById(R.id.toolbar) as Toolbar
recordToolbarHeight(toolbar)
setSupportActionBar(toolbar)
supportFragmentManager.addOnBackStackChangedListener {
colorSystemAndToolBars(this@PickActivity)
}
val bitmap = fixTransparency(
rs,
getBitmapFromIntent()
)
if (bitmap == null) {
applicationContext.toast(R.string.error_no_content)
finish()
return
}
cropImageView = findViewById(R.id.image) as CropImageView
cropImageView.restrictTranslation = false
cropImageView.setImageBitmap(bitmap)
cropImageView.onScan = {
scanWithinBounds(bitmap)
}
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 {
menuInflater.inflate(R.menu.activity_pick, menu)
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.rotate -> {
rotateClockwise()
true
}
else -> super.onOptionsItemSelected(item)
}
}
private fun rotateClockwise() {
cropImageView.imageRotation = cropImageView.imageRotation + 90 % 360
}
private fun loadSentImage(intent: Intent): Bitmap? {
val uri = intent.getParcelableExtra<Parcelable>(
Intent.EXTRA_STREAM
) as? Uri ?: return null
return loadImageUri(contentResolver, uri)
}
private fun loadImageToOpen(intent: Intent): Bitmap? {
val uri = intent.data ?: return null
return loadImageUri(contentResolver, uri)
}
private fun showResult() {
result?.let {
showResult(this, it)
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
)
}
@@ -3,6 +3,7 @@ package de.markusfisch.android.binaryeye.activity
import android.content.Intent
import android.os.Bundle
import android.support.v7.app.AppCompatActivity
import de.markusfisch.android.binaryeye.app.setRestartCount
class SplashActivity : AppCompatActivity() {
override fun onCreate(state: Bundle?) {
@@ -12,6 +13,7 @@ class SplashActivity : AppCompatActivity() {
// because that would happen after the app is fully
// initialized what is too late
setRestartCount(intent)
startActivity(Intent(applicationContext, CameraActivity::class.java))
finish()
}
@@ -1,8 +1,5 @@
package de.markusfisch.android.binaryeye.adapter
import de.markusfisch.android.binaryeye.data.Database
import de.markusfisch.android.binaryeye.R
import android.content.Context
import android.database.Cursor
import android.view.LayoutInflater
@@ -10,22 +7,48 @@ import android.view.View
import android.view.ViewGroup
import android.widget.CursorAdapter
import android.widget.TextView
import de.markusfisch.android.binaryeye.R
import de.markusfisch.android.binaryeye.database.Database
class ScansAdapter(context: Context, cursor: Cursor) :
CursorAdapter(context, cursor, false) {
var selectedScanId = 0L
var selectedScanPosition = -1
private val idIndex = cursor.getColumnIndex(Database.SCANS_ID)
private val timeIndex = cursor.getColumnIndex(Database.SCANS_DATETIME)
private val nameIndex = cursor.getColumnIndex(Database.SCANS_NAME)
private val contentIndex = cursor.getColumnIndex(Database.SCANS_CONTENT)
private val formatIndex = cursor.getColumnIndex(Database.SCANS_FORMAT)
fun select(id: Long, position: Int) {
selectedScanId = id
selectedScanPosition = position
}
fun clearSelection() {
selectedScanId = 0L
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,
parent: ViewGroup
): View {
return LayoutInflater.from(parent.context).inflate(
R.layout.row_scan, parent, false
)
}
): View? = LayoutInflater.from(parent.context).inflate(
R.layout.row_scan, parent, false
)
override fun bindView(
view: View,
@@ -34,21 +57,36 @@ class ScansAdapter(context: Context, cursor: Cursor) :
) {
val holder = getViewHolder(view)
holder.timeView.text = cursor.getString(timeIndex)
holder.contentView.text = cursor.getString(contentIndex)
val name = cursor.getString(nameIndex)
val content = cursor.getString(contentIndex)
var icon = 0
holder.contentView.text = when {
name?.isNotEmpty() == true -> {
icon = R.drawable.ic_label
name
}
content?.isEmpty() == true -> context.getString(R.string.binary_data)
else -> content
}
holder.contentView.setCompoundDrawablesWithIntrinsicBounds(
icon, 0, 0, 0
)
holder.formatView.text = 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
}
}
private fun getViewHolder(view: View): ViewHolder {
var holder = view.tag as ViewHolder?
if (holder == null) {
holder = ViewHolder(
view.findViewById(R.id.time),
view.findViewById(R.id.content),
view.findViewById(R.id.format)
)
view.tag = holder
return view.tag as ViewHolder? ?: ViewHolder(
view.findViewById(R.id.time),
view.findViewById(R.id.content),
view.findViewById(R.id.format)
).also {
view.tag = it
}
return holder
}
private data class ViewHolder(
@@ -0,0 +1,23 @@
package de.markusfisch.android.binaryeye.app
import android.content.Context
import android.support.v7.app.AlertDialog
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
suspend inline fun <T : Any> alertDialog(
context: Context,
crossinline build: AlertDialog.Builder.(resume: (T?) -> Unit) -> Unit
): T? = withContext(Dispatchers.Main) {
suspendCoroutine<T?> { continuation ->
AlertDialog.Builder(context).apply {
setOnCancelListener {
continuation.resume(null)
}
build(continuation::resume)
show()
}
}
}
@@ -1,9 +1,9 @@
package de.markusfisch.android.binaryeye.app
import de.markusfisch.android.binaryeye.data.Database
import de.markusfisch.android.binaryeye.preference.Preferences
import android.app.Application
import android.support.v8.renderscript.RenderScript
import de.markusfisch.android.binaryeye.database.Database
import de.markusfisch.android.binaryeye.preference.Preferences
val db = Database()
val prefs = Preferences()
@@ -13,5 +13,9 @@ class BinaryEyeApp : Application() {
super.onCreate()
db.open(this)
prefs.init(this)
if (prefs.forceCompat) {
RenderScript.forceCompat()
}
}
}
@@ -0,0 +1,17 @@
package de.markusfisch.android.binaryeye.app
import android.content.ClipData
import android.content.Context
import android.os.Build
fun Context.copyToClipboard(text: String) {
val service = getSystemService(Context.CLIPBOARD_SERVICE)
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) {
@Suppress("DEPRECATION")
(service as android.text.ClipboardManager).text = text
} else {
(service as android.content.ClipboardManager).setPrimaryClip(
ClipData.newPlainText("plain text", text)
)
}
}
@@ -1,11 +1,9 @@
package de.markusfisch.android.binaryeye.app
import de.markusfisch.android.binaryeye.R
import android.annotation.SuppressLint
import android.support.v4.app.Fragment
import android.support.v4.app.FragmentManager
import android.support.v4.app.FragmentTransaction
import de.markusfisch.android.binaryeye.R
fun setFragment(fm: FragmentManager?, fragment: Fragment) {
fm?.let { getTransaction(fm, fragment).commit() }
@@ -19,6 +17,4 @@ fun addFragment(fm: FragmentManager?, fragment: Fragment) {
private fun getTransaction(
fm: FragmentManager,
fragment: Fragment
): FragmentTransaction {
return fm.beginTransaction().replace(R.id.content_frame, fragment)
}
) = fm.beginTransaction().replace(R.id.content_frame, fragment)
@@ -0,0 +1,14 @@
package de.markusfisch.android.binaryeye.app
import android.net.Uri
import android.os.Build
private val nonPrintable = "[\\x00-\\x08\\x0e-\\x1f]".toRegex()
fun hasNonPrintableCharacters(s: String) = nonPrintable.containsMatchIn(s)
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() = joinToString("") { "%02X".format(it) }
@@ -0,0 +1,86 @@
package de.markusfisch.android.binaryeye.app
import android.app.Activity
import android.content.ContentValues
import android.content.Context
import android.os.Build
import android.os.Environment
import android.provider.MediaStore
import android.support.annotation.MainThread
import android.widget.EditText
import de.markusfisch.android.binaryeye.R
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import java.io.OutputStream
fun addSuffixIfNotGiven(name: String, suffix: String): String {
val trimmed = name.trim()
return if (suffix.isNotEmpty() && !trimmed.endsWith(suffix)) {
"$trimmed$suffix"
} else {
trimmed
}
}
// Dialogs doesn't have any root layout, need to be inflated with null root
@Suppress("InflateParams")
@MainThread
suspend fun Activity.askForFileName(suffix: String = ""): String? {
val view = layoutInflater.inflate(R.layout.dialog_save_file, null)
val editText = view.findViewById<EditText>(R.id.file_name)
return alertDialog<String>(this as Context) { resume ->
setView(view)
setPositiveButton(android.R.string.ok) { _, _ ->
resume(editText.text.toString())
}
}?.let { name ->
addSuffixIfNotGiven(name, suffix)
}
}
fun Boolean.toSaveResult() = if (this) {
R.string.saved_in_downloads
} else {
R.string.error_saving_file
}
fun writeExternalFile(
context: Context,
fileName: String,
mimeType: String,
write: (outputStream: OutputStream) -> Unit
): Boolean = try {
openExternalOutputStream(context, fileName, mimeType).use { write(it) }
true
} catch (e: IOException) {
false
}
private fun openExternalOutputStream(
context: Context,
fileName: String,
mimeType: String
): OutputStream = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
@Suppress("DEPRECATION")
val file = File(
Environment.getExternalStoragePublicDirectory(
Environment.DIRECTORY_DOWNLOADS
),
fileName
)
if (file.exists()) {
throw IOException()
}
FileOutputStream(file)
} else {
val resolver = context.contentResolver
val uri = resolver.insert(
MediaStore.Downloads.EXTERNAL_CONTENT_URI,
ContentValues().apply {
put(MediaStore.Downloads.DISPLAY_NAME, fileName)
put(MediaStore.Downloads.MIME_TYPE, mimeType)
}
) ?: throw IOException()
resolver.openOutputStream(uri) ?: throw IOException()
}
@@ -0,0 +1,13 @@
package de.markusfisch.android.binaryeye.app
import android.content.Context
import android.view.View
import android.view.inputmethod.InputMethodManager
fun Context.hideSoftKeyboard(view: View) {
(getSystemService(
Context.INPUT_METHOD_SERVICE
) as InputMethodManager).hideSoftInputFromWindow(
view.windowToken, 0
)
}
@@ -0,0 +1,20 @@
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 locale = 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)
}
}
@@ -0,0 +1,55 @@
package de.markusfisch.android.binaryeye.app
import android.Manifest
import android.app.Activity
import android.content.pm.PackageManager
import android.support.v4.app.ActivityCompat
import android.support.v4.content.ContextCompat
var permissionGrantedCallback: (() -> Any)? = null
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,
PERMISSION_WRITE
)
}
const val PERMISSION_LOCATION = 3
fun hasLocationPermission(activity: Activity, callback: () -> Any): Boolean {
permissionGrantedCallback = callback
return hasPermission(
activity,
Manifest.permission.ACCESS_FINE_LOCATION,
PERMISSION_LOCATION
)
}
private fun hasPermission(
activity: Activity,
permission: String,
requestCode: Int
) = if (ContextCompat.checkSelfPermission(
activity,
permission
) != PackageManager.PERMISSION_GRANTED
) {
ActivityCompat.requestPermissions(
activity,
arrayOf(permission),
requestCode
)
false
} else {
true
}
@@ -0,0 +1,26 @@
package de.markusfisch.android.binaryeye.app
import android.app.Activity
import android.content.Intent
import de.markusfisch.android.binaryeye.activity.SplashActivity
private const val RESTART_COUNT = "restart_count"
private var restartCount = 0
fun setRestartCount(intent: Intent?) {
intent?.let {
restartCount = intent.getIntExtra(RESTART_COUNT, restartCount)
}
}
fun restartApp(activity: Activity? = null) {
if (activity != null && restartCount < 3) {
val intent = Intent(activity, SplashActivity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
intent.putExtra(RESTART_COUNT, ++restartCount)
activity.startActivity(intent)
activity.finish()
}
Runtime.getRuntime().exit(0)
}
@@ -0,0 +1,52 @@
package de.markusfisch.android.binaryeye.app
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.support.v4.content.FileProvider
import de.markusfisch.android.binaryeye.BuildConfig
import de.markusfisch.android.binaryeye.R
import de.markusfisch.android.binaryeye.widget.toast
import java.io.File
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)
}
fun shareUri(context: Context, uri: Uri, type: String) {
val intent = Intent(Intent.ACTION_SEND)
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)
}
}
fun shareFile(context: Context, file: File, type: String) {
getUriForFile(context, file)?.let {
shareUri(context, it, type)
}
}
fun getUriForFile(context: Context, file: File): Uri? {
return if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
Uri.fromFile(file)
} else {
FileProvider.getUriForFile(
context,
BuildConfig.APPLICATION_ID + ".provider",
file
)
}
}
@@ -1,49 +1,131 @@
package de.markusfisch.android.binaryeye.app
import de.markusfisch.android.binaryeye.R
import android.content.res.Resources
import android.content.Context
import android.content.ContextWrapper
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.Window
import android.view.WindowManager
import android.widget.AbsListView
import de.markusfisch.android.binaryeye.R
fun initSystemBars(activity: AppCompatActivity?) {
val view = activity?.findViewById(R.id.main_layout)
if (view != null &&
setSystemBarColor(
activity.window,
ContextCompat.getColor(
activity,
R.color.primary_dark_translucent
)
)
val systemBarListViewScrollListener = object : AbsListView.OnScrollListener {
override fun onScroll(
view: AbsListView,
firstVisibleItem: Int,
visibleItemCount: Int,
totalItemCount: Int
) {
view.setPadding(0, getStatusBarHeight(activity.resources), 0, 0)
// 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 = scrolled || totalItemCount > 0 && lastChildOutOfView(view)
colorSystemAndToolBars(view.context, scrolled, scrollable)
}
}
override fun onScrollStateChanged(view: AbsListView, scrollState: Int) {
}
}
fun setSystemBarColor(window: Window, color: Int): Boolean {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
return false
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)
}
window.statusBarColor = color
window.navigationBarColor = color
window.decorView.systemUiVisibility =
View.SYSTEM_UI_FLAG_LAYOUT_STABLE or
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
return true
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
}
}
fun getStatusBarHeight(res: Resources): Int {
return getIdentifierDimen(res, "status_bar_height")
private fun firstChildScrolled(listView: AbsListView): Boolean {
val child = listView.getChildAt(0)
return child != null && child.top < listView.paddingTop
}
private fun getIdentifierDimen(res: Resources, name: String): Int {
val id = res.getIdentifier(name, "dimen", "android")
return if (id > 0) res.getDimensionPixelSize(id) else 0
private fun lastChildOutOfView(listView: AbsListView): Boolean {
val child = listView.getChildAt(listView.lastVisiblePosition)
return child != null && child.bottom >= listView.height
}
fun initSystemBars(activity: AppCompatActivity) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
// 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
}
colorSystemAndToolBars(activity)
}
private var statusBarColorLocked = false
fun lockStatusBarColor() {
statusBarColorLocked = true
}
fun unlockStatusBarColor() {
statusBarColorLocked = false
}
private var translucentPrimaryColor = 0
private val actionBarBackground = ColorDrawable()
fun colorSystemAndToolBars(
context: Context,
scrolled: Boolean = false,
scrollable: Boolean = false
) {
if (translucentPrimaryColor == 0) {
translucentPrimaryColor = ContextCompat.getColor(
context,
R.color.primary_translucent
)
}
val topColor = if (scrolled) translucentPrimaryColor else 0
val activity = getAppCompatActivity(context) ?: return
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
val window = activity.window
if (!statusBarColorLocked) {
window.statusBarColor = topColor
}
window.navigationBarColor = if (scrolled || scrollable) {
translucentPrimaryColor
} else {
0
}
}
activity.supportActionBar?.setBackgroundDrawable(
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
// avoid allocation on Honeycomb and better
actionBarBackground.color = topColor
actionBarBackground
} else {
// ColorDrawable.setColor() doesn't exist pre Honeycomb
ColorDrawable(topColor)
}
)
}
private fun getAppCompatActivity(context: Context): AppCompatActivity? {
var ctx = context
while (ctx is ContextWrapper) {
if (ctx is AppCompatActivity) {
return ctx
}
ctx = ctx.baseContext ?: break
}
return null
}
@@ -0,0 +1,24 @@
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,92 +0,0 @@
package de.markusfisch.android.binaryeye.data
import android.content.ContentValues
import android.content.Context
import android.database.Cursor
import android.database.sqlite.SQLiteDatabase
import android.database.sqlite.SQLiteOpenHelper
import android.text.format.DateFormat
class Database {
private lateinit var db: SQLiteDatabase
fun open(context: Context) {
db = OpenHelper(context).writableDatabase
}
fun getScans(): Cursor? = db.rawQuery(
"""SELECT
$SCANS_ID,
$SCANS_DATETIME,
$SCANS_CONTENT,
$SCANS_FORMAT
FROM $SCANS
ORDER BY $SCANS_DATETIME DESC
""", null
)
fun getScan(id: Long): Cursor? = db.rawQuery(
"""SELECT
$SCANS_DATETIME,
$SCANS_CONTENT,
$SCANS_FORMAT
FROM $SCANS
WHERE $SCANS_ID = ?
""", arrayOf("$id")
)
fun insertScan(timestamp: Long, name: String, code: String): Long {
val cv = ContentValues()
cv.put(
SCANS_DATETIME, DateFormat.format(
"yyyy-MM-dd HH:mm:ss",
timestamp
).toString()
)
cv.put(SCANS_CONTENT, name)
cv.put(SCANS_FORMAT, code)
return db.insert(SCANS, null, cv)
}
fun removeScan(id: Long) {
db.delete(SCANS, "$SCANS_ID = ?", arrayOf("$id"))
}
fun removeScans() {
db.delete(SCANS, null, null)
}
private class OpenHelper(context: Context) :
SQLiteOpenHelper(context, "history.db", null, 1) {
override fun onCreate(db: SQLiteDatabase) {
createScans(db)
}
override fun onUpgrade(
db: SQLiteDatabase,
oldVersion: Int,
newVersion: Int
) {
}
}
companion object {
const val SCANS = "scans"
const val SCANS_ID = "_id"
const val SCANS_DATETIME = "_datetime"
const val SCANS_CONTENT = "content"
const val SCANS_FORMAT = "format"
private fun createScans(db: SQLiteDatabase) {
db.execSQL("DROP TABLE IF EXISTS $SCANS")
db.execSQL(
"""CREATE TABLE $SCANS (
$SCANS_ID INTEGER PRIMARY KEY AUTOINCREMENT,
$SCANS_DATETIME DATETIME NOT NULL,
$SCANS_CONTENT TEXT NOT NULL,
$SCANS_FORMAT TEXT NOT NULL
)"""
)
}
}
}
@@ -0,0 +1,94 @@
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 java.io.ByteArrayOutputStream
import java.io.OutputStream
fun exportCsv(
context: Context,
name: String,
cursor: Cursor,
delimiter: String
) = writeExternalFile(context, name, "text/csv") { outputStream ->
exportCsv(outputStream, cursor, delimiter)
}
fun exportCsv(cursor: Cursor, delimiter: String): String? {
val outputStream = ByteArrayOutputStream()
exportCsv(outputStream, cursor, delimiter)
return outputStream.toString()
}
private fun exportCsv(
outputStream: OutputStream,
cursor: Cursor,
delimiter: String
) {
if (!cursor.moveToFirst()) {
return
}
val columns = arrayOf(
Database.SCANS_DATETIME,
Database.SCANS_FORMAT,
Database.SCANS_CONTENT,
Database.SCANS_ERROR_CORRECTION_LEVEL,
Database.SCANS_ISSUE_NUMBER,
Database.SCANS_ORIENTATION,
Database.SCANS_OTHER_META_DATA,
Database.SCANS_PDF417_EXTRA_METADATA,
Database.SCANS_POSSIBLE_COUNTRY,
Database.SCANS_SUGGESTED_PRICE,
Database.SCANS_UPC_EAN_EXTENSION
)
val indices = columns.map {
cursor.getColumnIndex(it)
}
val contentIndex = cursor.getColumnIndex(Database.SCANS_CONTENT)
val rawIndex = cursor.getColumnIndex(Database.SCANS_RAW)
outputStream.write(
columns.joinToString(
delimiter,
postfix = "\n"
).toByteArray()
)
do {
var deviation: Pair<Int, String>? = null
if (cursor.getString(contentIndex)?.isEmpty() == true) {
deviation = Pair(
contentIndex,
cursor.getBlob(rawIndex).toHexString()
)
}
outputStream.write(
cursor.toCsvRecord(indices, delimiter, deviation)
)
} while (cursor.moveToNext())
}
private fun Cursor.toCsvRecord(
indices: List<Int>,
delimiter: String,
deviation: Pair<Int, String>?
): ByteArray {
val sb = StringBuilder()
indices.forEach {
val value = if (deviation?.first == it) {
deviation.second
} else {
this.getString(it)
}
sb.append(value?.quoteAndEscape() ?: "")
sb.append(delimiter)
}
sb.append("\n")
return sb.toString().toByteArray()
}
private fun String.quoteAndEscape() = "\"${
this
.replace("\n", " ")
.replace("\"", "\"\"")
}\""
@@ -0,0 +1,285 @@
package de.markusfisch.android.binaryeye.database
import android.content.ContentValues
import android.content.Context
import android.database.Cursor
import android.database.sqlite.SQLiteDatabase
import android.database.sqlite.SQLiteOpenHelper
import de.markusfisch.android.binaryeye.app.hasNonPrintableCharacters
import de.markusfisch.android.binaryeye.app.prefs
class Database {
private lateinit var db: SQLiteDatabase
fun open(context: Context) {
db = OpenHelper(context).writableDatabase
}
fun getScans(query: String? = null): Cursor? = db.rawQuery(
"""SELECT
$SCANS_ID,
$SCANS_DATETIME,
$SCANS_NAME,
$SCANS_CONTENT,
$SCANS_FORMAT
FROM $SCANS
${getWhereClause(query)}
ORDER BY $SCANS_DATETIME DESC
""", getWhereArguments(query)
)
fun getScansDetailed(query: String? = null): Cursor? = db.rawQuery(
"""SELECT
$SCANS_ID,
$SCANS_DATETIME,
$SCANS_NAME,
$SCANS_CONTENT,
$SCANS_RAW,
$SCANS_FORMAT,
$SCANS_ERROR_CORRECTION_LEVEL,
$SCANS_ISSUE_NUMBER,
$SCANS_ORIENTATION,
$SCANS_OTHER_META_DATA,
$SCANS_PDF417_EXTRA_METADATA,
$SCANS_POSSIBLE_COUNTRY,
$SCANS_SUGGESTED_PRICE,
$SCANS_UPC_EAN_EXTENSION
FROM $SCANS
${getWhereClause(query)}
ORDER BY $SCANS_DATETIME DESC
""", getWhereArguments(query)
)
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) {
val instr = "%$query%"
arrayOf(instr, instr)
} else {
null
}
fun getScan(id: Long): Scan? = db.rawQuery(
"""SELECT
$SCANS_ID,
$SCANS_DATETIME,
$SCANS_NAME,
$SCANS_CONTENT,
$SCANS_RAW,
$SCANS_FORMAT,
$SCANS_ERROR_CORRECTION_LEVEL,
$SCANS_ISSUE_NUMBER,
$SCANS_ORIENTATION,
$SCANS_OTHER_META_DATA,
$SCANS_PDF417_EXTRA_METADATA,
$SCANS_POSSIBLE_COUNTRY,
$SCANS_SUGGESTED_PRICE,
$SCANS_UPC_EAN_EXTENSION
FROM $SCANS
WHERE $SCANS_ID = ?
""", arrayOf("$id")
)?.use {
if (it.moveToFirst()) {
Scan(
it.getString(SCANS_CONTENT),
it.getBlob(SCANS_RAW),
it.getString(SCANS_FORMAT),
it.getString(SCANS_ERROR_CORRECTION_LEVEL),
it.getString(SCANS_ISSUE_NUMBER),
it.getString(SCANS_ORIENTATION),
it.getString(SCANS_OTHER_META_DATA),
it.getString(SCANS_PDF417_EXTRA_METADATA),
it.getString(SCANS_POSSIBLE_COUNTRY),
it.getString(SCANS_SUGGESTED_PRICE),
it.getString(SCANS_UPC_EAN_EXTENSION),
it.getString(SCANS_DATETIME),
it.getLong(SCANS_ID)
)
} else {
null
}
}
fun insertScan(scan: Scan): Long {
val cv = ContentValues()
cv.put(SCANS_DATETIME, scan.timestamp)
val isRaw = hasNonPrintableCharacters(scan.content)
if (isRaw) {
cv.put(SCANS_CONTENT, "")
cv.put(SCANS_RAW, scan.raw ?: scan.content.toByteArray())
} else {
cv.put(SCANS_CONTENT, scan.content)
}
cv.put(SCANS_FORMAT, scan.format)
scan.errorCorrectionLevel?.let {
cv.put(SCANS_ERROR_CORRECTION_LEVEL, it)
}
scan.issueNumber?.let { cv.put(SCANS_ISSUE_NUMBER, it) }
scan.orientation?.let { cv.put(SCANS_ORIENTATION, it) }
scan.otherMetaData?.let { cv.put(SCANS_OTHER_META_DATA, it) }
scan.pdf417ExtraMetaData?.let { cv.put(SCANS_PDF417_EXTRA_METADATA, it) }
scan.possibleCountry?.let { cv.put(SCANS_POSSIBLE_COUNTRY, it) }
scan.suggestedPrice?.let { cv.put(SCANS_SUGGESTED_PRICE, it) }
scan.upcEanExtension?.let { cv.put(SCANS_UPC_EAN_EXTENSION, it) }
if (prefs.ignoreConsecutiveDuplicates) {
val id = getIdOfLastScan(
cv.get(SCANS_CONTENT) as String,
if (isRaw) cv.get(SCANS_RAW) as ByteArray else null,
scan.format
)
if (id > 0L) {
return id
}
}
return db.insert(SCANS, null, cv)
}
private fun getIdOfLastScan(
content: String,
raw: ByteArray?,
format: String
): Long {
return db.rawQuery(
"""SELECT
$SCANS_ID,
$SCANS_CONTENT,
$SCANS_RAW,
$SCANS_FORMAT
FROM $SCANS
ORDER BY $SCANS_ID DESC
LIMIT 1
""", null
)?.use {
if (it.count > 0 &&
it.moveToFirst() &&
it.getString(it.getColumnIndex(SCANS_CONTENT)) == content &&
(raw == null || it.getBlob(it.getColumnIndex(SCANS_RAW))
?.contentEquals(raw) == true) &&
it.getString(it.getColumnIndex(SCANS_FORMAT)) == format
) {
it.getLong(it.getColumnIndex(SCANS_ID))
} else {
0L
}
} ?: 0L
}
fun removeScan(id: Long) {
db.delete(SCANS, "$SCANS_ID = ?", arrayOf("$id"))
}
fun removeScans(query: String? = null) {
db.delete(SCANS, getWhereClause(query, ""), getWhereArguments(query))
}
fun renameScan(id: Long, name: String) {
val cv = ContentValues()
cv.put(SCANS_NAME, name)
db.update(SCANS, cv, "$SCANS_ID = ?", arrayOf("$id"))
}
private class OpenHelper(context: Context) :
SQLiteOpenHelper(context, FILE_NAME, null, 4) {
override fun onCreate(db: SQLiteDatabase) {
createScans(db)
}
override fun onUpgrade(
db: SQLiteDatabase,
oldVersion: Int,
newVersion: Int
) {
if (oldVersion < 2) {
addRawColumn(db)
}
if (oldVersion < 3) {
addMetaDataColumns(db)
}
if (oldVersion < 4) {
addNameColumn(db)
}
}
}
companion object {
const val FILE_NAME = "history.db"
const val SCANS = "scans"
const val SCANS_ID = "_id"
const val SCANS_DATETIME = "_datetime"
const val SCANS_NAME = "name"
const val SCANS_CONTENT = "content"
const val SCANS_RAW = "raw"
const val SCANS_FORMAT = "format"
const val SCANS_ERROR_CORRECTION_LEVEL = "error_correction_level"
const val SCANS_ISSUE_NUMBER = "issue_number"
const val SCANS_ORIENTATION = "orientation"
const val SCANS_OTHER_META_DATA = "other_meta_data"
const val SCANS_PDF417_EXTRA_METADATA = "pdf417_extra_metadata"
const val SCANS_POSSIBLE_COUNTRY = "possible_country"
const val SCANS_SUGGESTED_PRICE = "suggested_price"
const val SCANS_UPC_EAN_EXTENSION = "upc_ean_extension"
private fun createScans(db: SQLiteDatabase) {
db.execSQL("DROP TABLE IF EXISTS $SCANS")
db.execSQL(
"""CREATE TABLE $SCANS (
$SCANS_ID INTEGER PRIMARY KEY AUTOINCREMENT,
$SCANS_DATETIME DATETIME NOT NULL,
$SCANS_NAME TEXT,
$SCANS_CONTENT TEXT NOT NULL,
$SCANS_RAW BLOB,
$SCANS_FORMAT TEXT NOT NULL,
$SCANS_ERROR_CORRECTION_LEVEL TEXT,
$SCANS_ISSUE_NUMBER INT,
$SCANS_ORIENTATION INT,
$SCANS_OTHER_META_DATA TEXT,
$SCANS_PDF417_EXTRA_METADATA TEXT,
$SCANS_POSSIBLE_COUNTRY TEXT,
$SCANS_SUGGESTED_PRICE TEXT,
$SCANS_UPC_EAN_EXTENSION TEXT
)"""
)
}
private fun addRawColumn(db: SQLiteDatabase) {
db.execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_RAW BLOB")
}
private fun addMetaDataColumns(db: SQLiteDatabase) {
db.execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_ERROR_CORRECTION_LEVEL TEXT")
db.execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_ISSUE_NUMBER INT")
db.execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_ORIENTATION INT")
db.execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_OTHER_META_DATA TEXT")
db.execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_PDF417_EXTRA_METADATA TEXT")
db.execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_POSSIBLE_COUNTRY TEXT")
db.execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_SUGGESTED_PRICE TEXT")
db.execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_UPC_EAN_EXTENSION TEXT")
}
private fun addNameColumn(db: SQLiteDatabase) {
db.execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_NAME TEXT")
}
}
}
private fun Cursor.getString(name: String) = this.getString(
this.getColumnIndex(name)
)
private fun Cursor.getBlob(name: String) = this.getBlob(
this.getColumnIndex(name)
)
private fun Cursor.getLong(name: String) = this.getLong(
this.getColumnIndex(name)
)
@@ -0,0 +1,24 @@
package de.markusfisch.android.binaryeye.database
import android.app.Activity
import android.os.Environment
import de.markusfisch.android.binaryeye.app.writeExternalFile
import java.io.File
import java.io.FileInputStream
fun exportDatabase(activity: Activity, fileName: String): Boolean {
val dbFile = File(
Environment.getDataDirectory(),
"//data//${activity.packageName}//databases//${Database.FILE_NAME}"
)
if (!dbFile.exists()) {
return false
}
return writeExternalFile(
activity,
fileName,
"application/vnd.sqlite3"
) {
FileInputStream(dbFile).copyTo(it)
}
}
@@ -0,0 +1,75 @@
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 org.json.JSONArray
import org.json.JSONException
import org.json.JSONObject
fun exportJson(
context: Context,
name: String,
cursor: Cursor
) = writeExternalFile(context, name, "application/json") { outputStream ->
exportJson(cursor)?.let {
outputStream.write(it.toByteArray())
}
}
fun exportJson(cursor: Cursor): String? {
if (!cursor.moveToFirst()) {
return null
}
val columns = arrayOf(
Database.SCANS_DATETIME,
Database.SCANS_FORMAT,
Database.SCANS_CONTENT,
Database.SCANS_ERROR_CORRECTION_LEVEL,
Database.SCANS_ISSUE_NUMBER,
Database.SCANS_ORIENTATION,
Database.SCANS_OTHER_META_DATA,
Database.SCANS_PDF417_EXTRA_METADATA,
Database.SCANS_POSSIBLE_COUNTRY,
Database.SCANS_SUGGESTED_PRICE,
Database.SCANS_UPC_EAN_EXTENSION
)
val indices = columns.map {
Pair(cursor.getColumnIndex(it), it)
}
val contentIndex = cursor.getColumnIndex(Database.SCANS_CONTENT)
val rawIndex = cursor.getColumnIndex(Database.SCANS_RAW)
val root = JSONArray()
do {
var deviation: Pair<Int, String>? = null
if (cursor.getString(contentIndex)?.isEmpty() == true) {
deviation = Pair(
contentIndex,
cursor.getBlob(rawIndex).toHexString()
)
}
root.put(cursor.toJsonObject(indices, deviation))
} while (cursor.moveToNext())
return root.toString()
}
private fun Cursor.toJsonObject(
indices: List<Pair<Int, String>>,
deviation: Pair<Int, String>?
): JSONObject {
val obj = JSONObject()
return try {
indices.forEach {
val value = if (deviation?.first == it.first) {
deviation.second
} else {
this.getString(it.first)
}
obj.put(it.second, value ?: "")
}
obj
} catch (e: JSONException) {
obj
}
}
@@ -0,0 +1,169 @@
package de.markusfisch.android.binaryeye.database
import android.os.Parcel
import android.os.Parcelable
import android.text.format.DateFormat
import com.google.zxing.Result
import com.google.zxing.ResultMetadataType
data class Scan(
val content: String,
val raw: ByteArray?,
val format: String,
val errorCorrectionLevel: String?,
val issueNumber: String?,
val orientation: String?,
val otherMetaData: String?,
val pdf417ExtraMetaData: String?,
val possibleCountry: String?,
val suggestedPrice: String?,
val upcEanExtension: String?,
val timestamp: String = getDateTime(),
var id: Long = 0L
) : Parcelable {
constructor(result: Result) : this(
result.text,
getRawBytes(result),
result.barcodeFormat.toString(),
result.getMetaString(ResultMetadataType.ERROR_CORRECTION_LEVEL),
result.getMetaString(ResultMetadataType.ISSUE_NUMBER),
result.getMetaString(ResultMetadataType.ORIENTATION),
result.getMetaString(ResultMetadataType.OTHER),
result.getMetaString(ResultMetadataType.PDF417_EXTRA_METADATA),
result.getMetaString(ResultMetadataType.POSSIBLE_COUNTRY),
result.getMetaString(ResultMetadataType.SUGGESTED_PRICE),
result.getMetaString(ResultMetadataType.UPC_EAN_EXTENSION)
)
// Needs to be overwritten manually, as ByteArray is an array and
// this isn't handled well by Kotlin
override fun equals(other: Any?): Boolean {
if (this === other) {
return true
}
if (javaClass != other?.javaClass) {
return false
}
other as Scan
return id == other.id &&
timestamp == other.timestamp &&
content == other.content &&
((raw == null && other.raw == null) ||
(raw != null && other.raw != null && raw.contentEquals(other.raw))) &&
format == other.format &&
errorCorrectionLevel == other.errorCorrectionLevel &&
issueNumber == other.issueNumber &&
orientation == other.orientation &&
otherMetaData == other.otherMetaData &&
pdf417ExtraMetaData == other.pdf417ExtraMetaData &&
possibleCountry == other.possibleCountry &&
suggestedPrice == other.suggestedPrice &&
upcEanExtension == other.upcEanExtension
}
// Needs to be overwritten manually, as ByteArray is an array and
// this isn't handled well by Kotlin
override fun hashCode(): Int {
var result = id.hashCode()
result = 31 * result + timestamp.hashCode()
result = 31 * result + content.hashCode()
result = 31 * result + (raw?.contentHashCode() ?: 0)
result = 31 * result + format.hashCode()
result = 31 * result + (errorCorrectionLevel?.hashCode() ?: 0)
result = 31 * result + (issueNumber?.hashCode() ?: 0)
result = 31 * result + (orientation?.hashCode() ?: 0)
result = 31 * result + (otherMetaData?.hashCode() ?: 0)
result = 31 * result + (pdf417ExtraMetaData?.hashCode() ?: 0)
result = 31 * result + (possibleCountry?.hashCode() ?: 0)
result = 31 * result + (suggestedPrice?.hashCode() ?: 0)
result = 31 * result + (upcEanExtension?.hashCode() ?: 0)
return result
}
private constructor(parcel: Parcel) : this(
content = parcel.readString() ?: "",
raw = readByteArray(parcel),
format = parcel.readString() ?: "",
errorCorrectionLevel = parcel.readString(),
issueNumber = parcel.readString(),
orientation = parcel.readString(),
otherMetaData = parcel.readString(),
pdf417ExtraMetaData = parcel.readString(),
possibleCountry = parcel.readString(),
suggestedPrice = parcel.readString(),
upcEanExtension = parcel.readString(),
timestamp = parcel.readString() ?: "",
id = parcel.readLong()
)
override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeString(content)
writeByteArray(parcel, raw)
parcel.writeString(format)
parcel.writeString(errorCorrectionLevel)
parcel.writeString(issueNumber)
parcel.writeString(orientation)
parcel.writeString(otherMetaData)
parcel.writeString(pdf417ExtraMetaData)
parcel.writeString(possibleCountry)
parcel.writeString(suggestedPrice)
parcel.writeString(upcEanExtension)
parcel.writeString(timestamp)
parcel.writeLong(id)
}
override fun describeContents() = 0
companion object {
@JvmField
val CREATOR = object : Parcelable.Creator<Scan> {
override fun createFromParcel(parcel: Parcel) = Scan(parcel)
override fun newArray(size: Int) = arrayOfNulls<Scan>(size)
}
}
}
private fun getDateTime(time: Long = System.currentTimeMillis()) = DateFormat.format(
"yyyy-MM-dd HH:mm:ss",
time
).toString()
private fun getRawBytes(result: Result): ByteArray? {
val metadata = result.resultMetadata ?: return null
val segments = metadata[ResultMetadataType.BYTE_SEGMENTS] ?: return null
var bytes = ByteArray(0)
@Suppress("UNCHECKED_CAST")
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:"
return if (bytes.size >= result.text.length) bytes else null
}
private fun Result.getMetaString(
key: ResultMetadataType
): String? = this.resultMetadata?.let {
it[key]?.toString()
}
private fun writeByteArray(parcel: Parcel, array: ByteArray?) {
val size = array?.size ?: 0
parcel.writeInt(size)
if (size > 0) {
parcel.writeByteArray(array)
}
}
private fun readByteArray(parcel: Parcel): ByteArray? {
val size = parcel.readInt()
return if (size > 0) {
val array = ByteArray(size)
parcel.readByteArray(array)
array
} else {
null
}
}
@@ -1,39 +1,57 @@
package de.markusfisch.android.binaryeye.fragment
import com.google.zxing.BarcodeFormat
import de.markusfisch.android.binaryeye.zxing.Zxing
import de.markusfisch.android.binaryeye.BuildConfig
import de.markusfisch.android.binaryeye.R
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.Bundle
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.support.v4.content.FileProvider
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.Toast
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
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.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.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
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import java.io.OutputStream
import java.util.*
class BarcodeFragment : Fragment() {
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
override fun onCreate(state: Bundle?) {
super.onCreate(state)
setHasOptionsMenu(true)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
state: Bundle?
): View? {
activity.setTitle(R.string.view_barcode)
val ac = activity ?: return null
ac.setTitle(R.string.view_barcode)
val view = inflater.inflate(
R.layout.fragment_barcode,
@@ -41,96 +59,200 @@ class BarcodeFragment : Fragment() {
false
)
val args = arguments
args?.let {
val content = args.getString(CONTENT)
val format = args.getSerializable(FORMAT) as BarcodeFormat?
if (content != null && format != null) {
val size = args.getInt(SIZE)
val bitmap: Bitmap?
try {
bitmap = Zxing.encodeAsBitmap(
content,
format,
size,
size
)
} catch (e: Exception) {
Toast.makeText(
activity,
e.message,
Toast.LENGTH_SHORT
).show()
fragmentManager.popBackStack()
return null
}
view.findViewById<ImageView>(R.id.barcode).setImageBitmap(bitmap)
view.findViewById<View>(R.id.share).setOnClickListener {
bitmap?.let {
share(bitmap)
}
}
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 {
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()) {
message = getString(R.string.error_encoding_barcode)
}
message?.let {
ac.toast(message)
}
fragmentManager.popBackStack()
return null
}
this.content = content
this.format = format
val imageView = view.findViewById<ConfinedScalingImageView>(
R.id.barcode
)
imageView.setImageBitmap(barcodeBitmap)
imageView.post {
// make sure to invoke this after ScalingImageView.onLayout()
imageView.minWidth /= 2f
}
view.findViewById<View>(R.id.share).setOnClickListener {
pickFileType(context, R.string.share_as) {
shareAs(it)
}
}
(view.findViewById(R.id.inset_layout) as View).setPaddingFromWindowInsets()
imageView.doOnApplyWindowInsets { v, insets ->
(v as ConfinedScalingImageView).insets.set(insets)
}
return view
}
private fun share(bitmap: Bitmap) {
GlobalScope.launch {
val file = saveBitmap(bitmap)
GlobalScope.launch(Main) {
file?.let {
val intent = Intent(Intent.ACTION_SEND)
intent.putExtra(Intent.EXTRA_STREAM, getUriForFile(file))
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
intent.type = "image/png"
startActivity(intent)
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.fragment_barcode, menu)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
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)
}
}
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(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 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) { _, _ ->
}
.show()
}
private fun saveAs(
fileName: String,
mimeType: String,
write: (outputStream: OutputStream) -> Unit
) {
val ac = activity ?: return
GlobalScope.launch {
val message = writeExternalFile(ac, fileName, mimeType, write).toSaveResult()
GlobalScope.launch(Main) {
ac.toast(message)
}
}
}
private fun saveBitmap(bitmap: Bitmap): File? {
return try {
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(Dispatchers.IO) {
val file = File(
context.externalCacheDir,
"shared_barcode.png"
)
val fos = FileOutputStream(file)
bitmap.compress(Bitmap.CompressFormat.PNG, 90, fos)
fos.close()
file
} catch (e: IOException) {
null
}
}
private fun getUriForFile(file: File): Uri {
return if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
Uri.fromFile(file)
} else {
FileProvider.getUriForFile(
context,
BuildConfig.APPLICATION_ID + ".provider",
file
)
val success = try {
FileOutputStream(file).use {
bitmap.saveAsPng(it)
}
true
} catch (e: IOException) {
false
}
GlobalScope.launch(Main) {
if (success) {
shareFile(context, file, "image/png")
} else {
activity?.toast(R.string.error_saving_file)
}
}
}
}
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
@@ -138,3 +260,11 @@ class BarcodeFragment : Fragment() {
}
}
}
private fun Bitmap.saveAsPng(outputStream: OutputStream, quality: Int = 90) {
this.compress(Bitmap.CompressFormat.PNG, quality, outputStream)
}
private val fileNameCharacters = "[^A-Za-z0-9]".toRegex()
private fun encodeFileName(name: String): String =
fileNameCharacters.replace(name, "_").take(16).trim('_').toLowerCase(Locale.getDefault())
@@ -1,34 +1,46 @@
package de.markusfisch.android.binaryeye.fragment
import com.google.zxing.BarcodeFormat
import de.markusfisch.android.binaryeye.app.addFragment
import de.markusfisch.android.binaryeye.R
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.support.design.widget.FloatingActionButton
import android.support.v4.app.Fragment
import android.text.ClipboardManager
import android.text.Editable
import android.text.TextWatcher
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.view.*
import android.widget.EditText
import android.widget.TableLayout
import android.widget.TableRow
import android.widget.TextView
import android.widget.Toast
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.app.*
import de.markusfisch.android.binaryeye.database.Scan
import de.markusfisch.android.binaryeye.view.setPaddingFromWindowInsets
import de.markusfisch.android.binaryeye.widget.toast
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
class DecodeFragment : Fragment() {
private lateinit var contentView: EditText
private lateinit var formatView: TextView
private lateinit var metaView: TableLayout
private lateinit var hexView: TextView
private lateinit var format: BarcodeFormat
private lateinit var format: String
private lateinit var fab: FloatingActionButton
private val parentJob = Job()
private val scope: CoroutineScope = CoroutineScope(Dispatchers.Main + parentJob)
private val content: String
get() = contentView.text.toString()
private var closeAutomatically = false
private var action = ActionRegistry.DEFAULT_ACTION
private var isBinary = false
private var id = 0L
override fun onCreate(state: Bundle?) {
super.onCreate(state)
@@ -40,7 +52,7 @@ class DecodeFragment : Fragment() {
container: ViewGroup?,
state: Bundle?
): View {
activity.setTitle(R.string.content)
activity?.setTitle(R.string.content)
val view = inflater.inflate(
R.layout.fragment_decode,
@@ -48,174 +60,304 @@ class DecodeFragment : Fragment() {
false
)
val content = arguments?.getString(CONTENT) ?: ""
format = arguments?.getSerializable(FORMAT) as BarcodeFormat? ?: BarcodeFormat.QR_CODE
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()
format = scan.format
contentView = view.findViewById(R.id.content)
contentView.setText(content)
contentView.addTextChangedListener(object : TextWatcher {
override fun afterTextChanged(s: Editable?) {
updateFormatAndHex(contentView.text.toString())
fab = view.findViewById(R.id.open)
if (!isBinary) {
contentView.setText(inputContent)
contentView.addTextChangedListener(object : TextWatcher {
override fun afterTextChanged(s: Editable?) {
updateViewsAndAction(content.toByteArray())
}
override fun beforeTextChanged(
s: CharSequence?,
start: Int,
count: Int,
after: Int
) {
}
override fun onTextChanged(
s: CharSequence?,
start: Int,
before: Int,
count: Int
) {
}
})
fab.setOnClickListener {
executeAction(content.toByteArray())
}
if (prefs.openImmediately) {
executeAction(content.toByteArray())
}
} else {
contentView.setText(R.string.binary_data)
contentView.isEnabled = false
fab.setImageResource(R.drawable.ic_action_save)
fab.setOnClickListener {
askForFileNameAndSave(raw)
}
override fun beforeTextChanged(
s: CharSequence?,
start: Int,
count: Int,
after: Int
) {}
override fun onTextChanged(
s: CharSequence?,
start: Int,
before: Int,
count: Int
) {}
})
formatView = view.findViewById(R.id.format)
hexView = view.findViewById(R.id.hex)
view.findViewById<View>(R.id.share).setOnClickListener {
share(getContent())
}
updateFormatAndHex(content)
formatView = view.findViewById(R.id.format)
metaView = view.findViewById(R.id.meta)
hexView = view.findViewById(R.id.hex)
updateViewsAndAction(raw)
if (prefs.showMetaData) {
fillMetaView(metaView, scan)
}
(view.findViewById(R.id.inset_layout) as View).setPaddingFromWindowInsets()
(view.findViewById(R.id.scroll_view) as View).setPaddingFromWindowInsets()
return view
}
private fun updateFormatAndHex(content: String) {
override fun onDestroyView() {
parentJob.cancel()
super.onDestroyView()
}
private fun updateViewsAndAction(bytes: ByteArray) {
val prevAction = action
if (!prevAction.canExecuteOn(bytes)) {
action = ActionRegistry.getAction(bytes)
}
formatView.text = resources.getQuantityString(
R.plurals.barcode_info,
content.length,
format.toString(),
content.length
bytes.size,
format,
bytes.size
)
hexView.text = hexDump(content, 33)
hexView.text = if (prefs.showHexDump) hexDump(bytes) else ""
if (prevAction !== action) {
fab.setImageResource(action.iconResId)
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
fab.setOnLongClickListener { v ->
v.context.toast(action.titleResId)
true
}
} else {
fab.tooltipText = getString(action.titleResId)
}
}
}
private fun fillMetaView(tableLayout: TableLayout, scan: Scan) {
val ctx = tableLayout.context
val spaceBetween = (16f * ctx.resources.displayMetrics.density).toInt()
var hasMeta = false
sortedMapOf(
R.string.error_correction_level to scan.errorCorrectionLevel,
R.string.issue_number to scan.issueNumber,
R.string.orientation to scan.orientation,
R.string.other_meta_data to scan.otherMetaData,
R.string.pdf417_extra_metadata to scan.pdf417ExtraMetaData,
R.string.possible_country to scan.possibleCountry,
R.string.suggested_price to scan.suggestedPrice,
R.string.upc_ean_extension to scan.upcEanExtension
).forEach { item ->
item.value?.let {
val tr = TableRow(ctx)
val keyView = TextView(ctx)
keyView.setText(item.key)
val valueView = TextView(ctx)
valueView.setPadding(spaceBetween, 0, 0, 0)
valueView.text = it
tr.addView(keyView)
tr.addView(valueView)
tableLayout.addView(tr)
hasMeta = true
}
}
if (hasMeta) {
tableLayout.setPadding(0, 0, 0, spaceBetween)
}
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.fragment_decode, menu)
if (isBinary) {
menu.findItem(R.id.copy_to_clipboard).isVisible = false
menu.findItem(R.id.create).isVisible = false
}
if (id > 0L) {
menu.findItem(R.id.remove).isVisible = true
}
if (action is WifiAction) {
menu.findItem(R.id.copy_password).isVisible = true
}
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.copy_to_clipboard -> {
copyToClipboard(getContent())
R.id.copy_password -> {
copyPasswordToClipboard()
maybeBackOrFinish()
true
}
R.id.open_url -> {
openUrl(getContent())
R.id.copy_to_clipboard -> {
copyToClipboard(content)
maybeBackOrFinish()
true
}
R.id.share -> {
context?.also {
shareText(it, content)
maybeBackOrFinish()
}
true
}
R.id.create -> {
addFragment(
fragmentManager,
EncodeFragment.newInstance(getContent(), format)
EncodeFragment.newInstance(content, format)
)
true
}
R.id.remove -> {
db.removeScan(id)
backOrFinish()
true
}
else -> super.onOptionsItemSelected(item)
}
}
private fun getContent(): String {
return contentView.text.toString()
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 ?: return
val cm = activity.getSystemService(
Context.CLIPBOARD_SERVICE
) as ClipboardManager
cm.text = text
Toast.makeText(
activity,
R.string.put_into_clipboard,
Toast.LENGTH_SHORT
).show()
activity?.apply {
copyToClipboard(text)
toast(R.string.copied_to_clipboard)
}
}
private fun openUrl(url: String) {
if (activity == null || url.isEmpty()) {
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) { executeAction(content) }
) {
return
}
scope.launch {
action.execute(ac, content)
maybeBackOrFinish()
}
}
}
private fun askForFileNameAndSave(raw: ByteArray) {
val ac = activity ?: return
if (!hasWritePermission(ac) { askForFileNameAndSave(raw) }) {
return
}
var uri = Uri.parse(url)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
uri = uri.normalizeScheme()
scope.launch(Dispatchers.Main) {
val name = ac.askForFileName() ?: return@launch
val message = writeExternalFile(
ac,
name,
"application/octet-stream"
) {
it.write(raw)
}.toSaveResult()
ac.toast(message)
}
val intent = Intent(Intent.ACTION_VIEW, uri)
if (intent.resolveActivity(activity.packageManager) != null) {
startActivity(intent)
} else {
Toast.makeText(
activity,
R.string.cannot_resolve_action,
Toast.LENGTH_SHORT
).show()
}
}
private fun share(text: String) {
val intent = Intent(Intent.ACTION_SEND)
intent.putExtra(Intent.EXTRA_TEXT, text)
intent.type = "text/plain"
startActivity(intent)
}
private fun hexDump(content: String, charsPerLine: Int): String {
if (charsPerLine < 4 || content.isEmpty()) {
return ""
}
val dump = StringBuilder()
val hex = StringBuilder()
val ascii = StringBuilder()
val itemsPerLine = (charsPerLine - 1) / 4
val len = content.length
var i = 0
while (true) {
val chr = content[i]
val ord = chr.toInt()
hex.append(String.format("%02X ", ord))
ascii.append(if (ord > 31) chr else " ")
++i
val posInLine = i % itemsPerLine
val atEnd = i >= len
var atLineEnd = posInLine == 0
if (atEnd && !atLineEnd) {
for (j in posInLine until itemsPerLine) {
hex.append(" ")
}
atLineEnd = true
}
if (atLineEnd) {
dump.append(hex.toString())
dump.append(" ")
dump.append(ascii.toString())
dump.append("\n")
hex.setLength(0)
ascii.setLength(0)
}
if (atEnd) {
break
}
}
return dump.toString()
}
companion object {
private const val CONTENT = "content"
private const val FORMAT = "format"
private const val SCAN = "scan"
fun newInstance(content: String, format: BarcodeFormat): Fragment {
fun newInstance(scan: Scan): Fragment {
val args = Bundle()
args.putString(CONTENT, content)
args.putSerializable(FORMAT, format)
args.putParcelable(SCAN, scan)
val fragment = DecodeFragment()
fragment.arguments = args
return fragment
}
}
}
private fun hexDump(bytes: ByteArray, charsPerLine: Int = 33): String {
if (charsPerLine < 4 || bytes.isEmpty()) {
return ""
}
val dump = StringBuilder()
val hex = StringBuilder()
val ascii = StringBuilder()
val itemsPerLine = (charsPerLine - 1) / 4
val len = bytes.size
var i = 0
while (true) {
val ord = bytes[i]
hex.append(String.format("%02X ", ord))
ascii.append(if (ord > 31) ord.toChar() else " ")
++i
val posInLine = i % itemsPerLine
val atEnd = i >= len
var atLineEnd = posInLine == 0
if (atEnd && !atLineEnd) {
for (j in posInLine until itemsPerLine) {
hex.append(" ")
}
atLineEnd = true
}
if (atLineEnd) {
dump.append(hex.toString())
dump.append(" ")
dump.append(ascii.toString())
dump.append("\n")
hex.setLength(0)
ascii.setLength(0)
}
if (atEnd) {
break
}
}
return dump.toString()
}
@@ -1,28 +1,29 @@
package de.markusfisch.android.binaryeye.fragment
import com.google.zxing.BarcodeFormat
import de.markusfisch.android.binaryeye.app.addFragment
import de.markusfisch.android.binaryeye.R
import android.content.Context
import android.os.Bundle
import android.support.v4.app.Fragment
import android.view.inputmethod.InputMethodManager
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ArrayAdapter
import android.widget.EditText
import android.widget.SeekBar
import android.widget.Spinner
import android.widget.TextView
import android.widget.Toast
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.app.addFragment
import de.markusfisch.android.binaryeye.app.hideSoftKeyboard
import de.markusfisch.android.binaryeye.app.prefs
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
private val writers = arrayListOf(
BarcodeFormat.AZTEC,
BarcodeFormat.CODABAR,
@@ -41,8 +42,9 @@ class EncodeFragment : Fragment() {
inflater: LayoutInflater,
container: ViewGroup?,
state: Bundle?
): View {
activity.setTitle(R.string.compose_barcode)
): View? {
val ac = activity ?: return null
ac.setTitle(R.string.compose_barcode)
val view = inflater.inflate(
R.layout.fragment_encode,
@@ -51,53 +53,104 @@ class EncodeFragment : Fragment() {
)
formatView = view.findViewById(R.id.format)
formatView.adapter = ArrayAdapter<String>(
activity,
android.R.layout.simple_list_item_1,
val formatAdapter = ArrayAdapter(
ac,
android.R.layout.simple_spinner_item,
writers.map { 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)
initSizeBar()
val contentView = view.findViewById<EditText>(R.id.content)
contentView = view.findViewById(R.id.content)
val args = arguments
args?.let {
contentView.setText(args.getString(CONTENT))
formatView.setSelection(
writers.indexOf(
args.getSerializable(FORMAT) as BarcodeFormat?
)
)
args?.also {
contentView.setText(it.getString(CONTENT))
}
view.findViewById<View>(R.id.encode).setOnClickListener { v ->
val format = writers[formatView.selectedItemPosition]
val size = getSize(sizeBarView.progress)
val content = contentView.text.toString()
if (content.isEmpty()) {
Toast.makeText(
v.context,
R.string.error_no_content,
Toast.LENGTH_SHORT
).show()
} else {
hideSoftKeyboard(contentView)
addFragment(
fragmentManager, BarcodeFragment.newInstance(
content,
format,
size
)
)
val barcodeFormat = args?.getString(FORMAT)
if (barcodeFormat != null) {
formatView.setSelection(
writers.indexOf(resolveFormat(barcodeFormat))
)
} else if (state == null) {
formatView.post {
formatView.setSelection(prefs.indexOfLastSelectedFormat)
}
}
view.findViewById<View>(R.id.encode).setOnClickListener {
encode()
}
(view.findViewById(R.id.inset_layout) as View).setPaddingFromWindowInsets()
(view.findViewById(R.id.scroll_view) as View).setPaddingFromWindowInsets()
return view
}
override fun onPause() {
super.onPause()
prefs.indexOfLastSelectedFormat = formatView.selectedItemPosition
}
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()) {
Toast.makeText(
context,
R.string.error_no_content,
Toast.LENGTH_SHORT
).show()
} else {
activity?.hideSoftKeyboard(contentView)
addFragment(
fragmentManager,
BarcodeFragment.newInstance(content, format, size, hints)
)
}
}
private fun initSizeBar() {
updateSize(sizeBarView.progress)
sizeBarView.setOnSeekBarChangeListener(
@@ -123,29 +176,26 @@ class EncodeFragment : Fragment() {
private fun getSize(power: Int) = 128 * (power + 1)
private fun hideSoftKeyboard(view: View) {
val im = activity?.getSystemService(
Context.INPUT_METHOD_SERVICE
) as InputMethodManager?
im?.let {
im.hideSoftInputFromWindow(view.windowToken, 0)
}
}
companion object {
private const val CONTENT = "content"
private const val FORMAT = "format"
fun newInstance(
content: String,
format: BarcodeFormat = BarcodeFormat.AZTEC
format: String? = null
): Fragment {
val args = Bundle()
args.putString(CONTENT, content)
args.putSerializable(FORMAT, format)
format?.let { args.putString(FORMAT, it) }
val fragment = EncodeFragment()
fragment.arguments = args
return fragment
}
}
}
private fun resolveFormat(name: String): BarcodeFormat = try {
BarcodeFormat.valueOf(name)
} catch (_: IllegalArgumentException) {
BarcodeFormat.QR_CODE
}
@@ -1,38 +1,130 @@
package de.markusfisch.android.binaryeye.fragment
import com.google.zxing.BarcodeFormat
import de.markusfisch.android.binaryeye.adapter.ScansAdapter
import de.markusfisch.android.binaryeye.app.db
import de.markusfisch.android.binaryeye.app.addFragment
import de.markusfisch.android.binaryeye.app.prefs
import de.markusfisch.android.binaryeye.data.Database
import de.markusfisch.android.binaryeye.R
import android.annotation.SuppressLint
import android.app.AlertDialog
import android.app.SearchManager
import android.content.Context
import android.database.Cursor
import android.os.Build
import android.os.Bundle
import android.os.Parcelable
import android.support.v4.app.ActivityCompat
import android.support.v4.app.Fragment
import android.support.v4.content.ContextCompat
import android.support.v7.app.AppCompatActivity
import android.support.v7.view.ActionMode
import android.support.v7.widget.SearchView
import android.support.v7.widget.SwitchCompat
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.*
import android.widget.EditText
import android.widget.ListView
import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import de.markusfisch.android.binaryeye.R
import de.markusfisch.android.binaryeye.adapter.ScansAdapter
import de.markusfisch.android.binaryeye.app.*
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.view.setPaddingFromWindowInsets
import de.markusfisch.android.binaryeye.view.useVisibility
import de.markusfisch.android.binaryeye.widget.toast
import kotlinx.coroutines.*
class HistoryFragment : Fragment() {
private lateinit var useHistorySwitch: SwitchCompat
private lateinit var listView: ListView
private lateinit var fab: View
private lateinit var progressView: View
private val parentJob = Job()
private val scope = CoroutineScope(Dispatchers.IO + parentJob)
private val actionModeCallback = object : ActionMode.Callback {
override fun onCreateActionMode(
mode: ActionMode,
menu: Menu
): Boolean {
mode.menuInflater.inflate(
R.menu.fragment_history_edit,
menu
)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
lockStatusBarColor()
val ac = activity ?: return false
ac.window.statusBarColor = ContextCompat.getColor(
ac,
R.color.accent_dark
)
}
return true
}
override fun onPrepareActionMode(
mode: ActionMode,
menu: Menu
): Boolean {
return false
}
override fun onActionItemClicked(
mode: ActionMode,
item: MenuItem
): 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(
ac,
it.selectedScanId,
getScanName(it.selectedScanPosition)
)
}
closeActionMode()
true
}
R.id.remove_scan -> {
scansAdapter?.let {
askToRemoveScan(ac, it.selectedScanId)
}
closeActionMode()
true
}
else -> false
}
}
override fun onDestroyActionMode(mode: ActionMode) {
closeActionMode()
}
}
private var scansAdapter: ScansAdapter? = null
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)
setHasOptionsMenu(true)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
state: Bundle?
): View {
activity.setTitle(R.string.history)
): View? {
val ac = activity ?: return null
ac.setTitle(R.string.history)
val view = inflater.inflate(
R.layout.fragment_history,
@@ -40,35 +132,100 @@ class HistoryFragment : Fragment() {
false
)
val useHistorySwitch = view.findViewById(R.id.use_history) as SwitchCompat
useHistorySwitch = view.findViewById(
R.id.use_history
) as SwitchCompat
initHistorySwitch(useHistorySwitch)
listView = view.findViewById(R.id.scans)
listView.emptyView = useHistorySwitch
listView.setOnItemClickListener { _, _, _, id ->
showScan(id)
}
listView.setOnItemLongClickListener { _, v, _, id ->
askToRemoveScan(v.context, id)
listView.setOnItemLongClickListener { _, v, position, id ->
v.isSelected = true
scansAdapter?.select(id, position)
if (actionMode == null && ac is AppCompatActivity) {
actionMode = ac.delegate.startSupportActionMode(
actionModeCallback
)
}
true
}
listView.setOnScrollListener(systemBarListViewScrollListener)
fab = view.findViewById(R.id.clear)
fab = view.findViewById(R.id.share)
fab.setOnClickListener { v ->
askToRemoveAllScans(v.context)
pickListSeparatorAndShare(v.context)
}
progressView = view.findViewById(R.id.progress_view)
(view.findViewById(R.id.inset_layout) as View).setPaddingFromWindowInsets()
listView.setPaddingFromWindowInsets()
update()
return view
}
override fun onDestroy() {
super.onDestroy()
(listView.adapter as ScansAdapter?)?.changeCursor(null)
scansAdapter?.changeCursor(null)
parentJob.cancel()
}
override fun onResume() {
super.onResume()
update(context)
override fun onPause() {
super.onPause()
listViewState = listView.onSaveInstanceState()
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.fragment_history, menu)
initSearchView(menu.findItem(R.id.search))
menu.setGroupVisible(R.id.scans_available, scansAdapter?.count != 0)
clearListMenuItem = menu.findItem(R.id.clear)
exportHistoryMenuItem = menu.findItem(R.id.export_history)
}
private fun initSearchView(item: MenuItem?) {
item ?: return
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) {
item.isVisible = false
} else {
val ac = activity ?: return
val searchView = item.actionView as SearchView
val searchManager = ac.getSystemService(
Context.SEARCH_SERVICE
) as SearchManager
searchView.setSearchableInfo(
searchManager.getSearchableInfo(ac.componentName)
)
searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String): Boolean {
update(query)
return false
}
override fun onQueryTextChange(query: String): Boolean {
update(query)
return false
}
})
}
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.clear -> {
askToRemoveScans(context)
true
}
R.id.export_history -> {
askToExportToFile(context)
true
}
else -> super.onOptionsItemSelected(item)
}
}
private fun initHistorySwitch(switchView: SwitchCompat) {
@@ -80,46 +237,90 @@ class HistoryFragment : Fragment() {
}
}
private fun update(context: Context) {
GlobalScope.launch {
val cursor = db.getScans()
GlobalScope.launch(Main) {
fab.visibility = if (cursor != null && cursor.count > 0) {
private fun updateAndClearFilter() {
filter = null
update()
}
private fun update(query: String? = null) {
query?.let { filter = it }
scope.launch {
val cursor = db.getScans(filter)
withContext(Dispatchers.Main) {
val ac = activity ?: return@withContext
val hasScans = cursor != null && cursor.count > 0
if (filter == null) {
if (!hasScans) {
listView.emptyView = useHistorySwitch
}
ActivityCompat.invalidateOptionsMenu(ac)
}
enableMenuItems(hasScans)
fab.visibility = if (hasScans) {
View.VISIBLE
} else {
View.GONE
}
cursor?.let {
if (listView.adapter == null) {
listView.adapter = ScansAdapter(context, cursor)
} else {
val adapter = listView.adapter as ScansAdapter
adapter.changeCursor(cursor)
adapter.notifyDataSetChanged()
cursor?.let { cursor ->
// close previous cursor
scansAdapter?.also { it.changeCursor(null) }
scansAdapter = ScansAdapter(ac, cursor)
listView.adapter = scansAdapter
listViewState?.also {
listView.onRestoreInstanceState(it)
}
}
}
}
}
private fun showScan(id: Long) {
val cursor = db.getScan(id)
if (cursor != null && cursor.moveToFirst()) {
val contentIndex = cursor.getColumnIndex(Database.SCANS_CONTENT)
val formatIndex = cursor.getColumnIndex(Database.SCANS_FORMAT)
try {
addFragment(
fragmentManager,
DecodeFragment.newInstance(
cursor.getString(contentIndex),
BarcodeFormat.valueOf(cursor.getString(formatIndex))
)
)
} catch (e: IllegalArgumentException) {
// shouldn't ever happen
}
private fun enableMenuItems(enabled: Boolean) {
clearListMenuItem?.isEnabled = enabled
exportHistoryMenuItem?.isEnabled = enabled
}
private fun closeActionMode() {
unlockStatusBarColor()
scansAdapter?.clearSelection()
actionMode?.finish()
actionMode = null
scansAdapter?.notifyDataSetChanged()
}
private fun showScan(id: Long) = db.getScan(id)?.also { scan ->
closeActionMode()
try {
addFragment(
fragmentManager,
DecodeFragment.newInstance(scan)
)
} catch (e: IllegalArgumentException) {
// ignore, can never happen
}
cursor?.close()
}
private fun getScanName(position: Int): String? {
val cursor = scansAdapter?.getItem(position) as Cursor?
return cursor?.getString(cursor.getColumnIndex(Database.SCANS_NAME))
}
// dialogs don't have a parent layout
@SuppressLint("InflateParams")
private fun askForName(context: Context, id: Long, text: String?) {
val view = LayoutInflater.from(context).inflate(
R.layout.dialog_enter_name, null
)
val nameView = view.findViewById<EditText>(R.id.name)
nameView.setText(text)
AlertDialog.Builder(context)
.setView(view)
.setPositiveButton(android.R.string.ok) { _, _ ->
val name = nameView.text.toString()
db.renameScan(id, name)
update()
}
.setNegativeButton(android.R.string.cancel) { _, _ -> }
.show()
}
private fun askToRemoveScan(context: Context, id: Long) {
@@ -127,22 +328,119 @@ class HistoryFragment : Fragment() {
.setMessage(R.string.really_remove_scan)
.setPositiveButton(android.R.string.ok) { _, _ ->
db.removeScan(id)
update(context)
if (scansAdapter?.count == 1) {
updateAndClearFilter()
} else {
update()
}
}
.setNegativeButton(android.R.string.cancel) { _, _ ->
}
.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()
update(context)
db.removeScans(filter)
updateAndClearFilter()
}
.setNegativeButton(android.R.string.cancel) { _, _ ->
}
.show()
}
private fun askToExportToFile(context: Context) {
scope.launch {
val ac = activity ?: return@launch
progressView.useVisibility {
if (!hasWritePermission(ac) { askToExportToFile(context) }) {
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])
}
} ?: 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)
}
}
}
}
private fun pickListSeparatorAndShare(context: Context) {
val separators = context.resources.getStringArray(
R.array.list_separators_values
)
AlertDialog.Builder(context)
.setTitle(R.string.pick_list_separator)
.setItems(R.array.list_separators_names) { _, which ->
shareScans(separators[which])
}
.show()
}
private fun shareScans(format: String) = scope.launch {
progressView.useVisibility {
var text: String? = null
db.getScansDetailed(filter)?.use { cursor ->
val details = format.split(":")
text = when (details[0]) {
"text" -> exportText(cursor, details[1])
"csv" -> exportCsv(cursor, details[1])
else -> exportJson(cursor)
}
}
text?.let {
withContext(Dispatchers.Main) {
shareText(context, it)
}
}
}
}
}
private fun exportText(cursor: Cursor, separator: String): String {
val sb = StringBuilder()
val contentIndex = cursor.getColumnIndex(Database.SCANS_CONTENT)
if (cursor.moveToFirst()) {
do {
val content = cursor.getString(contentIndex)
if (content?.isNotEmpty() == true) {
sb.append(content)
sb.append(separator)
}
} while (cursor.moveToNext())
}
return sb.toString()
}
@@ -0,0 +1,103 @@
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.os.Build
import android.os.Bundle
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.activity.SplashActivity
import de.markusfisch.android.binaryeye.app.prefs
import de.markusfisch.android.binaryeye.app.systemBarRecyclerViewScrollListener
import de.markusfisch.android.binaryeye.preference.UrlPreference
import de.markusfisch.android.binaryeye.view.setPaddingFromWindowInsets
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)
}
}
}
override fun onCreatePreferences(state: Bundle?, rootKey: String?) {
addPreferencesFromResource(R.xml.preferences)
activity?.setTitle(R.string.preferences)
}
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
}
@@ -0,0 +1,87 @@
package de.markusfisch.android.binaryeye.graphics
import android.content.ContentResolver
import android.graphics.Bitmap
import android.graphics.BitmapFactory
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 {
val options = BitmapFactory.Options()
cr.openInputStream(uri)?.use {
options.inJustDecodeBounds = true
BitmapFactory.decodeStream(it, null, options)
options.inSampleSize = calculateInSampleSize(
options.outWidth,
options.outHeight
)
options.inJustDecodeBounds = false
}
cr.openInputStream(uri)?.use {
BitmapFactory.decodeStream(it, null, options)
}
} catch (e: IOException) {
null
}
private fun calculateInSampleSize(
width: Int,
height: Int,
reqWidth: Int = 1024,
reqHeight: Int = 1024
): Int {
var inSampleSize = 1
if (height > reqHeight || width > reqWidth) {
val halfHeight = height / 2
val halfWidth = width / 2
while (
halfHeight / inSampleSize >= reqHeight ||
halfWidth / inSampleSize >= reqWidth
) {
inSampleSize *= 2
}
}
return inSampleSize
}
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,
x,
y,
min(w - x, (rect.width() * w).roundToInt()),
min(h - y, (rect.height() * h).roundToInt())
)
} catch (e: OutOfMemoryError) {
null
} catch (e: IllegalArgumentException) {
null
}
private fun erect(bitmap: Bitmap, rotation: Float): Bitmap = if (
rotation % 360f != 0f
) {
val matrix = Matrix()
matrix.setRotate(rotation)
Bitmap.createBitmap(
bitmap,
0,
0,
bitmap.width,
bitmap.height,
matrix,
true
)
} else {
bitmap
}
@@ -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)
}
@@ -0,0 +1,29 @@
package de.markusfisch.android.binaryeye.graphics
import android.content.Context
import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.Point
import android.support.v4.content.ContextCompat
import de.markusfisch.android.binaryeye.R
class 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.dot)
paint.style = Paint.Style.FILL
}
fun draw(canvas: Canvas, points: List<Point>) {
for (point in points) {
canvas.drawCircle(
point.x.toFloat(),
point.y.toFloat(),
radius,
paint
)
}
}
}
@@ -0,0 +1,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
}
@@ -0,0 +1,85 @@
package de.markusfisch.android.binaryeye.graphics
import android.graphics.Point
import android.graphics.Rect
import com.google.zxing.Result
import com.google.zxing.ResultPoint
import kotlin.math.roundToInt
fun mapResult(
frameWidth: Int,
frameHeight: Int,
frameOrientation: Int,
viewRect: Rect,
result: Result
): List<Point> = frameToView(
frameWidth,
frameHeight,
frameOrientation,
viewRect
).map(
result.resultPoints
)
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 frameWidth: Int,
val frameHeight: Int,
val frameOrientation: Int,
val viewWidth: Float,
val viewHeight: Float,
val offsetX: Int,
val offsetY: Int
) {
fun map(resultPoint: ResultPoint): Point {
val point = Point(
resultPoint.x.roundToInt(),
resultPoint.y.roundToInt()
)
rotate(point)
val w: Int
val h: Int
if (isPortrait(frameOrientation)) {
w = frameHeight
h = frameWidth
} else {
w = frameWidth
h = frameHeight
}
val ratioX = viewWidth / w.toFloat()
val ratioY = viewHeight / h.toFloat()
point.set(
(point.x * ratioX).roundToInt() + offsetX,
(point.y * ratioY).roundToInt() + offsetY
)
return point
}
fun map(points: Array<ResultPoint?>): List<Point> =
// because ZXing apparently returns null in this array sometimes
points.filterNotNull().map { map(it) }
private fun rotate(point: Point) = when (frameOrientation) {
90 -> point.set(frameHeight - point.y, point.x)
180 -> point.set(frameWidth - point.x, frameHeight - point.y)
270 -> point.set(point.y, frameWidth - point.x)
else -> {
}
}
}
@@ -0,0 +1,121 @@
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
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?)
@@ -7,9 +7,115 @@ import android.preference.PreferenceManager
class Preferences {
lateinit var preferences: SharedPreferences
var cropHandleX = -1
set(value) {
apply(CROP_HANDLE_X, value)
field = value
}
var cropHandleY = -1
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) {
// immediately save this setting because it's only ever read
// before the camera is opened
commit(TRY_HARDER, value)
field = value
}
var vibrate = true
set(value) {
apply(VIBRATE, value)
field = value
}
var useHistory = false
set(value) {
setBoolean(USE_HISTORY, value)
apply(USE_HISTORY, value)
field = value
}
var ignoreConsecutiveDuplicates = true
set(value) {
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)
field = value
}
var showMetaData = true
set(value) {
apply(SHOW_META_DATA, value)
field = value
}
var showHexDump = true
set(value) {
apply(SHOW_HEX_DUMP, value)
field = value
}
var closeAutomatically = false
set(value) {
apply(CLOSE_AUTOMATICALLY, 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)
field = value
}
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
commit(FORCE_COMPAT, value)
field = value
}
@@ -19,16 +125,95 @@ 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)
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(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
)
forceCompat = preferences.getBoolean(FORCE_COMPAT, forceCompat)
}
private fun setBoolean(label: String, value: Boolean) {
val editor = preferences.edit()
editor.putBoolean(label, value)
editor.apply()
private fun put(label: String, value: Boolean) =
preferences.edit().putBoolean(label, value)
private fun commit(label: String, value: Boolean) {
put(label, value).commit()
}
private fun apply(label: String, value: Boolean) {
put(label, value).apply()
}
private fun apply(label: String, value: String) {
preferences.edit().putString(label, value).apply()
}
private fun apply(label: String, value: Int) {
preferences.edit().putInt(label, value).apply()
}
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 VIBRATE = "vibrate"
const val USE_HISTORY = "use_history"
const val IGNORE_CONSECUTIVE_DUPLICATES = "ignore_consecutive_duplicates"
const val OPEN_IMMEDIATELY = "open_immediately"
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 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,29 +1,28 @@
package de.markusfisch.android.binaryeye.rs
import de.markusfisch.android.binaryeye.renderscript.ScriptC_rotator
import android.graphics.Rect
import android.support.v8.renderscript.*
import kotlin.math.max
import kotlin.math.roundToInt
import android.content.Context
import android.support.v8.renderscript.Allocation
import android.support.v8.renderscript.Element
import android.support.v8.renderscript.RenderScript
import android.support.v8.renderscript.ScriptIntrinsicResize
import android.support.v8.renderscript.Type
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
@@ -42,15 +41,42 @@ class Preprocessor(
Allocation.USAGE_SCRIPT
)
val f = .75f
var w = Math.round(width * f)
var h = Math.round(height * f)
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,
@@ -58,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() {
@@ -87,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()
@@ -96,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,35 @@
package de.markusfisch.android.binaryeye.view
import android.graphics.Rect
import android.view.View
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
suspend inline fun View.useVisibility(
whileExecuting: Int = View.VISIBLE,
otherwise: Int = View.GONE,
crossinline block: suspend () -> Unit
) {
if (visibility == whileExecuting) {
return
}
withContext(Dispatchers.Main) {
visibility = whileExecuting
}
try {
block()
} finally {
withContext(Dispatchers.Main) {
visibility = otherwise
}
}
}
fun View.setPadding(rect: Rect) {
this.setPadding(
rect.left,
rect.top,
rect.right,
rect.bottom
)
}
@@ -0,0 +1,61 @@
package de.markusfisch.android.binaryeye.view
import android.graphics.Rect
import android.os.Build
import android.support.annotation.RequiresApi
import android.support.v4.view.ViewCompat
import android.support.v4.view.WindowInsetsCompat
import android.support.v7.widget.Toolbar
import android.view.View
private var toolbarHeight = 0
fun recordToolbarHeight(toolbar: Toolbar) {
toolbarHeight = toolbar.layoutParams.height
}
fun View.setPaddingFromWindowInsets() {
doOnApplyWindowInsets { v, insets -> v.setPadding(insets) }
}
// A slight variation of the idea from Google's Chris Banes to avoid
// adding the toolbar height for every view that needs to be inset.
// For the original post see here:
// https://medium.com/androiddevelopers/windowinsets-listeners-to-layouts-8f9ccc8fa4d1
fun View.doOnApplyWindowInsets(f: (View, Rect) -> Unit) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
f(this, insetsWithToolbar())
} else {
ViewCompat.setOnApplyWindowInsetsListener(this) { v, insets ->
f(v, insetsWithToolbar(insets))
insets
}
// 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
requestApplyInsetsWhenAttached()
}
}
private fun insetsWithToolbar(insets: WindowInsetsCompat? = null) = Rect(
insets?.systemWindowInsetLeft ?: 0,
(insets?.systemWindowInsetTop ?: 0) + toolbarHeight,
insets?.systemWindowInsetRight ?: 0,
insets?.systemWindowInsetBottom ?: 0
)
@RequiresApi(Build.VERSION_CODES.KITKAT)
private fun View.requestApplyInsetsWhenAttached() {
if (isAttachedToWindow) {
ViewCompat.requestApplyInsets(this)
} else {
addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener {
override fun onViewAttachedToWindow(v: View) {
v.removeOnAttachStateChangeListener(this)
ViewCompat.requestApplyInsets(v)
}
override fun onViewDetachedFromWindow(v: View) = Unit
})
}
}
@@ -0,0 +1,41 @@
package de.markusfisch.android.binaryeye.widget
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.scalingimageview.widget.ScalingImageView
open class ConfinedScalingImageView : ScalingImageView {
val insets = Rect()
constructor(context: Context, attrs: AttributeSet, defStyle: Int) :
super(context, attrs, defStyle)
constructor(context: Context, attrs: AttributeSet) :
this(context, attrs, 0)
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
colorSystemAndToolBars(context, !bounds.contains(mappedRect))
}
override fun layoutImage(
changed: Boolean,
left: Int,
top: Int,
right: Int,
bottom: Int
) {
if (changed) {
setBounds(
(left + insets.left).toFloat(),
(top + insets.top).toFloat(),
(right - insets.right).toFloat(),
(bottom - insets.bottom).toFloat()
)
}
centerRemap()
}
}
@@ -0,0 +1,45 @@
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
class ConfinedScrollView : ScrollView {
private var scrollable = false
constructor(context: Context, attrs: AttributeSet, defStyle: Int) :
super(context, attrs, defStyle)
constructor(context: Context, attrs: AttributeSet) :
this(context, attrs, 0)
override fun onLayout(
changed: Boolean,
left: Int,
top: Int,
right: Int,
bottom: Int
) {
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
post {
getChildAt(0)?.also { child ->
scrollable = height < child.height + paddingTop + paddingBottom
colorSystemAndToolBars(
context,
scrollY > 0,
scrollable
)
}
}
}
}
override fun onScrollChanged(x: Int, y: Int, oldx: Int, oldy: Int) {
super.onScrollChanged(x, y, oldx, oldy)
colorSystemAndToolBars(context, y > 0, scrollable)
}
}
@@ -0,0 +1,33 @@
package de.markusfisch.android.binaryeye.widget
import android.content.Context
import android.graphics.Canvas
import android.graphics.Rect
import android.graphics.RectF
import android.util.AttributeSet
import kotlin.math.roundToInt
class CropImageView(context: Context, attr: AttributeSet) :
ConfinedScalingImageView(context, attr) {
var onScan: (() -> Unit)? = null
private val lastMappedRect = RectF()
private val onScanRunnable = Runnable { onScan?.invoke() }
fun getBoundsRect() = Rect(
bounds.left.roundToInt(),
bounds.top.roundToInt(),
bounds.right.roundToInt(),
bounds.bottom.roundToInt()
)
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
val mr = mappedRect ?: return
if (mr != lastMappedRect) {
removeCallbacks(onScanRunnable)
postDelayed(onScanRunnable, 300)
lastMappedRect.set(mr)
}
}
}
@@ -0,0 +1,381 @@
package de.markusfisch.android.binaryeye.widget
import android.annotation.SuppressLint
import android.content.Context
import android.graphics.*
import android.os.Build
import android.os.Parcel
import android.os.Parcelable
import android.util.AttributeSet
import android.view.MotionEvent
import android.view.View
import de.markusfisch.android.binaryeye.R
import de.markusfisch.android.binaryeye.app.prefs
import de.markusfisch.android.binaryeye.graphics.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 {
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)
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()
removeCallbacks(invalidateRunnable)
postDelayed(invalidateRunnable, 500)
}
override fun onSaveInstanceState(): Parcelable? {
if (!handleActive) {
return super.onSaveInstanceState()
}
return SavedState(super.onSaveInstanceState()).apply {
savedHandlePos.set(getCropHandlePos())
savedOrientation = currentOrientation
}
}
override fun onRestoreInstanceState(state: Parcelable) {
super.onRestoreInstanceState(
if (state is SavedState) {
setCropHandlePos(
state.savedHandlePos.x,
state.savedHandlePos.y,
state.savedOrientation
)
state.superState
} else {
state
}
)
}
@SuppressLint("ClickableViewAccessibility")
override fun onTouchEvent(event: MotionEvent?): Boolean {
event ?: return super.onTouchEvent(event)
val x = event.x.roundToInt()
val y = event.y.roundToInt()
return when (event.actionMasked) {
MotionEvent.ACTION_DOWN -> {
if (prefs.showCropHandle) {
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()
handleActive = true
} 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()
)
}
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 (handleActive) {
updateClipRect()
} else {
reset()
}
}
override fun onDraw(canvas: Canvas) {
if (handleActive) {
drawClip(canvas)
}
marks?.let {
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()
}
}
@@ -0,0 +1,16 @@
package de.markusfisch.android.binaryeye.widget
import android.content.Context
import android.widget.Toast
fun Context.toast(message: Int) = Toast.makeText(
applicationContext,
message,
Toast.LENGTH_LONG
).show()
fun Context.toast(message: String) = Toast.makeText(
applicationContext,
message,
Toast.LENGTH_LONG
).show()
@@ -1,33 +1,22 @@
package de.markusfisch.android.binaryeye.zxing
import com.google.zxing.BarcodeFormat
import com.google.zxing.BinaryBitmap
import com.google.zxing.DecodeHintType
import com.google.zxing.LuminanceSource
import com.google.zxing.MultiFormatReader
import com.google.zxing.MultiFormatWriter
import com.google.zxing.PlanarYUVLuminanceSource
import com.google.zxing.RGBLuminanceSource
import com.google.zxing.ReaderException
import com.google.zxing.Result
import com.google.zxing.common.HybridBinarizer
import android.graphics.Bitmap
import com.google.zxing.*
import com.google.zxing.common.BitMatrix
import com.google.zxing.common.HybridBinarizer
import java.util.*
import java.util.Arrays
import java.util.EnumMap
import java.util.EnumSet
class Zxing {
private val multiFormatReader: MultiFormatReader = MultiFormatReader()
class Zxing(possibleResultPoint: ResultPointCallback? = null) {
private val hints = EnumMap<DecodeHintType, Any>(DecodeHintType::class.java)
private val multiFormatReader = MultiFormatReader()
init {
val decodeFormats = EnumSet.noneOf<BarcodeFormat>(
val decodeFormats = EnumSet.noneOf(
BarcodeFormat::class.java
)
decodeFormats.addAll(
EnumSet.copyOf(
Arrays.asList(
listOf(
BarcodeFormat.AZTEC,
BarcodeFormat.CODABAR,
BarcodeFormat.CODE_39,
@@ -48,11 +37,18 @@ class Zxing {
)
)
)
val hints = EnumMap<DecodeHintType, Any>(DecodeHintType::class.java)
hints[DecodeHintType.POSSIBLE_FORMATS] = decodeFormats
possibleResultPoint?.let {
hints[DecodeHintType.NEED_RESULT_POINT_CALLBACK] = it
}
}
multiFormatReader.setHints(hints)
fun updateHints(tryHarder: Boolean) {
if (tryHarder) {
hints[DecodeHintType.TRY_HARDER] = java.lang.Boolean.TRUE
} else {
hints.remove(DecodeHintType.TRY_HARDER)
}
}
fun decode(
@@ -71,6 +67,39 @@ class Zxing {
height,
false
)
return decodeLuminanceSource(source, invert)
}
fun decodePositiveNegative(bitmap: Bitmap): Result? =
decode(bitmap, false) ?: decode(bitmap, true)
private fun decode(bitmap: Bitmap, invert: Boolean = false): Result? {
val pixels = IntArray(bitmap.width * bitmap.height)
return decode(pixels, bitmap, invert)
}
private fun decode(
pixels: IntArray,
bitmap: Bitmap,
invert: Boolean = false
): Result? {
val width = bitmap.width
val height = bitmap.height
if (bitmap.config != Bitmap.Config.ARGB_8888) {
bitmap.copy(Bitmap.Config.ARGB_8888, true)
} else {
bitmap
}.getPixels(pixels, 0, width, 0, 0, width, height)
return decodeLuminanceSource(
RGBLuminanceSource(width, height, pixels),
invert
)
}
private fun decodeLuminanceSource(
source: LuminanceSource,
invert: Boolean
): Result? {
return decodeLuminanceSource(
if (invert) {
source.invert()
@@ -80,64 +109,108 @@ class Zxing {
)
}
fun decode(bitmap: Bitmap): Result? {
val width = bitmap.width
val height = bitmap.height
val pixels = IntArray(width * height)
if (bitmap.config != Bitmap.Config.ARGB_8888) {
bitmap.copy(Bitmap.Config.ARGB_8888, true)
} else {
bitmap
}.getPixels(pixels, 0, width, 0, 0, width, height)
val source = RGBLuminanceSource(width, height, pixels)
val result = decodeLuminanceSource(source)
if (result != null) {
return result
}
return decodeLuminanceSource(source.invert())
}
private fun decodeLuminanceSource(source: LuminanceSource): Result? {
val bitmap = BinaryBitmap(HybridBinarizer(source))
return try {
multiFormatReader.decodeWithState(bitmap)
multiFormatReader.decode(bitmap, hints)
} catch (e: ReaderException) {
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 result = MultiFormatWriter().encode(
text, format,
width, height, null
)
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, width, 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,12 @@
<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="M54,55.787m-5.719,0a5.719,5.719 0,1 1,11.438 0a5.719,5.719 0,1 1,-11.438 0"
android:fillColor="#ffffff"/>
<path
android:pathData="M48.639,37.916L45.368,41.491L39.703,41.491C37.738,41.491 36.129,43.099 36.129,45.065L36.129,66.509C36.129,68.475 37.738,70.084 39.703,70.084L68.297,70.084C70.262,70.084 71.871,68.475 71.871,66.509L71.871,45.065C71.871,43.099 70.262,41.491 68.297,41.491L62.632,41.491L59.361,37.916ZM54,64.722C49.068,64.722 45.065,60.719 45.065,55.787C45.065,50.855 49.068,46.852 54,46.852C58.932,46.852 62.935,50.855 62.935,55.787C62.935,60.719 58.932,64.722 54,64.722Z"
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,9 @@
<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.25L56.25,56.25L56.25,69.75L51.75,69.75L51.75,56.25L38.25,56.25L38.25,51.75L51.75,51.75L51.75,38.25L56.25,38.25L56.25,51.75L69.75,51.75Z"
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,9 @@
<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.114C70.147,55.439 70.192,54.742 70.192,54C70.192,53.281 70.147,52.561 70.034,51.886L74.6,48.333C75.005,48.018 75.117,47.411 74.87,46.961L70.552,39.495C70.282,39 69.72,38.843 69.225,39L63.85,41.159C62.726,40.305 61.534,39.585 60.207,39.045L59.397,33.333C59.307,32.793 58.858,32.411 58.318,32.411L49.682,32.411C49.142,32.411 48.715,32.793 48.625,33.333L47.816,39.045C46.489,39.585 45.274,40.327 44.172,41.159L38.798,39C38.303,38.82 37.741,39 37.471,39.495L33.175,46.961C32.906,47.433 32.996,48.018 33.445,48.333L38.011,51.886C37.898,52.561 37.808,53.303 37.808,54C37.808,54.697 37.853,55.439 37.966,56.114L33.4,59.667C32.996,59.982 32.883,60.589 33.13,61.039L37.448,68.505C37.718,69 38.28,69.158 38.775,69L44.15,66.841C45.274,67.696 46.466,68.415 47.793,68.955L48.603,74.667C48.715,75.207 49.142,75.589 49.682,75.589L58.318,75.589C58.858,75.589 59.307,75.207 59.375,74.667L60.184,68.955C61.511,68.415 62.726,67.696 63.828,66.841L69.202,69C69.697,69.18 70.259,69 70.529,68.505L74.847,61.039C75.117,60.544 75.005,59.982 74.577,59.667ZM54,62.096C49.547,62.096 45.904,58.453 45.904,54C45.904,49.547 49.547,45.904 54,45.904C58.453,45.904 62.096,49.547 62.096,54C62.096,58.453 58.453,62.096 54,62.096Z"
android:fillColor="#ffffff"/>
</vector>
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 899 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

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