Compare commits

..
348 Commits
Author SHA1 Message Date
Markus Fisch f4af35ad46 Advance version number to 1.51.1 2022-06-16 16:51:39 +02:00
Markus Fisch 60f0d8bd87 Convert result text with ISO_8859_1 encoding
Important to suppress UTF-8 encoding.
2022-06-16 16:45:31 +02:00
Markus Fisch 1ccfcfcb8f Advance version number to 1.51.0 2022-06-16 13:37:44 +02:00
Markus Fisch 213689e9f9 Improve syntax for DP link generation 2022-06-16 13:30:07 +02:00
Markus Fisch 6e2f2cab92 Make drawing functions extensions in DetectorView
Because it's more idiomatic.
2022-06-16 13:20:32 +02:00
Markus Fisch 4b3d745154 Add and improve a few comments 2022-06-16 13:20:12 +02:00
Markus Fisch a2bcd67568 Use logical operator instead of bitwise 2022-06-16 13:19:14 +02:00
Markus Fisch 072eec5306 Simplify setting handleActive flag
And make sure it's false if handlePos is invalid.
2022-06-16 13:17:16 +02:00
ZahnstocherandGitHub d505c6b07d Add stamp tracking number 2022-06-14 18:41:41 +02:00
GiovanniandGitHub dbfbaab030 Update Italian translation 2022-06-14 14:26:07 +02:00
Markus Fisch 3d1402add4 Fix XML for zh-rCN translation 2022-06-12 20:29:20 +02:00
yzqzssandGitHub 1d2bd8d778 Update zh-rCN translation 2022-06-12 20:22:11 +02:00
Markus Fisch 2bb413e3e8 Add tests for MATMSG format 2022-06-07 20:43:15 +02:00
Markus Fisch 75a59bc3e9 Fix setting Intent.EXTRA_EMAIL for MatMsg contents
Remember, this must be an array.
2022-06-06 20:59:03 +02:00
inkhornandGitHub 9a15bf0871 Update ptbr translation including meta data 2022-05-31 20:28:03 +02:00
Markus Fisch 53248fd23d Allow free image rotation in pick activity
To make it easy to recognize barcodes at odd angles.
2022-05-23 23:33:23 +02:00
Markus Fisch 1dde8c2817 Advance version number to 1.50.0 2022-05-18 22:48:58 +02:00
Markus Fisch 57fea95b9b Add support for MATMSG format 2022-05-18 22:37:56 +02:00
solokotandGitHub 806dafbecd Updated Russian translation 2022-05-18 12:11:14 +02:00
Markus Fisch 44b788fa74 Add a setting to show the QR Code version
Which is determined by recreating the QR Code.

Knowing the exact version of a given QR Code can be useful sometimes.
2022-05-17 19:03:03 +02:00
ss11mikandGitHub 9f167e7a2b Update Czech translation
Add Czech translation for new strings and update some old ones
2022-05-15 13:25:46 +02:00
Markus Fisch 6852b65239 Recognize URLs in colloquial, incomplete form too
So "google.com" is recognized as web action.
2022-05-14 12:50:22 +02:00
Markus Fisch 3d7c9be7ba Update Kotlin version 2022-05-12 22:40:03 +02:00
Markus Fisch f9401a9dda Migrate to lint from lintOptions 2022-05-12 22:40:03 +02:00
Markus Fisch 79dd48b07a Move package from Android manifest to build files
Declaration of a project's namespace using the package attribute
of the Android manifest is deprecated in favour of a namespace
declaration in build files.
2022-05-12 22:40:03 +02:00
Markus Fisch 6a7ab2c6e5 Update build tools and gradle wrapper 2022-05-12 22:40:03 +02:00
NickoriginalandGitHub c76d96cbe4 Update Russian translation 2022-05-12 22:39:23 +02:00
Markus Fisch e1b3935659 Add timezone offset in calendar dates
VCalendar/VEvent dates may contain a timezone that was not actually
used, as Date.getTime() returns the number of milliseconds (since
January 1, 1970, 00:00:00 GMT) without taking the timezone into
account.
2022-05-09 21:47:51 +02:00
Markus Fisch b7a4db5f4b Update to latest ZXing version 2022-05-02 18:54:30 +02:00
Markus Fisch f23414b003 Remove unused getLinks() function
No longer used.
2022-04-30 20:25:06 +02:00
Markus Fisch 282d2499a8 Shorten encoding a barcode 2022-04-25 20:17:53 +02:00
Markus Fisch a6cc8ed32f Add sample for AndroidX Activity's to README
Because `startActivityForResult()` is deprecated there.
2022-04-25 18:11:12 +02:00
Markus Fisch 7b155369fd Fix closing end tag in Japanese translation 2022-04-24 15:16:54 +02:00
NPLandGitHub 55a69db2c5 Update Japanese translation 2022-04-24 15:14:17 +02:00
Balázs ÚrandGitHub 170878a795 Update Hungarian translation 2022-04-17 11:15:11 +02:00
Markus Fisch 18ade0fe33 Update icon and feature graphic for F-Droid 2022-04-10 13:30:37 +02:00
Markus Fisch 4565848344 Add changelog for latest version for f-droid 2022-04-08 12:12:37 +02:00
Markus Fisch 353e1b5494 Update tools version 2022-04-08 12:11:25 +02:00
Markus Fisch 9986a4ffb5 Advance version number to 1.49.2 2022-04-08 12:09:43 +02:00
Markus Fisch c464b7de02 Optimize latest vector drawables with avocado 2022-04-07 22:48:46 +02:00
Markus Fisch e588405413 Change icon design
Because Google thinks my original design would "impersonate someone
else".

I have absolutely no idea.

Well, in order to make the app available again as soon as possible,
and because there have already been complaints about the icon not
being "intuitive", and because it's probably pointless to argue with
Google's bots and minions, I'm just changing the design a bit.
2022-04-07 22:47:31 +02:00
mezy sincandMarkus Fisch fdaf20fd45 Fix pt-br translation 2022-04-02 19:32:33 +02:00
mezy sincandMarkus Fisch 7c8fdb8ea1 Update pt-br translation 2022-04-02 13:53:54 +02:00
Markus Fisch bb9d556c16 Advance version number to 1.49.1 2022-03-31 20:50:50 +02:00
VixbandGitHub 9e395d1928 Update Simplified Chinese translation 2022-03-31 19:11:01 +02:00
Markus Fisch 95f12e7c31 Return directly to caller for deep links
If this app was invoked via a deep link, but without a return URI,
we never want to return to the camera screen after scanning, but
to the caller.
2022-03-30 20:11:44 +02:00
Markus Fisch 9dac0b13fb Fix getting ORIENTATION member from meta data
Was UPC_EAN_EXTENSION instead.
2022-03-30 19:57:00 +02:00
Markus Fisch 7939a728bc Make isReturnUrl() a String extension
And thereby remove Intent from the scope of the function.
2022-03-30 19:51:19 +02:00
Markus Fisch b6ca20d813 Move Cursor extensions to a separate file 2022-03-30 19:48:43 +02:00
Markus Fisch fe7e36804e Advance version number to 1.49.0 2022-03-29 19:56:31 +02:00
Markus Fisch 8cf1c35029 Enable forwarding by default
To keep forwarding intact when updating an app.

This has no effect for new installations because initially there's
no forwarding URL set. Forwarding works only when this toggle is
enabled *and* if there was a forwarding URL set.
2022-03-29 19:44:05 +02:00
Markus Fisch 62ca2299a0 Clean up preprocessor test
And move creating RenderScript out of loop.

No need to re-create this for every sample.
2022-03-25 20:10:43 +01:00
Markus Fisch 2423586c8c Make Y in encodeYUV420SP() lower case
To comply with the Kotlin style guide.

And simply use s/t for x/y.
2022-03-24 19:49:21 +01:00
Markus Fisch 29b1f44bb9 Update getting Vibrator on Android S+
VIBRATOR_SERVICE is deprecated.
2022-03-24 19:18:36 +01:00
Markus Fisch acee18e016 Replace context getter with local variable
It's there anyway and we shouldn't run a function multiple times
where it's enough to do it once and go with the result.
2022-03-21 22:39:21 +01:00
Markus Fisch f798dce832 Make setZoom() a Camera extension 2022-03-21 22:31:09 +01:00
Markus Fisch 97838ec7c4 Remove unnecessary type declarations 2022-03-22 13:51:27 +01:00
Markus Fisch 767860ab07 Remove body of decodeLuminanceSource()
And use the single expression syntax instead.
2022-03-22 13:51:27 +01:00
Markus Fisch f8fd00f21b Use apply scope function to set ContentValues
A bit more elegant.
2022-03-22 13:51:27 +01:00
solokotandGitHub 2cf6aa0812 Update Russian translation 2022-03-22 12:00:31 +01:00
Markus Fisch 197554fd1b Add a toggle to quickly enable/disable forwarding
Easier than clearing the URL, especially if you want to keep the
URL for later.
2022-03-21 21:56:06 +01:00
Markus Fisch 3dc35ac60d Only play error peep when not in silent mode
The app shouldn't play any sound when the phone is set to silent.
2022-03-21 21:33:42 +01:00
Markus Fisch fd05209f57 Enable/disable torch mode according to parameters
And show an error toast in case setting flash parameters fails.
2022-03-21 19:48:59 +01:00
Markus Fisch 6d5aed816f Break very long lines in IAction
Improves readability for my taste.
2022-03-18 20:44:55 +01:00
Markus Fisch 268c395043 Configure new Intent with apply scope function
Instead of declaring a variable beforehand.
2022-03-18 20:41:36 +01:00
Markus Fisch 867dc36811 Desugar Java 8 language features for ZXing 3.4
Add desugaring support to reverse translate Java 8 language features
in order to use the latest ZXing without having to increase minSdk.

ZXing 3.4 doesn't work on SDK levels below 24 anymore, see:
https://github.com/zxing/zxing/issues/1170

Many thanks to @TheLastProject for showing me the way:
https://github.com/CatimaLoyalty/Android/commit/cbcf1bcd998b9a5fdf7354a4287c64e95f2c3845
2022-03-16 21:44:20 +01:00
Markus Fisch e6e9b912b6 Improve german translation 2022-03-15 20:15:15 +01:00
Markus Fisch 152b4e50fc Update Kotlin version 2022-03-15 20:15:02 +01:00
Markus Fisch 93070f38c9 Update Kotlin coroutines library 2022-03-15 20:14:39 +01:00
Markus Fisch 7a162d6fb0 Update JUnit version 2022-03-15 20:14:15 +01:00
solokotandGitHub 9c03e83d3a Update Russian translation 2022-03-15 09:28:44 +01:00
Markus Fisch 4be9421b44 Advance version number to 1.48.1 2022-03-11 15:21:16 +01:00
Markus Fisch b895997967 Fix broken detection of binary content
And simplify detection and handling of binary data.
2022-03-11 15:16:43 +01:00
Markus Fisch b5bbe659db Advance version number to 1.48.0 2022-03-10 12:04:34 +01:00
Markus Fisch bb7715455a Add an item to remove the format restriction
If it is set.
2022-03-09 19:53:37 +01:00
Markus Fisch 5283acee8f Add a menu item to restrict recognized formats
Only temporarily as long as the instance is used.

This allows to quickly restrict scanning to a certain format
without having to fiddle with all the formats in settings.
2022-03-09 19:47:33 +01:00
Markus Fisch 315abfb1dd Show alnum characters in binary data
And condense all non-alnum characters to just "…".

This may give a glimpse at the readable content and is probably
better to read than the hex dump for non-technical users.
2022-03-09 18:57:58 +01:00
Markus Fisch 05c776168e Make parseAndNormalizeUri() an extension function 2022-03-09 18:46:58 +01:00
Markus Fisch 880b8e0b24 Catch unsupported encodings in urlEncode()
And just return the original string in that case.

Invalid data shouldn't crash the app.
2022-03-09 18:43:33 +01:00
Markus Fisch 1597adb284 Fix showing binary data label for history scans
A scan's (String) "content" will be an empty string when read
from the database because I don't want to save the content twice
(binary data is already saved in the "raw" field).
2022-03-09 18:39:32 +01:00
Markus Fisch 1c356aeaed Update tools version 2022-03-09 18:37:16 +01:00
Markus Fisch fe934c5d6e Use AlertDialog from base SDK
Instead of the support variant.

There's no need to use the special variant from the support library.
2022-03-09 18:34:17 +01:00
Markus Fisch 5091565ff0 Make preference constants private
Because they can be.
2022-02-26 12:48:22 +01:00
Markus Fisch c7eb664958 Add a monochome launcher icon
As required for SDK 32, Android 13.
2022-02-21 19:48:08 +01:00
Markus Fisch c763c522e7 Update to SDK 32 2022-02-21 19:47:53 +01:00
Markus Fisch dc1beb5f28 Add new data extraction rules configuration
There's a new XML format for Android 12+.
2022-02-21 19:43:07 +01:00
Markus Fisch 76ea46cea8 Split data tag for VIEW intent filter
According to lint, it's better to have multiple tags with
individual attributes because it better "communicates the
combining behavior and is clearer".
2022-02-21 19:35:00 +01:00
Markus Fisch 0991fb85f0 Remove redundant label from activities 2022-02-21 19:30:57 +01:00
Markus Fisch 09f0d2e1db Remove unused quantity translations
According to lint, these quantities are not relevant.
2022-02-21 19:29:27 +01:00
Markus Fisch bc0655788c Add "many" plural for french translation
As required by the linter.
2022-02-21 19:24:57 +01:00
Markus Fisch 739a801cb4 Add "many" plural for czech translation
As required by the linter.
2022-02-21 19:23:50 +01:00
Markus Fisch c18f1b8770 Simplify Bitmap.saveAsPng() function 2022-02-19 18:37:13 +01:00
Markus Fisch b0df09d76f Simplify the execShareIntent function 2022-02-19 18:36:14 +01:00
Markus Fisch e8b8e578cb Fix handling empty meta strings
Check if the data is empty and not just null.

And prepare WiFi info like we do it for meta info.
2022-02-10 22:20:54 +01:00
Markus Fisch 09ce14125e Use apply scope function to use ViewHolder
And thereby avoid a temporary variable.

Also make getViewHolder() a bit more compact by using the
single-expression function syntax.
2022-02-10 19:48:12 +01:00
Markus Fisch 446d1543c3 Refactor Scan.timestamp to Scan.dateTime
Because it's really just a string containing the date and time
and not a timestamp in the sense of a Unix timestamp.
2022-02-08 19:23:33 +01:00
Markus Fisch 02a360beaf Refactor getRawBytes() to getRawData()
And make it an extension of Result.
2022-02-08 19:19:49 +01:00
Markus Fisch 9742e5b790 Give signing configuration a proper name
"config" isn't a very good name for a configuration.

These is the release configuration, so we should name it accordingly.
2022-02-07 20:38:55 +01:00
Markus Fisch 9c08aa1184 Update build tools 2022-01-27 23:01:57 +01:00
Markus Fisch ce8a5f89cd Remove buildToolsVersion as it's optional now
Since Gradle 3.0, the specification of "buildToolsVersion" in
"app/build.gradle" is optional.
2022-01-27 23:01:57 +01:00
Oğuz ErsenandGitHub 50795afeb4 Update Turkish translation 2022-01-27 23:01:40 +01:00
Markus Fisch b8c941ee62 Make sure to return a String and not null
Cursor.getString(int) may return null so this case needs
to be dealt with.
2022-01-26 16:44:40 +01:00
Markus Fisch 5b1738342d Ignore lint warning about the custom splash screen
Androidx SplashScreen compat library would require minSdk 21,
make the app unnecessarily larger and Android 12 automatically
replaces our custom splash screen anyway, which is exactly what
we want. So there is no reason to complain.

The migration guide says about our kind of splash screen:

> If your existing splash screen is implemented using a custom
> theme that overrides android:windowBackground, the system replaces
> your custom splash screen with a default Android system splash
> screen on Android 12 and higher (which may not be your app’s
> intended experience).

Source:
https://developer.android.com/guide/topics/ui/splash-screen/migrate

This is not only the intended but desired experience for this app.
2022-01-26 11:28:37 +01:00
Markus Fisch 004738440c Check return value of getColumnIndex() before use
To silence the Range lint error.

getColumnIndex() can return -1 which isn't suitable for
Cursor.get[Blob|Long|String]().

IMHO this is a quite useless, maybe even dangerous, lint error.
This kind of error should only happen in development where I
want to break things to see the problem. Now, using a wrong
name or asking for a non-existent column doesn't result in an
exception and is masking the problem.
2022-01-26 11:04:05 +01:00
Markus Fisch a90aee570a Ignore lint warning about ACCESS_COARSE_LOCATION
Android 12 requires apps that request ACCESS_FINE_LOCATION
to also request ACCESS_COARSE_LOCATION. But this app doesn't
request nor requires ACCESS_FINE_LOCATION above SDK 28.

So this is a false positive and we're ignoring it.
Hopefully, this will be fixed in future updates.
2022-01-25 18:38:12 +01:00
Markus Fisch 17f2f88b7f Update to SDK 31 2022-01-25 18:35:47 +01:00
Markus Fisch 071ea1500f Replace fraction string with fraction character
Using dedicated characters makes the text more readable.
2022-01-25 18:33:36 +01:00
Markus Fisch 25b142407b Explicitly set android:exported
As of Android 12, android:exported must be set.
2022-01-25 18:33:22 +01:00
Markus Fisch 848b4bbaa8 Upgrade gradle wrapper 2022-01-18 21:37:24 +01:00
Markus Fisch c2dc8984c1 Update syntax of zxing auxiliary functions 2022-01-18 21:23:09 +01:00
Sergiy StuparandGitHub e37a4d21ea Update Ukrainian translation 2022-01-13 18:41:49 +01:00
MastiggiaandGitHub da2a20ba1f Update the French translation 2021-12-29 22:32:44 +01:00
kwiateuszandGitHub 5fe69e5a3d Update Polish translation
And fix typo in english texts.
2021-12-27 16:19:30 +01:00
solokotandGitHub f50e1dab0a Updated Russian translation 2021-12-25 19:20:44 +01:00
Markus Fisch b165192377 Advance version number to 1.47.0 2021-12-23 20:34:15 +01:00
Markus Fisch c0a4d67cc5 Add new scan type to open data directly in browser
This way Binary Eye can also be used as a front-end to your own
website that deals with the scanned content directly.

This is of course the same as adding a URL for unknown content and
picking it from the dialog. However, this requires two taps and is
therefore more suitable for unusual use.
2021-12-23 18:32:23 +01:00
Markus Fisch ef36a2f7e0 Unify URL encoding with a String extension 2021-12-23 18:26:01 +01:00
Markus Fisch 40b90e0be2 Make hasNonPrintableCharacters() an extension 2021-12-23 17:49:46 +01:00
Markus Fisch 680030e89e Simplify opening URLs in external browser
So we can use `openUrl()` from anywhere now.
2021-12-23 17:49:46 +01:00
Markus Fisch b4fefad54f Improve readability with a traditional null check
Sure, this can be expressed more briefly, but not more comprehensibly.
2021-12-23 17:49:46 +01:00
Markus Fisch f65e942100 Break long expressions
For better comprehensibility. Should've done this a long time ago.
2021-12-23 17:49:46 +01:00
Markus Fisch 95d7a84648 Reset visibility of zoom bar
It needs to be made visible again after making it invisible.

Happens when one of the cameras doesn't support zooming.
2021-12-20 19:57:14 +01:00
Markus Fisch 1e4a0fde7a Make links to permissions references
So PRIVACY can be read in the shell.
2021-12-16 22:44:33 +01:00
Markus Fisch a6785d500e Update changelogs for F-Droid 2021-12-15 21:22:03 +01:00
Markus Fisch d77e464554 Advance version number to 1.46.3 2021-12-15 21:20:49 +01:00
Markus Fisch d7198934d7 Make fragment functions extension functions 2021-12-15 19:05:51 +01:00
Markus Fisch 1a3b3f263f Make bitmap editor functions extensions 2021-12-15 10:30:22 +01:00
Markus Fisch d4c5c6a1dc Fix cropping still images
Can't use Rect.width()/.height() with negative left/top values
because these get cropped at 0/0 of course.

Before this commit, the cropped image was larger than it should be.
2021-12-15 10:27:15 +01:00
Markus Fisch 8618a5b482 Avoid modifying ROI when scanning in PickActivity
detectorView.roi was modified by Rect.intersect() so we
need to make a copy to keep the currently selected ROI.
2021-12-15 10:22:42 +01:00
Markus Fisch 6cb65635e3 Update changelogs for F-Droid 2021-12-14 21:40:30 +01:00
Markus Fisch 5e37ff41d6 Make r/w byte array functions of Scan extensions 2021-12-14 21:37:23 +01:00
Markus Fisch 03ac3ff0e9 Make export and file saving functions extensions 2021-12-14 21:36:28 +01:00
Markus Fisch 480a248e71 Make restart functions extensions 2021-12-14 21:31:01 +01:00
Markus Fisch b4060d70ce Make permission functions extensions too 2021-12-14 21:30:37 +01:00
Markus Fisch 7ceb97e942 Make sharing functions Context extensions
Makes things simpler and more clear.
2021-12-14 21:27:37 +01:00
Markus Fisch 7100f79efc Use explicit CoroutineScope instead of GlobalScope
We shouldn't use GlobalScope directly because it can't be cancelled.
2021-12-14 18:56:23 +01:00
Markus Fisch 00a1ec877b Explicitly trim margin of SQL queries
And clean up helper functions.
2021-12-14 18:55:23 +01:00
Markus Fisch 89e029687b Cancel scanning if rectangles do not overlap
When scanning from an image. In reality, the rectangles will
always overlap but Android Studio can't know that so it's better
to add a safe guard.
2021-12-14 18:53:38 +01:00
Markus Fisch 5f4f18a062 Move zoom slider up and increase spacing around it
And center it with the FAB.

This should improve the usability because the slider should
now be a bit easier to grab. Also, it should interfere less
likely with gesture navigation.
2021-12-14 18:53:17 +01:00
Klaas SwartandMarkus Fisch 69ec2cda04 Update Dutch translation 2021-12-14 18:51:50 +01:00
anenasaandGitHub 4aba885fb6 Update zh-TW translation 2021-12-13 20:30:01 +01:00
Markus Fisch 9dbb78ce67 Advance version number to 1.46.2 2021-12-10 19:52:21 +01:00
Markus Fisch 47c252ab18 Fix requesting write permissions on Q and above
Requesting WRITE_EXTERNAL_STORAGE is only required before Android Q.
2021-12-10 19:45:50 +01:00
Markus Fisch 10e02b3164 Update tools version 2021-12-10 19:45:45 +01:00
Markus Fisch 7e82dbe730 Update changelogs for F-Droid 2021-12-06 19:43:55 +01:00
Markus Fisch e91c30f7c2 Advance version number to 1.46.1 2021-12-06 19:42:38 +01:00
Markus Fisch 30a36e7e07 Update Kotlin version 2021-12-04 19:35:54 +01:00
Markus Fisch 5f91fe2f02 Fix setting number of coordinates in DetectorView
mapResultToView() already gives the number of components (in
the target array), not points. So this number doesn't need to
be doubled.
2021-12-04 19:06:12 +01:00
Markus Fisch cab8d6c038 Restore/save crop limiter settings in PickActivity
Use the same setup that the user configured (in CameraActivity)
in PickActivity, too. There's no good reason to change the
scanning experience for scanning from an image.
2021-12-04 18:58:23 +01:00
Markus Fisch 4aac5dc61b Remove extra link on result screen
Because the very same functionality is already available with
the FAB, which is the default action for all content that can
be opened or shared.

I don't like having an extra element just for URLs that doesn't
add anything over the already available default action button.

So I tried, but I just don't like it. Sorry.

This reverts commit 9fb2eb2d2b.
2021-11-27 17:45:15 +01:00
Oğuz ErsenandGitHub fbbc20d327 Update Turkish translation 2021-11-25 18:45:24 +01:00
Markus Fisch 5595b68ff4 Update changelogs for F-Droid 2021-11-13 15:46:33 +01:00
Markus Fisch c492897fa9 Advance version number to 1.46.0 2021-11-13 15:45:30 +01:00
Markus Fisch 647c08bfb8 Add documentation for Deep Links and SCAN intent 2021-11-13 15:31:39 +01:00
Markus Fisch e5025ef58c Support deep links
To open the app from web links.
2021-11-12 23:54:19 +01:00
inkhornandGitHub 74bb421b2d Update pt-br translation 2021-11-08 21:47:23 +01:00
Markus Fisch 4ed8d4614a Update tools version 2021-10-28 13:51:01 +02:00
Markus Fisch f038eb989b Remove double setting WPA passphrase
It's already set in the enterprise configuration.
2021-10-25 21:11:08 +02:00
Markus Fisch c5722a3890 Collapse setting WPA passphrases in one line 2021-10-25 21:11:08 +02:00
Sergiy StuparandGitHub 41108007ad Update Ukrainian translation 2021-10-18 12:56:11 +02:00
Markus Fisch 867f179a37 Add support for "A:" in WIFI codes
Anonymous Identity may be "A" or "AI".
2021-10-17 19:19:49 +02:00
Markus Fisch 892916b0d0 Catch exception when passphrase isn't encodeable
And ignore WiFi codes that have passphrases that aren't ASCII
encodeable.
2021-10-15 21:23:37 +02:00
Markus Fisch 3bd440fdb7 Update tools version 2021-10-07 22:53:03 +02:00
Markus Fisch a4c8551be8 Clean up new code 2021-10-07 20:38:28 +02:00
Markus Fisch 316644d3ae Allow multiple query arguments in custom URL 2021-10-07 20:28:07 +02:00
Damir RakhimulinandGitHub 9fb2eb2d2b Add visual support for links after scanning 2021-10-07 20:23:07 +02:00
solokotandGitHub dd046a5d52 Russian translation for missing Wi-Fi info support 2021-10-07 12:13:32 +02:00
Balázs ÚrandGitHub f3ddd458cb Update Hungarian translation 2021-10-06 09:39:21 +02:00
Markus Fisch 157d928620 Add missing translations for Wi-Fi info support 2021-10-05 12:22:04 +02:00
Ngô Ngọc Đức HuyandGitHub 94cb509f0b Add Vietnamese translation 2021-10-05 12:15:27 +02:00
Sylvia van OsandGitHub df18f8ebd3 Add content data section
Initially with only Wi-Fi info support, but could be extended in the
future.
2021-10-05 12:15:11 +02:00
solokotandGitHub 705095472f Update Russian language 2021-09-30 19:50:25 +02:00
Markus Fisch f897973b3f Update changelogs for F-Droid 2021-09-29 22:28:59 +02:00
Markus Fisch c88d5cf4cd Advance version number to 1.45.0 2021-09-29 22:25:18 +02:00
Markus Fisch 8ecd49731f Add a setting to set the delay for bulk mode
As requested.
2021-09-29 22:21:15 +02:00
Markus Fisch 6f3b4dbdab Update build tools and Kotlin version 2021-08-31 17:08:58 +02:00
Markus Fisch 3c8c0737c4 Add support for MECARD contact format
By silently transforming it into VCARD format.

This might be genius or a really bad idea. Let's find out ;)
2021-08-31 17:06:49 +02:00
Markus Fisch 9baf6d621a Remove unnecessary safe call operators 2021-08-31 17:04:14 +02:00
Markus Fisch f0d0ee04af Replace toUpperCase with just uppercase
And the same for toLowerCase. Following Android Studio's suggestion
for the latest release of Kotlin.
2021-08-31 17:02:36 +02:00
Markus Fisch 4d38da045a Remove unused permissions on Android Q 2021-08-24 23:15:52 +02:00
Markus Fisch af70d9507b Remove enableR8=true from gradle.properties
No longer used with Gradle 7.
2021-08-13 09:51:12 +02:00
Markus Fisch 273f38c36c Update build tools and gradle 2021-08-10 10:52:22 +02:00
Markus Fisch 210cdfc171 Get the Context only once and put in a val
Instead of (silently) invoking getters multiple times.
2021-08-10 10:41:25 +02:00
Markus Fisch 3ec8c4b5d7 Write exported barcode file on IO dispatcher 2021-08-10 10:40:45 +02:00
Markus Fisch d99f0ca039 Update changelogs for F-Droid 2021-07-29 21:51:30 +02:00
Markus Fisch 4a9e700d20 Advance version number to 1.44.1 2021-07-29 21:50:06 +02:00
Markus Fisch 40e339ce89 Use a matrix to map from frame to view
Because it's more elegant and less code.

However, this solution requires more calculations than before
because `Matrix.mapPoints()` carries out a matrix vector
multiplication which consists of more calculations than what
was required before.

But even mobile CPUs are fast enough that this won't make any
difference.

Also, this solution avoids allocating Point and List objects in
the hot path. Also not too important because ZXing still
allocates a lot of objects but that's okay too.
2021-07-29 10:35:33 +02:00
Markus Fisch 9d476b3491 Include all language resources in App Bundles
So the in-app language setting works for App Bundles too.

Normally, Google Play delivers only the language resource of
the device to minimise download size.

Another solution would have been to use the PlayCore API to
download language resources on demand, but it makes much more
sense to simply include the ~60 kb than adding fragile code
and dependencies.
2021-07-29 10:29:43 +02:00
Markus Fisch 22b58265a5 Clean up code
According to Android Studio recommendations.
2021-07-15 16:33:20 +02:00
Markus Fisch bd4576b7ac Update tools version 2021-07-15 14:05:24 +02:00
ElaborendumandGitHub a57f6d1904 Update Spanish translation 2021-07-15 09:24:42 +02:00
Markus Fisch 842b38c986 Update tools and Kotlin version 2021-06-29 21:46:50 +02:00
Markus Fisch b1fae727a9 Calculate ratio for mapping on initialization
Because the ratio is fixed for the mapping.
2021-06-20 18:01:12 +02:00
Markus Fisch db24f608d5 Update changelogs for F-Droid 2021-06-06 14:31:07 +02:00
gnuhead-chiebandGitHub 9f49a00bae Update Japanese translation
* Update app Japanese translation
* Add Japanese store description
2021-06-06 14:29:38 +02:00
Markus Fisch 947bbf3031 Advance version number to 1.44.0 2021-06-05 14:49:33 +02:00
GazandGitHub b3b57a7ba8 Update Traditional Chinese Translation 2021-06-04 15:29:00 +02:00
solokotandGitHub 1627f3ead9 Update Russian language 2021-06-03 13:53:53 +02:00
Heimen StoffelsandGitHub 4343f5da82 Update Dutch translation 2021-06-02 22:18:41 +02:00
Markus Fisch 67395a067d Add a setting to choose barcode formats to scan
This setting isn't available before Honeycomb because there is
no MultiSelectListPreference before.
2021-06-02 19:05:55 +02:00
Markus Fisch 0de571f3c1 Ask before removing network suggestions
To avoid unintentional removal.
2021-06-02 19:02:38 +02:00
Markus Fisch d83cd77a87 Update changelogs for F-Droid 2021-06-02 19:02:38 +02:00
Heimen StoffelsandGitHub 5ca7aff5df Updated Dutch translation 2021-06-02 18:58:19 +02:00
solokotandGitHub e4cc516a9b Updated & improved Russian language 2021-06-02 18:57:52 +02:00
Markus Fisch f160cf0953 Advance version number to 1.43.0 2021-06-01 22:04:17 +02:00
Markus Fisch ef5b5aa34a Add a preference to remove network suggestions
Because as it turns out, WiFi network suggestions cannot be removed
by the user. Which is pretty silly in my opinion. But anyway, now a
user can remove all the networks added by Binary Eye.
2021-06-01 21:57:20 +02:00
GazandGitHub 536ee5bc3d Update Traditional Chinese Translation 2021-05-25 12:03:51 +02:00
solokotandGitHub 0becada572 Update Russian language 2021-05-25 11:55:34 +02:00
Markus Fisch 0c773d6f05 Update changelogs for F-Droid 2021-05-24 22:19:23 +02:00
Markus Fisch f2df364840 Advance version number to 1.42.0 2021-05-24 22:18:26 +02:00
Markus Fisch 5740b43568 Add a setting to pick a default search engine
To open unknown data with.
2021-05-24 22:12:13 +02:00
GazandGitHub b384b99b01 Update zh-TW(Traditional Chinese) Translation 2021-05-23 12:21:27 +02:00
Heimen StoffelsandGitHub 6affa4ce25 Update Dutch translation 2021-05-18 18:40:05 +02:00
Markus Fisch 455d329fff Remove resolveActivity() for opening content
Because on Android 11+ resolveActivity() can no longer be used
to query if there's an app that can handle the intent for privacy
reasons.
2021-05-07 22:15:38 +02:00
Markus Fisch 792be87283 Advance version number to 1.41.1 2021-05-06 23:01:49 +02:00
Markus Fisch 381e10bddc Ignore exceptions in MultiFormatReader.decode()
Usually it's bad practice to blindly catch *all* exceptions
because this can hide problems we want to know about.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Unfortunately, it's (currently) not possible to configure Gradle
to simply ignore the NDK. `nkd.dir` cannot be set to nothing or
an invalid value.
2020-07-04 14:09:57 +02:00
Markus Fisch fa3cc8c408 Remove extra action item to create bar code
Since the FAB's now always above the keyboard, there's no
need for an extra action item.
2020-06-29 19:47:27 +02:00
Markus Fisch e4863bad4c Keep soft keyboard from repositioning the layout
Only important for the EncodingFragment.
2020-06-29 19:46:52 +02:00
Markus Fisch 484bbbc997 Add changelog for latest version for f-droid 2020-06-23 21:38:07 +02:00
248 changed files with 6861 additions and 3533 deletions
+187
View File
@@ -1,5 +1,192 @@
# Change Log
## 1.51.1
* Fix extraction of raw bytes for some barcodes
## 1.51.0
* Add support for Deutsche Post Matrixcode stamp
* Allow free image rotation for scanning from images
* Fix setting TO field in MATMSG format
* Update Italian translation
* Update zh-rCN translation
* Update pt-br translation
## 1.50.0
* Add support for MATMSG format
* Add a setting to show the QR Code version
* Add timezone offset in calendar dates
* Recognize colloquial incomplete URLs
* Update ZXing library to latest version
* Update Czech translation
* Update Russian translation
* Update Japanese translation
* Update Hungarian translation
## 1.49.2
* Update design of app icon
* Update pt-br translation
## 1.49.1
* Update Simplified Chinese translation
* Return directly to caller for deep links
* Fix metadata for deep links
## 1.49.0
* Add a setting to quickly enable/disable forwarding
* Stop playing error beep when in silent mode
* Improve enabling/disabling torch mode
* Update to latest ZXing version
* Update German translation
* Update Russian translation
## 1.48.1
* Fix broken detection of binary content
## 1.48.0
* Add a menu item to temporarily scan a certain format only
* Add a monochome launcher icon for Android 13
* Show alpha numeric characters in binary data
* Fix showing binary data label for scans from the history
* Fix handling empty meta strings
* Update Turkish translation
* Update Ukrainian translation
* Update French translation
* Update Polish translation
* Update Russian translation
## 1.47.0
* Add forwarding into external browser
* Fix visibility of zoom slider when switching cameras
## 1.46.3
* Fix cropping still images
* Improve usability of zoom slider
* Update Dutch translation
* Update Chinese translation
## 1.46.2
* Fix saving external files on Android Q and newer
## 1.46.1
* Use cropping limiter settings for image picking too
* Remove extra link on result screen
* Update Turkish translation
## 1.46.0
* Support deep links
* Support "A:" in WIFI codes
* Allow multiple query arguments in custom URL
* Add Wi-Fi info data section
* Add Vietnamese translation
* Update Brazilian Portuguese translation
* Update Hungarian translation
* Update Ukrainian translation
* Update Russian translation
## 1.45.0
* Add a setting to set the delay for bulk mode
* Add support for MECARD contact format
* Remove unused permissions on Android Q
## 1.44.1
* Update Spanish translation
* Update Japanese translation
* Fix in-app language switching for App Bundles
## 1.44.0
* Add a setting to choose barcode formats to scan
* Ask before removing network suggestions
* Update Traditional Chinese Translation
* Update Russian language
* Update Dutch translation
## 1.43.0
* Add a preference item to remove WiFi network suggestions
* Update Traditional Chinese Translation
* Update Russian language
## 1.42.0
* Add a setting to pick a default search engine
* Update Traditional Chinese
* Update Dutch translation
## 1.41.1
* Update Bengali translation
* Update Simplified Chinese
## 1.41.0
* Use cropping limiter by default for new installations
* Truncate all toasts with strings from the outside
* Add Bangla translation
* Add missing Japanese locale
* Update Hungarian translation
* Update Russian app description
## 1.40.0
* Add a setting to show/hide scanned data when scanning continuously
* Only show the first 50 characters when scanning continuously
* Share binary data as hex dump
* Update Russian language
## 1.39.0
* Fix adding contacts and calendar entries on Android 11+
* Add a setting to always start in bulk mode
* Add Czech translation
* Update Spanish translation
* Update Czech translation
## 1.38.0
* Add a quick settings tile to scan from settings
* Make custom background requests time out after 5 seconds
* Add Japanese translation
* Updated French translation
## 1.37.0
* Improve formatting date and time in history listing
* Add haptic and audible feedback for background request errors
## 1.36.0
* Add a setting to automatically return to scanning screen after copying
* Add a button to test the background request
* Show HTTP response for background requests
* Improve SVG export
* Automatically continue with an action after granting a permission
* Update Chinese translation
* Update French translation
* Update Indonesian translation
* Update Turkish translation
* Update Ukrainian translation
## 1.35.0
* Put images with transparent pixels on white/black
* Use cropping limiter when scanning from images too
* Support setting the error correction level when creating QR Codes
* Support setting a custom deviating locale
* Use adaptive icons for shortcut icons
* Add exporting barcodes as plain text
* Add Ukrainian translation
* Add Turkish translation
* Add Georgian translation
* Update Indonesian translation
* Update Russian translation
* Update Italian translation
* Update Simplified Chinese translation
## 1.34.0
* Use Material Design settings with headers
* Add a setting to choose the message type when forwarding scanned content
## 1.33.0
* Add a setting to automatically send the scanned content to a custom URL
* Add a setting to automatically put contents into clipboard
* Add an explicit button to copy WiFi password into clipboard
* Update Indonesian translation
## 1.32.1
* Alternatively remove region of interest by dragging handle to a screen corner
* Restrict region of interest to screen space
* Remove extra action item to create barcode
## 1.32.0
* Add bulk mode
* Remember region of interest permanently
+13
View File
@@ -23,6 +23,19 @@ test:
cat:
./gradlew test cAT
testview:
adb shell am start -W -a android.intent.action.VIEW -d 'binaryeye://scan'
testscan:
adb shell am start -a android.intent.action.VIEW \
-c android.intent.category.BROWSABLE \
-d 'binaryeye://scan/?ret=http%3A%2F%2Fmarkusfisch.de%2F%3Fresult%3D{RESULT}'
testurl:
adb shell am start -a android.intent.action.VIEW \
-c android.intent.category.BROWSABLE \
-d 'http://markusfisch.de/BinaryEye?ret=http%3A%2F%2Fmarkusfisch.de%2F%3Fresult%3D{RESULT}'
install:
adb $(TARGET) install -r app/build/outputs/apk/debug/app-debug.apk
+54
View File
@@ -4,3 +4,57 @@ Binary Eye does not collect any personal information.
If you have any questions or suggestions about my Privacy Policy, do not
hesitate to contact me.
## Permissions
### [ACCESS_FINE_LOCATION][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][CAMERA]
Required to read a barcode from the camera image.
### [INTERNET][INTERNET]
Required to send a scanned code to a custom URL you can specify in settings.
### [VIBRATE][VIBRATE]
Required to vibrate on detection.
### [WRITE_EXTERNAL_STORAGE][WRITE_EXTERNAL_STORAGE]
Required to save generated barcodes as files before Android Q.
### [CHANGE_WIFI_STATE][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][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`.
[ACCESS_FINE_LOCATION]: https://developer.android.com/reference/android/Manifest.permission#ACCESS_FINE_LOCATION
[CAMERA]: https://developer.android.com/reference/android/Manifest.permission#CAMERA
[INTERNET]: https://developer.android.com/reference/android/Manifest.permission#INTERNET
[VIBRATE]: https://developer.android.com/reference/android/Manifest.permission#VIBRATE
[WRITE_EXTERNAL_STORAGE]: https://developer.android.com/reference/android/Manifest.permission#WRITE_EXTERNAL_STORAGE
[CHANGE_WIFI_STATE]: https://developer.android.com/reference/android/Manifest.permission#CHANGE_WIFI_STATE
[ACCESS_WIFI_STATE]: https://developer.android.com/reference/android/Manifest.permission#ACCESS_FINE_LOCATION
[configuredNetworks]: https://developer.android.com/reference/android/net/wifi/WifiManager#getConfiguredNetworks()
[addNetworkSuggestions]: https://developer.android.com/reference/android/net/wifi/WifiManager#addNetworkSuggestions(java.util.List%3Candroid.net.wifi.WifiNetworkSuggestion%3E)
+82 -26
View File
@@ -12,9 +12,14 @@ library.
## Screenshots
![Screenshot Scanning](fastlane/metadata/android/en-US/images/phoneScreenshots/screencap-scanning.png)
![Screenshot Result](fastlane/metadata/android/en-US/images/phoneScreenshots/screencap-decoded.png)
![Screenshot Compose Barcode](fastlane/metadata/android/en-US/images/phoneScreenshots/screencap-compose-barcode.png)
<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/screencap-scanning.png"
alt="Screenshot Gallery" width="160"/>
<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/screencap-scanning-cropped.png"
alt="Screenshot Gallery" width="160"/>
<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/screencap-decoded.png"
alt="Screenshot Theme" width="160"/>
<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/screencap-compose-barcode.png"
alt="Screenshot Editor" width="160"/>
## Download
@@ -59,6 +64,77 @@ ZXing can generate the following barcode formats:
* [QR CODE][qr_code]
* [UPC A][upc_a]
## Deep Links and Intents
### Deep Links
You can invoke Binary Eye with a web URI intent from anything that can
open URIs. There are two options:
1. [binaryeye://scan](binaryeye://scan)
2. [http(s)://markusfisch.de/BinaryEye](http://markusfisch.de/BinaryEye)
If you want to get the scanned contents, you can add a `ret` query
argument with a (URL encoded) URI template. For example:
[http://markusfisch.de/BinaryEye?ret=http%3A%2F%2Fexample.com%2F%3Fresult%3D{RESULT}](http://markusfisch.de/BinaryEye?ret=http%3A%2F%2Fexample.com%2F%3Fresult%3D{RESULT})
Supported symbols are:
* `RESULT` - scanned content
* `RESULT_BYTES` - raw result as a hex string
* `FORMAT` - barcode format
* `META` - the meta data, if available
### SCAN Intent
You can also use Binary Eye from other apps by using the
`com.google.zxing.client.android.SCAN` Intent with
[startActivityForResult()][start_activity] like this:
```kotlin
startActivityForResult(
Intent("com.google.zxing.client.android.SCAN"),
SOME_NUMBER
)
```
And process the result in [onActivityResult()][on_activity_result] of your
`Activity`:
```kotlin
override fun onActivityResult(
requestCode: Int,
resultCode: Int,
data: Intent?
) {
when (requestCode) {
SOME_NUMBER -> if (resultCode == RESULT_OK) {
val result = data.getStringExtra("SCAN_RESULT")
}
}
}
```
If you're using AndroidX, this would be the new,
[recommended way][intent_result]:
```kotlin
class YourActivity : Activity() {
private val resultLauncher = registerForActivityResult(StartActivityForResult()) { result ->
if (result.resultCode == Activity.RESULT_OK) {
val scan = result.data?.getStringExtra("SCAN_RESULT")
}
}
fun openScanner() {
resultLauncher.launch(Intent("com.google.zxing.client.android.SCAN"))
}
}
```
## RenderScript
This app uses [RenderScript][rs] to resize and rotate the camera image.
@@ -79,29 +155,6 @@ be thrown and the only option is to restart the app, this time with calling
Calling `RenderScript.forceCompat()` means the processing is done in
software so you probably don't want to enable it by default.
### macOS Catalina
At the time of writing, calling `RenderScript.forceCompat()` in a build made
on macOS Catalina with build tools 29.0.3 will crash the app. This is a bug
in the build tools for Catalina.
So *do not* build release builts on macOS Catalina with build tools 29.0.3.
Building on Linux with 29.0.3 works fine.
### Side by Side NDK
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.
### App Bundle
At the time of writing, `./gradlew bundleRelease` will produce a broken
App Bundle without the necessary RenderScript libraries. This is another
bug in the build tools.
### 2D barcodes
If you want to fork this and are only interested in reading 2D barcodes
@@ -132,3 +185,6 @@ custom RenderScript kernel for each architecture you want to support.
[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
[start_activity]: https://developer.android.com/reference/android/app/Activity#startActivityForResult(android.content.Intent,%20int)
[on_activity_result]: https://developer.android.com/reference/android/app/Activity#onActivityResult(int,%20int,%20android.content.Intent)
[intent_result]: https://developer.android.com/training/basics/intents/result
+40 -16
View File
@@ -2,26 +2,30 @@ apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
android {
namespace 'de.markusfisch.android.binaryeye'
compileSdkVersion sdk_version
buildToolsVersion build_tools_version
defaultConfig {
minSdkVersion 9
targetSdkVersion sdk_version
versionCode 69
versionName '1.32.0'
versionCode 99
versionName '1.51.1'
// it's recommended to set this value to the lowest API level
// able to provide all the functionality
// Required for desugaring.
multiDexEnabled true
// It's recommended to set this value to the lowest API level
// able to provide all the functionality.
renderscriptTargetApi 24
renderscriptSupportModeEnabled true
renderscriptSupportModeBlasEnabled true
testInstrumentationRunner 'android.support.test.runner.AndroidJUnitRunner'
}
signingConfigs {
config {
release {
keyAlias System.getenv('ANDROID_KEY_ALIAS')
keyPassword System.getenv('ANDROID_KEY_PASSWORD')
storePassword System.getenv('ANDROID_STORE_PASSWORD')
@@ -45,14 +49,33 @@ android {
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
minifyEnabled true
shrinkResources true
signingConfig signingConfigs.config
signingConfig signingConfigs.release
}
}
lintOptions {
// because at the time of writing, even the latest version
bundle {
language {
// To make the app bundle contain all language resources
// so the in-app language setting works. Another solution
// would be to use the PlayCore API to download language
// resources on demand, but it makes much more sense to
// simply include the ~60 kb than adding fragile code and
// dependencies.
enableSplit = false
}
}
compileOptions {
// To desugar fancy Java features in ZXing 3.4.+ that don't
// work on SDK < 24. See:
// https://github.com/zxing/zxing/issues/1170
coreLibraryDesugaringEnabled true
}
lint {
// Because at the time of writing, even the latest version
// of RenderScript is raising this error no matter if
// renderscriptTargetApi matches targetSdkVersion
// renderscriptTargetApi matches targetSdkVersion.
disable 'GradleCompatible'
}
}
@@ -64,15 +87,16 @@ dependencies {
androidTestImplementation('com.android.support.test:rules:1.0.2') {
exclude module: 'support-annotations'
}
testImplementation 'junit:junit:4.12'
testImplementation 'junit:junit:4.13.2'
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.2"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2"
implementation "com.android.support:appcompat-v7:$support_version"
implementation "com.android.support:design:$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.android.support:preference-v7:$support_version"
implementation "com.android.support:preference-v14:$support_version"
implementation 'com.google.zxing:core:3.5.0'
implementation 'com.github.markusfisch:CameraView:1.9.1'
implementation 'com.github.markusfisch:ScalingImageView:1.2.0'
implementation 'com.github.markusfisch:ScalingImageView:1.4.0'
}
+10
View File
@@ -9,4 +9,14 @@
<issue id="UnusedResources">
<ignore path="*/res/rs/debug/raw/bc*" />
</issue>
<!-- Android 12 requires apps that request ACCESS_FINE_LOCATION
to also request ACCESS_COARSE_LOCATION. But this app doesn't
request nor requires ACCESS_FINE_LOCATION above SDK 28. So
this is a false positive. -->
<issue id="CoarseFineLocation" severity="ignore"/>
<!-- Androidx SplashScreen compat library would require minSdk 21,
make the app unnecessarily larger and Android 12 automatically
replaces our custom splash screen anyway, which is exactly what
we want. So there is no reason to complain. -->
<issue id="CustomSplashScreen" severity="ignore"/>
</lint>
@@ -4,6 +4,7 @@ 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;
@@ -15,14 +16,17 @@ import java.util.regex.Pattern
class PreprocessorTest {
@Test
fun processSamples() {
val zxing = Zxing()
val assets = InstrumentationRegistry.getInstrumentation()
.context.assets
val samples = assets.list("samples")
checkNotNull(samples) { "no samples found" }
val pattern = Pattern.compile(
"[0-9]+-([0-9]+)deg.jpg"
)
val samples = assets.list("samples")
checkNotNull(samples) { "no samples found" }
val rs = RenderScript.create(
InstrumentationRegistry.getTargetContext(),
)
val zxing = Zxing()
for (sample in samples) {
val m = pattern.matcher(sample)
if (!m.find() || m.groupCount() < 1) {
@@ -36,7 +40,7 @@ class PreprocessorTest {
val frameHeight = bitmap.height
val frameOrientation = m.group(1) ?: return
val preprocessor = Preprocessor(
InstrumentationRegistry.getTargetContext(),
rs,
frameWidth,
frameHeight,
null
@@ -55,9 +59,9 @@ class PreprocessorTest {
}
val result = zxing.decode(frameData, outWidth, outHeight)
preprocessor.destroy()
checkNotNull(result) { "no barcode found in $sample" }
}
rs.destroy()
}
}
@@ -76,21 +80,21 @@ private fun encodeYUV420SP(argb: IntArray, width: Int, height: Int): ByteArray {
var r: Int
var g: Int
var b: Int
var Y: 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) {
for (t in 0 until height) {
for (s 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
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[yIndex++] = (if (y < 0) 0 else if (y > 255) 255 else y).toByte()
if (t % 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()
}
@@ -4,35 +4,7 @@
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#FF000000"
android:pathData="M52,-884.362h29v71h-29z"/>
<path
android:fillColor="#FF000000"
android:pathData="M52,-884.362h29v71h-29z"/>
<path
android:fillColor="#FF000000"
android:pathData="M52,-884.362h29v71h-29z"/>
<path
android:fillColor="#FF000000"
android:pathData="M32.002,-24.974h3.127v7.656h-3.127z"/>
<path
android:fillColor="#FF000000"
android:pathData="M52,-884.362h29v71h-29z"/>
<path
android:fillColor="#FF000000"
android:pathData="M52,-884.362h29v71h-29z"/>
<path
android:fillColor="#FF000000"
android:pathData="M52,-884.362h29v71h-29z"/>
<path
android:fillColor="#FF000000"
android:pathData="M52,-884.362h29v71h-29z"/>
<path
android:fillColor="#FF000000"
android:pathData="M52,-884.362h29v71h-29z"/>
<path
android:pathData="m54.826,27.893c-6.851,0 -13.701,2.581 -18.924,7.745 -10.445,10.328 -10.413,27.062 0,37.423l36.218,36.036 37.879,0.902 -0.07,-38.363 -36.18,-35.998c-5.207,-5.18 -12.073,-7.745 -18.924,-7.745z"
android:strokeAlpha="1"
android:pathData="M54.826,27.894C47.976,27.894 41.125,30.475 35.903,35.639C25.457,45.967 25.489,62.701 35.903,73.062L72.121,109.098L108,108L109.93,71.637L73.75,35.639C68.543,30.459 61.677,27.894 54.826,27.894Z"
android:strokeLineJoin="miter"
android:strokeWidth="8.60725021"
android:fillColor="#000000"
@@ -41,43 +13,25 @@
android:fillAlpha="0.1254902"
android:strokeLineCap="butt"/>
<path
android:pathData="M28.001,54.118a26.525,26.227 0,1 0,53.049 0a26.525,26.227 0,1 0,-53.049 0z"
android:strokeAlpha="1"
android:pathData="M27.475,54a26.525,26.5 0,1 0,53.05 0a26.525,26.5 0,1 0,-53.05 0z"
android:strokeLineJoin="miter"
android:strokeWidth="4"
android:fillColor="#ffffff"
android:strokeColor="#00000000"
android:fillAlpha="1"
android:strokeLineCap="butt"/>
<path
android:pathData="M39.26,54.988a14.509,14.346 0,1 0,29.019 0a14.509,14.346 0,1 0,-29.019 0z"
android:strokeAlpha="1"
android:pathData="M72.745,54.001C72.745,58.588 71.099,62.791 68.362,66.049L68.362,41.953C71.099,45.21 72.745,49.412 72.745,54.001ZM54.001,72.745C53.463,72.745 52.931,72.723 52.406,72.679L52.406,35.322C52.931,35.278 53.463,35.255 54.001,35.255C54.538,35.255 55.071,35.278 55.596,35.322L55.596,72.679C55.071,72.723 54.538,72.745 54.001,72.745ZM35.255,54.001C35.255,49.412 36.903,45.21 39.638,41.953L39.638,66.049C36.903,62.791 35.255,58.588 35.255,54.001ZM61.98,70.969C60.961,71.447 59.895,71.838 58.789,72.13L58.789,35.873C59.895,36.164 60.961,36.553 61.98,37.032L61.98,70.969ZM49.213,72.13C46.877,71.514 44.717,70.458 42.831,69.056L42.831,38.945C44.717,37.543 46.877,36.487 49.213,35.873L49.213,72.13Z"
android:strokeLineJoin="miter"
android:strokeWidth="4"
android:fillColor="#000000"
android:strokeColor="#00000000"
android:fillAlpha="1"
android:strokeLineCap="butt"/>
<path
android:pathData="M54.034,44.513a9.535,9.428 0,1 0,19.07 0a9.535,9.428 0,1 0,-19.07 0z"
android:strokeAlpha="1"
android:strokeLineJoin="miter"
android:strokeWidth="10"
android:fillColor="#ffffff"
android:strokeColor="#00000000"
android:fillAlpha="1"
android:strokeLineCap="butt"/>
<path
android:fillColor="#FF000000"
android:pathData="M52,-884.362h29v71h-29z"/>
<path
android:pathData="m62.745,24.783c-2.055,1.011 -2.87,3.415 -1.847,5.446 0.588,1.168 1.642,1.916 2.839,2.168 -0.564,0.884 -0.835,1.972 -0.925,3.156 -1.782,-0.45 -3.735,-0.191 -5.25,0.639a0.799,0.808 90,0 0,-0.132 0.172,0.799 0.808,90 0,0 -0.132,0.172 0.799,0.808 90,0 0,-0.132 0.39,0.799 0.808,90 0,0 0.044,0.304 0.799,0.808 90,0 0,0.437 0.433,0.799 0.808,90 0,0 0.131,0.043 0.799,0.808 90,0 0,0.131 0.043,0.799 0.808,90 0,0 0.218,-0 0.799,0.808 90,0 0,0.263 -0.129c1.29,-0.707 2.97,-0.892 4.375,-0.425 0.096,0.882 0.341,1.782 0.695,2.683 -0.456,0.072 -0.936,0.177 -1.357,0.343 -1.207,0.48 -2.245,1.302 -3.022,2.244a0.799,0.808 90,0 0,-0.044 0.129,0.799 0.808,90 0,0 -0.088,0.258 0.799,0.808 90,0 0,0.044 0.304,0.799 0.808,90 0,0 0.393,0.563 0.799,0.808 90,0 0,0.394 0.131,0.799 0.808,90 0,0 0.175,-0.086 0.799,0.808 90,0 0,0.175 -0.086,0.799 0.808,90 0,0 0.219,-0.215c0.922,-1.115 2.409,-1.925 3.897,-2.025 0.494,0.845 1.016,1.575 1.658,2.209 -1.232,1.149 -2.043,2.664 -2.239,4.234a0.799,0.808 90,0 0,-0.001 0.215,0.799 0.808,90 0,0 0.174,0.346 0.799,0.808 90,0 0,0.043 0.086,0.799 0.808,90 0,0 0.219,0.217 0.799,0.808 90,0 0,0.131 0.043,0.799 0.808,90 0,0 0.131,0.043 0.799,0.808 90,0 0,0.218 -0,0.799 0.808,90 0,0 0.218,-0 0.799,0.808 90,0 0,0.132 -0.172,0.799 0.808,90 0,0 0.175,-0.086 0.799,0.808 90,0 0,0.044 -0.129,0.799 0.808,90 0,0 0.088,-0.258 0.799,0.808 90,0 0,0.044 -0.129c0.159,-1.272 0.816,-2.493 1.799,-3.371 1.966,1.394 4.252,1.797 6.122,0.876 1.892,-0.931 2.903,-3.018 2.941,-5.444 1.327,-0.259 2.813,0.014 3.936,0.656a0.799,0.808 90,0 0,0.174 0.129,0.799 0.808,90 0,0 0.131,0.043 0.799,0.808 90,0 0,0.088 -0.043,0.799 0.808,90 0,0 0.218,-0 0.799,0.808 90,0 0,0.087 -0.043,0.799 0.808,90 0,0 0.175,-0.086 0.799,0.808 90,0 0,0.132 -0.172,0.799 0.808,90 0,0 0.132,-0.172 0.799,0.808 90,0 0,0.045 -0.562,0.799 0.808,90 0,0 -0.043,-0.086 0.799,0.808 90,0 0,-0.262 -0.303,0.799 0.808,90 0,0 -0.131,-0.043c-1.386,-0.792 -3.097,-1.066 -4.766,-0.787 -0.125,-0.89 -0.309,-1.783 -0.695,-2.683 0.982,-1.073 2.47,-1.756 3.896,-1.81a0.799,0.808 90,0 0,0.131 0.043,0.799 0.808,90 0,0 0.087,-0.043 0.799,0.808 90,0 0,0.482 -0.345,0.799 0.808,90 0,0 0.044,-0.129 0.799,0.808 90,0 0,0.088 -0.476,0.799 0.808,90 0,0 -0.043,-0.086 0.799,0.808 90,0 0,-0.262 -0.303,0.799 0.808,90 0,0 -0.043,-0.086 0.799,0.808 90,0 0,-0.394 -0.131,0.799 0.808,90 0,0 -0.131,-0.043c-1.227,0.048 -2.518,0.369 -3.633,1.031 -0.388,0.233 -0.762,0.548 -1.095,0.864 -0.513,-0.821 -1.093,-1.559 -1.746,-2.166 0.47,-1.39 1.632,-2.605 2.98,-3.195a0.799,0.808 90,0 0,0.175 -0.086,0.799 0.808,90 0,0 0.132,-0.172 0.799,0.808 90,0 0,0.132 -0.172,0.799 0.808,90 0,0 0.132,-0.39 0.799,0.808 90,0 0,-0.043 -0.086,0.799 0.808,90 0,0 -0.174,-0.346 0.799,0.808 90,0 0,-0.043 -0.086,0.799 0.808,90 0,0 -0.437,-0.217 0.799,0.808 90,0 0,-0.175 0.086,0.799 0.808,90 0,0 -0.131,-0.043 0.799,0.808 90,0 0,-0.087 0.043,0.799 0.808,90 0,0 -0.131,-0.043c-1.584,0.694 -2.97,2.079 -3.681,3.756 -1.012,-0.644 -2.048,-1.087 -3.104,-1.173 0.53,-1.101 0.642,-2.414 0.05,-3.59 -1.023,-2.031 -3.541,-2.795 -5.596,-1.783z"
android:pathData="m62.745,28.783c-2.055,1.011 -2.87,3.415 -1.847,5.446 0.588,1.168 1.642,1.916 2.839,2.168 -0.564,0.884 -0.835,1.972 -0.925,3.156 -1.782,-0.45 -3.735,-0.191 -5.25,0.639a0.799,0.808 90.001,0 0,-0.132 0.172,0.799 0.808,90.001 0,0 -0.132,0.172 0.799,0.808 90.001,0 0,-0.132 0.39,0.799 0.808,90.001 0,0 0.044,0.304 0.799,0.808 90.001,0 0,0.437 0.433,0.799 0.808,90.001 0,0 0.131,0.043 0.799,0.808 90.001,0 0,0.131 0.043,0.799 0.808,90.001 0,0 0.218,-0 0.799,0.808 90.001,0 0,0.263 -0.129c1.29,-0.707 2.97,-0.892 4.375,-0.425 0.096,0.882 0.341,1.782 0.695,2.683 -0.456,0.072 -0.936,0.177 -1.357,0.343 -1.207,0.48 -2.245,1.302 -3.022,2.244a0.799,0.808 90.001,0 0,-0.044 0.129,0.799 0.808,90.001 0,0 -0.088,0.258 0.799,0.808 90.001,0 0,0.044 0.304,0.799 0.808,90.001 0,0 0.393,0.563 0.799,0.808 90.001,0 0,0.394 0.131,0.799 0.808,90.001 0,0 0.175,-0.086 0.799,0.808 90.001,0 0,0.175 -0.086,0.799 0.808,90.001 0,0 0.219,-0.215c0.922,-1.115 2.409,-1.925 3.897,-2.025 0.494,0.845 1.016,1.575 1.658,2.209 -1.232,1.149 -2.043,2.664 -2.239,4.234a0.799,0.808 90.001,0 0,-0.001 0.215,0.799 0.808,90.001 0,0 0.174,0.346 0.799,0.808 90.001,0 0,0.043 0.086,0.799 0.808,90.001 0,0 0.219,0.217 0.799,0.808 90.001,0 0,0.131 0.043,0.799 0.808,90.001 0,0 0.131,0.043 0.799,0.808 90.001,0 0,0.218 -0,0.799 0.808,90.001 0,0 0.218,-0 0.799,0.808 90.001,0 0,0.132 -0.172,0.799 0.808,90.001 0,0 0.175,-0.086 0.799,0.808 90.001,0 0,0.044 -0.129,0.799 0.808,90.001 0,0 0.088,-0.258 0.799,0.808 90.001,0 0,0.044 -0.129c0.159,-1.272 0.816,-2.493 1.799,-3.371 1.966,1.394 4.252,1.797 6.122,0.876 1.892,-0.931 2.903,-3.018 2.941,-5.444 1.327,-0.259 2.813,0.014 3.936,0.656a0.799,0.808 90.001,0 0,0.174 0.129,0.799 0.808,90.001 0,0 0.131,0.043 0.799,0.808 90.001,0 0,0.088 -0.043,0.799 0.808,90.001 0,0 0.218,-0 0.799,0.808 90.001,0 0,0.087 -0.043,0.799 0.808,90.001 0,0 0.175,-0.086 0.799,0.808 90.001,0 0,0.132 -0.172,0.799 0.808,90.001 0,0 0.132,-0.172 0.799,0.808 90.001,0 0,0.045 -0.562,0.799 0.808,90.001 0,0 -0.043,-0.086 0.799,0.808 90.001,0 0,-0.262 -0.303,0.799 0.808,90.001 0,0 -0.131,-0.043c-1.386,-0.792 -3.097,-1.066 -4.766,-0.787 -0.125,-0.89 -0.309,-1.783 -0.695,-2.683 0.982,-1.073 2.47,-1.756 3.896,-1.81a0.799,0.808 90.001,0 0,0.131 0.043,0.799 0.808,90.001 0,0 0.087,-0.043 0.799,0.808 90.001,0 0,0.482 -0.345,0.799 0.808,90.001 0,0 0.044,-0.129 0.799,0.808 90.001,0 0,0.088 -0.476,0.799 0.808,90.001 0,0 -0.043,-0.086 0.799,0.808 90.001,0 0,-0.262 -0.303,0.799 0.808,90.001 0,0 -0.043,-0.086 0.799,0.808 90.001,0 0,-0.394 -0.131,0.799 0.808,90.001 0,0 -0.131,-0.043c-1.227,0.048 -2.518,0.369 -3.633,1.031 -0.388,0.233 -0.762,0.548 -1.095,0.864 -0.513,-0.821 -1.093,-1.559 -1.746,-2.166 0.47,-1.39 1.632,-2.605 2.98,-3.195a0.799,0.808 90.001,0 0,0.175 -0.086,0.799 0.808,90.001 0,0 0.132,-0.172 0.799,0.808 90.001,0 0,0.132 -0.172,0.799 0.808,90.001 0,0 0.132,-0.39 0.799,0.808 90.001,0 0,-0.043 -0.086,0.799 0.808,90.001 0,0 -0.174,-0.346 0.799,0.808 90.001,0 0,-0.043 -0.086,0.799 0.808,90.001 0,0 -0.437,-0.217 0.799,0.808 90.001,0 0,-0.175 0.086,0.799 0.808,90.001 0,0 -0.131,-0.043 0.799,0.808 90.001,0 0,-0.087 0.043,0.799 0.808,90.001 0,0 -0.131,-0.043c-1.584,0.694 -2.97,2.079 -3.681,3.756 -1.012,-0.644 -2.048,-1.087 -3.104,-1.173 0.53,-1.101 0.642,-2.414 0.05,-3.59 -1.023,-2.031 -3.541,-2.795 -5.596,-1.783z"
android:fillColor="#ff2a2a"
android:strokeColor="#00000000"
android:fillAlpha="1"/>
android:strokeColor="#00000000"/>
<path
android:pathData="m66.624,32.489c0.281,-0.138 1.896,2.465 3.62,5.889 1.724,3.424 2.893,6.354 2.613,6.492 -0.281,0.138 -1.94,-2.551 -3.663,-5.975 -1.724,-3.424 -2.85,-6.268 -2.569,-6.406z"
android:pathData="m66.624,36.489c0.281,-0.138 1.896,2.465 3.62,5.889 1.724,3.424 2.893,6.354 2.613,6.492 -0.281,0.138 -1.94,-2.551 -3.663,-5.975 -1.724,-3.424 -2.85,-6.268 -2.569,-6.406z"
android:fillColor="#d40000"
android:strokeColor="#00000000"
android:fillAlpha="1"/>
android:strokeColor="#00000000"/>
</vector>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.5 KiB

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

+45 -12
View File
@@ -1,23 +1,39 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="de.markusfisch.android.binaryeye"
android:installLocation="auto">
<supports-screens
android:largeScreens="true"
android:xlargeScreens="true"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"
android:maxSdkVersion="28"/>
<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.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="28"/>
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE"/>
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"
android:maxSdkVersion="28"/>
<uses-feature android:name="android.hardware.camera"/>
<uses-feature android:name="android.hardware.camera.autofocus"/>
<uses-sdk tools:overrideLibrary="android.support.v14.preference"/>
<queries>
<intent>
<action android:name="android.intent.action.VIEW"/>
<data android:scheme="https"/>
</intent>
<intent>
<action android:name="android.intent.action.VIEW"/>
<data android:scheme="http"/>
</intent>
</queries>
<application
tools:ignore="UnusedAttribute"
android:name=".app.BinaryEyeApp"
android:usesCleartextTraffic="true"
android:allowBackup="true"
android:fullBackupContent="@xml/backup_rules"
android:dataExtractionRules="@xml/extraction_rules"
android:supportsRtl="true"
android:resizeableActivity="true"
android:label="@string/app_name"
@@ -25,8 +41,8 @@
android:theme="@style/AppTheme">
<activity
android:name=".activity.SplashActivity"
android:label="@string/app_name"
android:theme="@style/SplashTheme">
android:theme="@style/SplashTheme"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
@@ -37,7 +53,8 @@
</activity>
<activity
android:name=".activity.CameraActivity"
android:label="@string/scan_code">
android:label="@string/scan_code"
android:exported="true">
<intent-filter android:label="@string/compose_barcode">
<action android:name="android.intent.action.SEND"/>
<category android:name="android.intent.category.DEFAULT"/>
@@ -51,15 +68,22 @@
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>
<data
android:host="www.markusfisch.de"
android:pathPrefix="/BinaryEye"
android:scheme="http"/>
<data android:host="markusfisch.de"/>
<data android:pathPrefix="/BinaryEye"/>
<data android:scheme="http"/>
<data android:scheme="https"/>
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>
<data android:scheme="binaryeye" android:host="scan"/>
</intent-filter>
</activity>
<activity
android:name=".activity.PickActivity"
android:label="@string/pick_code_to_scan"
android:exported="true"
tools:ignore="AppLinkUrlError">
<intent-filter android:label="@string/decode_barcode">
<action android:name="android.intent.action.SEND"/>
@@ -74,8 +98,17 @@
</activity>
<activity
android:name=".activity.MainActivity"
android:label="@string/app_name"
android:exported="true"/>
<service
android:name=".service.ScanTileService"
android:icon="@drawable/ic_action_scan"
android:label="@string/scan_code"
android:exported="true"
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
<intent-filter>
<action android:name="android.service.quicksettings.action.QS_TILE"/>
</intent-filter>
</service>
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="${applicationId}.provider"
@@ -1,6 +1,7 @@
package de.markusfisch.android.binaryeye.actions
import de.markusfisch.android.binaryeye.actions.mail.MailAction
import de.markusfisch.android.binaryeye.actions.mail.MatMsgAction
import de.markusfisch.android.binaryeye.actions.otpauth.OtpauthAction
import de.markusfisch.android.binaryeye.actions.search.OpenOrSearchAction
import de.markusfisch.android.binaryeye.actions.sms.SmsAction
@@ -15,13 +16,16 @@ object ActionRegistry {
private val REGISTRY: Set<IAction> = setOf(
MailAction,
MatMsgAction,
OtpauthAction,
SmsAction,
TelAction,
VCardAction,
VEventAction,
WebAction,
WifiAction
WifiAction,
// Try WebAction last because recognizing colloquial URLs is
// very aggressive.
WebAction
)
fun getAction(data: ByteArray): IAction = REGISTRY.find {
@@ -2,8 +2,8 @@ package de.markusfisch.android.binaryeye.actions
import android.content.Context
import android.content.Intent
import de.markusfisch.android.binaryeye.app.execShareIntent
import de.markusfisch.android.binaryeye.app.parseAndNormalizeUri
import de.markusfisch.android.binaryeye.content.execShareIntent
import de.markusfisch.android.binaryeye.content.openUrl
import de.markusfisch.android.binaryeye.widget.toast
interface IAction {
@@ -18,31 +18,36 @@ 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)
val intent = createIntent(context, data)
if (intent == null) {
context.toast(errorMsg)
} else {
context.execShareIntent(intent)
}
}
abstract suspend fun createIntent(context: Context, data: ByteArray): 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))
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))
context.openUrl(String(data))
}
}
@@ -0,0 +1,55 @@
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 MatMsgAction : IntentAction() {
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).startsWith("MATMSG:")
}
override suspend fun createIntent(
context: Context,
data: ByteArray
): Intent? {
val mm = MatMsg(String(data))
// Allow incomplete but not completely missing data.
if (mm.isEmpty()) {
return null
}
return Intent(
Intent.ACTION_SENDTO,
Uri.parse("mailto:")
).apply {
mm.to?.let {
putExtra(Intent.EXTRA_EMAIL, arrayOf(mm.to))
}
mm.sub?.let {
putExtra(Intent.EXTRA_SUBJECT, mm.sub)
}
mm.body?.let {
putExtra(Intent.EXTRA_TEXT, mm.body)
}
}
}
}
// Public so this can be tested.
class MatMsg(encoded: String) {
// Allow arbitrary order.
val to = encoded.extractFirst("""[:;]TO:([\w.%@+-]+);""")
val sub = encoded.extractFirst("""[:;]SUB:([^;]+);""")
val body = encoded.extractFirst("""[:;]BODY:([^;]+);""")
fun isEmpty() = to == null && sub == null && body == null
}
private fun String.extractFirst(regex: String) =
regex.toRegex().find(this)?.groupValues?.get(1)
@@ -1,15 +1,12 @@
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.urlEncode
import de.markusfisch.android.binaryeye.app.prefs
import de.markusfisch.android.binaryeye.widget.toast
import java.net.URLEncoder
import de.markusfisch.android.binaryeye.content.openUrl
object OpenOrSearchAction : IAction {
override val iconResId: Int = R.drawable.ic_action_search
@@ -18,28 +15,39 @@ object OpenOrSearchAction : IAction {
override fun canExecuteOn(data: ByteArray): Boolean = false
override suspend fun execute(context: Context, data: ByteArray) {
val intent = openUri(context, String(data)) ?: return
execShareIntent(context, intent)
view(context, String(data), true)
}
private suspend fun openUri(context: Context, data: String, search: Boolean = true): Intent? {
val uri = parseAndNormalizeUri(data)
val intent = Intent(Intent.ACTION_VIEW, uri)
when {
intent.resolveActivity(context.packageManager) != null -> return intent
search -> return getSearchIntent(context, data)
else -> context.toast(R.string.cannot_resolve_action)
private suspend fun view(
context: Context,
url: String,
search: Boolean
) {
if (!context.openUrl(url) && search) {
openSearch(context, url)
}
return null
}
private suspend fun getSearchIntent(context: Context, query: String): Intent? {
private suspend fun openSearch(context: Context, query: String) {
val defaultSearchUrl = prefs.defaultSearchUrl
if (defaultSearchUrl.isNotEmpty()) {
view(
context,
defaultSearchUrl + query.urlEncode(),
false
)
return
}
val names = context.resources.getStringArray(
R.array.search_engines_names
).toMutableList()
val urls = context.resources.getStringArray(
R.array.search_engines_values
).toMutableList()
// Remove the "Always ask" entry. The arrays search_engines_*
// are used in the preferences too.
names.removeFirst()
urls.removeFirst()
if (prefs.openWithUrl.isNotEmpty()) {
names.add(prefs.openWithUrl)
urls.add(prefs.openWithUrl)
@@ -47,9 +55,9 @@ object OpenOrSearchAction : IAction {
val queryUri = alertDialog<String>(context) { resume ->
setTitle(R.string.pick_search_engine)
setItems(names.toTypedArray()) { _, which ->
resume(urls[which] + URLEncoder.encode(query, "utf-8"))
resume(urls[which] + query.urlEncode())
}
} ?: return null
return openUri(context, queryUri, false)
} ?: return
view(context, queryUri, false)
}
}
@@ -7,8 +7,9 @@ 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)
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
@@ -28,7 +29,9 @@ object SmsAction : IntentAction() {
Intent.ACTION_SENDTO,
Uri.parse("smsto:$number")
).apply {
if (message.isNotEmpty()) putExtra("sms_body", message)
if (message.isNotEmpty()) {
putExtra("sms_body", message)
}
}
}
}
@@ -7,7 +7,9 @@ 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)
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
@@ -17,7 +19,11 @@ object TelAction : IntentAction() {
return String(data).matches(telRegex)
}
override suspend fun createIntent(context: Context, data: ByteArray): Intent {
return Intent(Intent.ACTION_DIAL, parseAndNormalizeUri(String(data)))
}
override suspend fun createIntent(
context: Context,
data: ByteArray
): Intent = Intent(
Intent.ACTION_DIAL,
String(data).parseAndNormalizeUri()
)
}
@@ -15,7 +15,7 @@ object VTypeParser {
fun parseVType(data: String): String? = vTypeRegex.matchEntire(
data
)?.groupValues?.get(1)?.toUpperCase(Locale.US)
)?.groupValues?.get(1)?.uppercase(Locale.US)
fun parseMap(
data: String
@@ -26,7 +26,7 @@ object VTypeParser {
it.groupValues[2]
)
}
}.groupBy({ it.first.toUpperCase(Locale.US) }) { it.second }
}.groupBy({ it.first.uppercase(Locale.US) }) { it.second }
}
data class VTypeProperty(
@@ -20,10 +20,19 @@ object VCardAction : IntentAction() {
get() = R.string.vcard_failed
override fun canExecuteOn(data: ByteArray): Boolean {
return VTypeParser.parseVType(String(data)) == "VCARD"
var stringData = String(data)
// Quick & dirty MECARD support: simply make it a VCARD.
if (stringData.startsWith("MECARD:")) {
stringData = stringData
.trim()
.replace("MECARD:", "BEGIN:VCARD\n")
.replace(";;", "\nEND:VCARD\n")
.replace(";", "\n")
}
return VTypeParser.parseVType(stringData) == "VCARD"
}
override suspend fun createIntent(context: Context, data: ByteArray): Intent? {
override suspend fun createIntent(context: Context, data: ByteArray): Intent {
val info = VTypeParser.parseMap(String(data))
return Intent(Intent.ACTION_INSERT_OR_EDIT).apply {
@@ -50,7 +59,7 @@ object VCardAction : IntentAction() {
}
putExtra(
extraPhone,
phoneProperty.value.toLowerCase(Locale.US).removePrefix("tel:")
phoneProperty.value.lowercase(Locale.US).removePrefix("tel:")
)
}
info["EMAIL"]?.forEachIndexed { index, mailProperty ->
@@ -142,7 +151,7 @@ object VCardAction : IntentAction() {
} ?: this
private val String.mailType: Int?
get() = when (this.toUpperCase(Locale.US)) {
get() = when (this.uppercase(Locale.US)) {
"HOME" -> ContactsContract.CommonDataKinds.Email.TYPE_HOME
"WORK" -> ContactsContract.CommonDataKinds.Email.TYPE_WORK
"OTHER" -> ContactsContract.CommonDataKinds.Email.TYPE_OTHER
@@ -151,7 +160,7 @@ object VCardAction : IntentAction() {
}
private val String.phoneType: Int?
get() = when (this.toUpperCase(Locale.US)) {
get() = when (this.uppercase(Locale.US)) {
"HOME" -> ContactsContract.CommonDataKinds.Phone.TYPE_HOME
"MOBILE", "CELL" -> ContactsContract.CommonDataKinds.Phone.TYPE_MOBILE
"WORK" -> ContactsContract.CommonDataKinds.Phone.TYPE_WORK
@@ -45,18 +45,25 @@ object VEventAction : IntentAction() {
}
info["DTSTART"]?.singleOrNull()?.also { eventStart ->
dateFormats.simpleFindParse(eventStart.value)?.also {
putExtra(CalendarContract.EXTRA_EVENT_BEGIN_TIME, it.time)
putExtra(
CalendarContract.EXTRA_EVENT_BEGIN_TIME,
it.localTime()
)
}
}
info["DTEND"]?.singleOrNull()?.also { eventEnd ->
dateFormats.simpleFindParse(eventEnd.value)?.also {
putExtra(CalendarContract.EXTRA_EVENT_END_TIME, it.time)
putExtra(
CalendarContract.EXTRA_EVENT_END_TIME,
it.localTime()
)
}
}
}
}
@SuppressLint("SimpleDateFormat") // we definitely don't wan't the local format
// We definitely don't wan't the local format.
@SuppressLint("SimpleDateFormat")
private val dateFormats = listOf(
SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"),
SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss"),
@@ -79,3 +86,6 @@ object VEventAction : IntentAction() {
null
}
}
private fun Date.localTime() = time - timezoneOffset * 60000L
@@ -1,11 +1,27 @@
package de.markusfisch.android.binaryeye.actions.web
import android.content.Context
import de.markusfisch.android.binaryeye.R
import de.markusfisch.android.binaryeye.actions.SchemeAction
import de.markusfisch.android.binaryeye.actions.IAction
import de.markusfisch.android.binaryeye.content.openUrl
object WebAction : IAction {
private val colloquialRegex = """^(http[s]*://)*([a-z0-9]+[a-z0-9-]*[a-z0-9]+[.])+[a-z]{2,}([?#/]+[\w/=&;._-]*)*$""".toRegex(
RegexOption.IGNORE_CASE
)
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
override fun canExecuteOn(data: ByteArray): Boolean {
return String(data).trim().matches(colloquialRegex)
}
override suspend fun execute(context: Context, data: ByteArray) {
var url = String(data).trim()
if (!url.startsWith("http") && !url.startsWith("ftp")) {
url = "http://${url}"
}
context.openUrl(url)
}
}
@@ -10,6 +10,12 @@ import android.support.annotation.RequiresApi
import de.markusfisch.android.binaryeye.R
import java.util.*
@RequiresApi(Build.VERSION_CODES.Q)
fun removeNetworkSuggestions(context: Context): Int =
(context.applicationContext.getSystemService(
Context.WIFI_SERVICE
) as WifiManager).removeNetworkSuggestions(listOf())
/**
* Normal:
* WIFI:S:[network SSID];T:<WPA|WEP|nopass|>;P:[network password];H:<true|false|>;;
@@ -45,7 +51,7 @@ object WifiConnector {
invoke(parsedData.password)
}
return if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
// WifiConfiguration is deprecated in Android Q
// WifiConfiguration is deprecated in Android Q.
@Suppress("DEPRECATION")
WifiConfiguration().apply(parsedData)
} else {
@@ -58,7 +64,7 @@ object WifiConnector {
Context.WIFI_SERVICE
) as WifiManager
return if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
// WifiConfiguration is deprecated in Android Q
// WifiConfiguration is deprecated in Android Q.
@Suppress("DEPRECATION")
val wifiConfig = config as WifiConfiguration
if (wifiManager.enableWifi() &&
@@ -72,7 +78,7 @@ object WifiConnector {
} else {
val suggestion = (config as WifiNetworkSuggestion.Builder).build()
val suggestions = listOf(suggestion)
// remove previous conflicting network suggestion
// Remove previous conflicting network suggestion.
wifiManager.removeNetworkSuggestions(suggestions)
val result = wifiManager.addNetworkSuggestions(suggestions)
if (result == WifiManager.STATUS_NETWORK_SUGGESTIONS_SUCCESS) {
@@ -84,17 +90,17 @@ object WifiConnector {
}
internal fun parseMap(string: String): Map<String, String>? {
// normally those codes should have the last semicolon, but many
// generators don't add it
// Normally those codes should have the last semicolon, but many
// generators don't add it.
val wifiRegex = """^WIFI:((?:.+?:(?:[^\\;]|\\.)*;)+);?$""".toRegex()
// should be: ^(.+):((?:[^\\;,":]|\\.)*);$ but allows unescaped , "
// and : because many QR Code creators don't escape properly
// Should be: ^(.+):((?:[^\\;,":]|\\.)*);$ but allows unescaped , "
// and : because many QR Code creators don't escape properly.
val pairRegex = """(.+?):((?:[^\\;]|\\.)*);""".toRegex()
return wifiRegex.matchEntire(
string
)?.groupValues?.get(1)?.let { pairs ->
pairRegex.findAll(pairs).map { pair ->
pair.groupValues[1].toUpperCase(Locale.US) to pair.groupValues[2]
pair.groupValues[1].uppercase(Locale.US) to pair.groupValues[2]
}.toMap()
}
}
@@ -102,7 +108,7 @@ object WifiConnector {
internal class SimpleDataAccessor private constructor(
private val inputMap: Map<String, String>
) {
// this should be private but because of testing it isn't possible
// This should be private but because of testing it isn't possible.
internal val ssid: String
get() = inputMap.getValue("S").unescape
internal val securityType: String
@@ -112,7 +118,7 @@ object WifiConnector {
internal val hidden: Boolean
get() = inputMap["H"]?.unescape == "true"
internal val anonymousIdentity: String
get() = inputMap["AI"]?.unescape ?: ""
get() = inputMap["AI"]?.unescape ?: inputMap["A"]?.unescape ?: ""
internal val identity: String
get() = inputMap["I"]?.unescape ?: ""
internal val eapMethod: Int?
@@ -164,7 +170,7 @@ object WifiConnector {
): WifiNetworkSuggestion.Builder? {
fun WifiNetworkSuggestion.Builder.applyCommon(
data: SimpleDataAccessor
): WifiNetworkSuggestion.Builder? {
): WifiNetworkSuggestion.Builder {
setSsid(data.ssid)
return this
}
@@ -172,47 +178,44 @@ object WifiConnector {
fun WifiNetworkSuggestion.Builder.applySecurity(
data: SimpleDataAccessor
): WifiNetworkSuggestion.Builder? {
when (data.securityType) {
"WPA", "WPA2" -> {
data.password?.let {
setWpa2Passphrase(it)
try {
when (data.securityType) {
"WPA", "WPA2" -> {
data.password?.let { setWpa2Passphrase(it) }
}
"WPA2-EAP" -> {
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" -> {
setWpa3EnterpriseConfig(WifiEnterpriseConfig().apply {
identity = data.identity
anonymousIdentity = data.anonymousIdentity
password = data.password
data.eapMethod?.let { eapMethod = it }
data.phase2Method?.let { phase2Method = 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 }
})
}
} catch (e: IllegalArgumentException) {
// Don't accept passphrases that aren't ASCII encodeable.
return null
}
return this
}
return this.applyCommon(data)?.applySecurity(data)
return this.applyCommon(data).applySecurity(data)
}
// WifiConfiguration is deprecated in Android Q
// WifiConfiguration is deprecated in Android Q.
@Suppress("DEPRECATION")
private fun WifiConfiguration.apply(
data: SimpleDataAccessor
@@ -222,7 +225,7 @@ object WifiConnector {
fun WifiConfiguration.applyCommon(
data: SimpleDataAccessor
): WifiConfiguration? {
): WifiConfiguration {
allowedAuthAlgorithms.clear()
allowedGroupCiphers.clear()
allowedKeyManagement.clear()
@@ -274,7 +277,7 @@ object WifiConnector {
allowedAuthAlgorithms.set(WifiConfiguration.AuthAlgorithm.OPEN)
allowedProtocols.set(WifiConfiguration.Protocol.RSN) // WPA2
allowedKeyManagement.set(WifiConfiguration.KeyMgmt.WPA_EAP)
@Suppress("DEPRECATION") // TKIP is insecure and has bad performance
@Suppress("DEPRECATION") // TKIP is insecure and has bad performance.
allowedPairwiseCiphers.set(WifiConfiguration.PairwiseCipher.TKIP)
allowedPairwiseCiphers.set(WifiConfiguration.PairwiseCipher.CCMP)
allowedGroupCiphers.set(WifiConfiguration.GroupCipher.TKIP)
@@ -292,7 +295,7 @@ object WifiConnector {
return this
}
return this.applyCommon(data)?.applySecurity(data)
return this.applyCommon(data).applySecurity(data)
}
private inline fun <T> requireSdk(version: Int, block: () -> T): T? {
@@ -300,8 +303,8 @@ object WifiConnector {
}
}
// keep possibility of wrongly unescaped \ by explicitly searching
// for special chars
// Keep possibility of wrongly unescaped \ by explicitly searching
// for special chars.
private val escapedRegex = """\\([\\;,":])""".toRegex()
private val String.unescape: String
get() = this.replace(escapedRegex) { escaped ->
@@ -327,12 +330,12 @@ private fun String.isHex() = length == 64 && matches(hexRegex)
private fun WifiManager.enableWifi(): Boolean {
// setWifiEnabled() will always return false for Android Q
// because Q doesn't allow apps to enable/disable Wi-Fi anymore
// because Q doesn't allow apps to enable/disable Wi-Fi anymore.
@Suppress("DEPRECATION")
return isWifiEnabled || setWifiEnabled(true)
}
// WifiConfiguration is deprecated in Android Q
// WifiConfiguration is deprecated in Android Q.
@Suppress("DEPRECATION")
private fun WifiManager.removeOldNetwork(
wifiConfig: WifiConfiguration
@@ -345,13 +348,13 @@ private fun WifiManager.removeOldNetwork(
removeNetwork(it)
}
} catch (e: SecurityException) {
// the user didn't allow ACCESS_FINE_LOCATION which is
// required to access configuredNetworks and that's fine
// The user didn't allow ACCESS_FINE_LOCATION which is
// required to access configuredNetworks and that's fine.
}
return true
}
// WifiConfiguration is deprecated in Android Q
// WifiConfiguration is deprecated in Android Q.
@Suppress("DEPRECATION")
private fun WifiManager.enableNewNetwork(
wifiConfig: WifiConfiguration
@@ -1,12 +1,16 @@
package de.markusfisch.android.binaryeye.activity
import android.app.Activity
import android.app.AlertDialog
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.graphics.Matrix
import android.graphics.Rect
import android.graphics.RectF
import android.hardware.Camera
import android.media.AudioManager
import android.media.ToneGenerator
import android.net.Uri
import android.os.Bundle
import android.os.Vibrator
@@ -14,6 +18,7 @@ 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
@@ -21,14 +26,21 @@ import android.view.View
import android.widget.SeekBar
import com.google.zxing.Result
import com.google.zxing.ResultMetadataType
import com.google.zxing.ResultPointCallback
import de.markusfisch.android.binaryeye.R
import de.markusfisch.android.binaryeye.adapter.prettifyFormatName
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.content.copyToClipboard
import de.markusfisch.android.binaryeye.content.execShareIntent
import de.markusfisch.android.binaryeye.content.openUrl
import de.markusfisch.android.binaryeye.database.toScan
import de.markusfisch.android.binaryeye.graphics.getFrameToViewMatrix
import de.markusfisch.android.binaryeye.graphics.map
import de.markusfisch.android.binaryeye.net.sendAsync
import de.markusfisch.android.binaryeye.os.error
import de.markusfisch.android.binaryeye.os.getVibrator
import de.markusfisch.android.binaryeye.os.vibrate
import de.markusfisch.android.binaryeye.rs.Preprocessor
import de.markusfisch.android.binaryeye.view.initSystemBars
import de.markusfisch.android.binaryeye.view.setPaddingFromWindowInsets
import de.markusfisch.android.binaryeye.widget.DetectorView
import de.markusfisch.android.binaryeye.widget.toast
@@ -39,16 +51,20 @@ import kotlin.math.min
import kotlin.math.roundToInt
class CameraActivity : AppCompatActivity() {
private val zxing = Zxing(ResultPointCallback { point ->
point?.let {
getMapping()?.map(it)?.let {
detectorView.post {
detectorView.mark(listOf(it))
}
}
private val zxing = Zxing { point ->
point ?: return@Zxing
val matrix = getMappingMatrix() ?: return@Zxing
detectorView.post {
detectorView.update(
matrix.map(
arrayOf(point),
detectorView.coordinates
)
)
}
})
}
private lateinit var rs: RenderScript
private lateinit var vibrator: Vibrator
private lateinit var cameraView: CameraView
private lateinit var detectorView: DetectorView
@@ -56,16 +72,18 @@ class CameraActivity : AppCompatActivity() {
private lateinit var flashFab: FloatingActionButton
private var preprocessor: Preprocessor? = null
private var nativeMapping: Mapping? = null
private var rotatedMapping: Mapping? = null
private var nativeMappingMatrix: Matrix? = null
private var rotatedMappingMatrix: Matrix? = 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 returnUrlTemplate: String? = null
private var finishAfterShowingResult = false
private var frontFacing = false
private var bulkMode = false
private var bulkMode = prefs.bulkMode
private var restrictFormat: String? = null
private var ignoreNext: String? = null
private var fallbackBuffer: IntArray? = null
@@ -101,11 +119,21 @@ class CameraActivity : AppCompatActivity() {
}
}
override fun attachBaseContext(base: Context?) {
base?.applyLocale(prefs.customLocale)
super.attachBaseContext(base)
}
override fun onCreate(state: Bundle?) {
super.onCreate(state)
setContentView(R.layout.activity_camera)
vibrator = getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
// Necessary to get the right translation after setting a
// custom locale.
setTitle(R.string.scan_code)
rs = RenderScript.create(this)
vibrator = getVibrator()
initSystemBars(this)
setSupportActionBar(findViewById(R.id.toolbar) as Toolbar)
@@ -131,19 +159,52 @@ class CameraActivity : AppCompatActivity() {
resetPreProcessor()
fallbackBuffer = null
saveZoom()
saveCropHandlePos()
detectorView.saveCropHandlePos()
rs.destroy()
}
override fun onResume() {
super.onResume()
System.gc()
zxing.updateHints(prefs.tryHarder)
returnResult = "com.google.zxing.client.android.SCAN" == intent.action
if (hasCameraPermission(this)) {
updateHints()
if (prefs.bulkMode && bulkMode != prefs.bulkMode) {
bulkMode = prefs.bulkMode
invalidateOptionsMenu()
ignoreNext = null
}
setReturnTarget(intent)
if (hasCameraPermission()) {
openCamera()
}
}
private fun updateHints() {
val restriction = restrictFormat
val formats = if (restriction != null) {
title = getString(
R.string.scan_format,
prettifyFormatName(restriction)
)
setOf(restriction)
} else {
setTitle(R.string.scan_code)
prefs.barcodeFormats
}
zxing.updateHints(prefs.tryHarder, formats)
}
private fun setReturnTarget(intent: Intent?) {
when {
intent?.action == "com.google.zxing.client.android.SCAN" -> {
returnResult = true
}
intent?.dataString?.isReturnUrl() == true -> {
finishAfterShowingResult = true
returnUrlTemplate = intent.data?.getQueryParameter("ret")
}
}
}
private fun resetPreProcessor() {
preprocessor?.destroy()
preprocessor = null
@@ -169,8 +230,6 @@ class CameraActivity : AppCompatActivity() {
private fun closeCamera() {
cameraView.close()
// closing the camera will also shut off the flash
flash = false
}
override fun onRestoreInstanceState(savedState: Bundle) {
@@ -179,6 +238,7 @@ class CameraActivity : AppCompatActivity() {
zoomBar.progress = savedState.getInt(ZOOM_LEVEL)
frontFacing = savedState.getBoolean(FRONT_FACING)
bulkMode = savedState.getBoolean(BULK_MODE)
restrictFormat = savedState.getString(RESTRICT_FORMAT)
}
override fun onSaveInstanceState(outState: Bundle) {
@@ -186,12 +246,13 @@ class CameraActivity : AppCompatActivity() {
outState.putInt(ZOOM_LEVEL, zoomBar.progress)
outState.putBoolean(FRONT_FACING, frontFacing)
outState.putBoolean(BULK_MODE, bulkMode)
outState.putString(RESTRICT_FORMAT, restrictFormat)
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
// Always give crop handle precedence over other controls
// because it can easily overlap and would then be inaccessible.
if (detectorView.onTouchEvent(ev)) {
return true
}
@@ -215,11 +276,11 @@ class CameraActivity : AppCompatActivity() {
true
}
R.id.pick_file -> {
val chooseFile = Intent(Intent.ACTION_GET_CONTENT)
chooseFile.type = "image/*"
startActivityForResult(
Intent.createChooser(
chooseFile,
Intent(Intent.ACTION_GET_CONTENT).apply {
type = "image/*"
},
getString(R.string.pick_file)
),
PICK_FILE_RESULT_CODE
@@ -233,6 +294,11 @@ class CameraActivity : AppCompatActivity() {
R.id.bulk_mode -> {
bulkMode = bulkMode xor true
item.isChecked = bulkMode
ignoreNext = null
true
}
R.id.restrict_format -> {
showRestrictionDialog()
true
}
R.id.preferences -> {
@@ -257,16 +323,33 @@ class CameraActivity : AppCompatActivity() {
openCamera()
}
private fun showRestrictionDialog() {
val names = resources.getStringArray(
R.array.barcode_formats_names
).toMutableList()
val formats = resources.getStringArray(
R.array.barcode_formats_values
).toMutableList()
if (restrictFormat != null) {
names.add(0, getString(R.string.remove_restriction))
formats.add(0, null)
}
AlertDialog.Builder(this).apply {
setTitle(R.string.restrict_format)
setItems(names.toTypedArray()) { _, which ->
restrictFormat = formats[which]
updateHints()
}
show()
}
}
private fun openReadme() {
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)
}
execShareIntent(intent)
}
private fun handleSendText(intent: Intent) {
@@ -308,8 +391,8 @@ class CameraActivity : AppCompatActivity() {
}
}
MotionEvent.ACTION_UP -> {
// stop calling focusTo() as soon as it returns false
// to avoid throwing and catching future exceptions
// Stop calling focusTo() as soon as it returns false
// to avoid throwing and catching future exceptions.
if (focus) {
focus = cameraView.focusTo(v, event.x, event.y)
if (focus) {
@@ -327,7 +410,7 @@ class CameraActivity : AppCompatActivity() {
override fun onConfigureParameters(
parameters: Camera.Parameters
) {
if (parameters.isZoomSupported) {
zoomBar.visibility = if (parameters.isZoomSupported) {
val max = parameters.maxZoom
if (zoomBar.max != max) {
zoomBar.max = max
@@ -335,8 +418,9 @@ class CameraActivity : AppCompatActivity() {
saveZoom()
}
parameters.zoom = zoomBar.progress
View.VISIBLE
} else {
zoomBar.visibility = View.GONE
View.GONE
}
val sceneModes = parameters.supportedSceneModes
sceneModes?.let {
@@ -357,9 +441,9 @@ class CameraActivity : AppCompatActivity() {
}
override fun onCameraReady(camera: Camera) {
// reset preprocessor to make sure it always fits the current
// frame orientation; important for landscape to landscape
// orientation changes
// 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
@@ -400,7 +484,7 @@ class CameraActivity : AppCompatActivity() {
progress: Int,
fromUser: Boolean
) {
setZoom(progress)
cameraView.camera?.setZoom(progress)
}
override fun onStartTrackingTouch(seekBar: SeekBar) {}
@@ -411,16 +495,13 @@ class CameraActivity : AppCompatActivity() {
}
@Suppress("DEPRECATION")
private fun setZoom(zoom: Int) {
val camera: Camera? = cameraView.camera
camera?.let {
try {
val params = camera.parameters
params.zoom = zoom
camera.parameters = params
} catch (e: RuntimeException) {
// ignore; there's nothing we can do
}
private fun Camera.setZoom(zoom: Int) {
try {
val params = parameters
params.zoom = zoom
parameters = params
} catch (e: RuntimeException) {
// Ignore. There's nothing we can do.
}
}
@@ -448,22 +529,7 @@ class CameraActivity : AppCompatActivity() {
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
)
detectorView.restoreCropHandlePos()
}
private fun updateFlashFab(unavailable: Boolean) {
@@ -478,20 +544,19 @@ class CameraActivity : AppCompatActivity() {
@Suppress("DEPRECATION")
private fun toggleTorchMode() {
val camera = cameraView.camera
val parameters = camera?.parameters
parameters?.flashMode = if (flash) {
val camera = cameraView.camera ?: return
val parameters = camera.parameters ?: return
parameters.flashMode = if (
parameters.flashMode != Camera.Parameters.FLASH_MODE_OFF
) {
Camera.Parameters.FLASH_MODE_OFF
} else {
Camera.Parameters.FLASH_MODE_TORCH
}
parameters?.let {
try {
camera.parameters = parameters
flash = flash xor true
} catch (e: RuntimeException) {
// ignore; there's nothing we can do
}
try {
camera.parameters = parameters
} catch (e: RuntimeException) {
toast(e.message ?: getString(R.string.error_flash))
}
}
@@ -534,10 +599,10 @@ class CameraActivity : AppCompatActivity() {
zxing.decode(frameData, w, h, invert)
} catch (e: RSRuntimeException) {
prefs.forceCompat = prefs.forceCompat xor true
// now the only option is to restart the app because
// Now the only option is to restart the app because
// RenderScript.forceCompat() needs to be called before
// RenderScript is initialized
restartApp(this)
// RenderScript is initialized.
restartApp()
null
}
}
@@ -553,7 +618,7 @@ class CameraActivity : AppCompatActivity() {
frameOrientation
)
val pp = Preprocessor(
this,
rs,
frameWidth,
frameHeight,
frameRoi
@@ -563,13 +628,13 @@ class CameraActivity : AppCompatActivity() {
} else {
cameraView.previewRect
}
nativeMapping = frameToView(
nativeMappingMatrix = getFrameToViewMatrix(
pp.outWidth,
pp.outHeight,
frameOrientation,
viewRect
)
rotatedMapping = frameToView(
rotatedMappingMatrix = getFrameToViewMatrix(
pp.outHeight,
pp.outWidth,
(frameOrientation - 90 + 360) % 360,
@@ -590,14 +655,19 @@ class CameraActivity : AppCompatActivity() {
) {
null
} else {
// map ROI in detectorView to cameraView because cameraView may
// be larger than the detectorView
// 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(
previewRect.left + viewRoi.left,
previewRect.top + viewRoi.top,
previewRect.left + viewRoi.right,
previewRect.top + viewRoi.bottom
previewLeft + viewRoi.left,
previewTop + viewRoi.top,
previewLeft + viewRoi.right,
previewTop + viewRoi.bottom
)
val previewRectWidth = previewRect.width().toFloat()
val previewRectHeight = previewRect.height().toFloat()
@@ -607,52 +677,95 @@ class CameraActivity : AppCompatActivity() {
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()
// Since the ROI is always centered and symmetric, we don't
// need to distinguish between 0 and 180 or 90 and 270 degree.
val errectedRoi = if (isPortrait(frameOrientation)) {
RectF(
normalizedRoi.top,
normalizedRoi.left,
normalizedRoi.bottom,
normalizedRoi.right
)
} else {
Rect(
(normalizedRoi.left * frameWidth.toFloat()).roundToInt(),
(normalizedRoi.top * frameHeight.toFloat()).roundToInt(),
(normalizedRoi.right * frameWidth.toFloat()).roundToInt(),
(normalizedRoi.bottom * frameHeight.toFloat()).roundToInt()
)
normalizedRoi
}
Rect(
(errectedRoi.left * frameWidth.toFloat()).roundToInt(),
(errectedRoi.top * frameHeight.toFloat()).roundToInt(),
(errectedRoi.right * frameWidth.toFloat()).roundToInt(),
(errectedRoi.bottom * frameHeight.toFloat()).roundToInt()
)
}
}
private fun postResult(result: Result) {
// get mapping for the current rotate value
val mapping = getMapping()
// Get matrix for the current rotate value.
val matrix = getMappingMatrix()
val resultPoints = result.resultPoints
if (matrix == null ||
resultPoints == null ||
resultPoints.isEmpty()
) {
return
}
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
detectorView.update(
matrix.map(
resultPoints,
detectorView.coordinates
)
)
vibrator.vibrate()
val returnUri = returnUrlTemplate?.let {
try {
completeUrl(it, result)
} catch (e: Exception) {
e.message?.let { message ->
toast(message)
}
null
}
}
when {
returnResult -> {
setResult(Activity.RESULT_OK, getReturnIntent(result))
finish()
}
returnUri != null -> execShareIntent(
Intent(Intent.ACTION_VIEW, returnUri)
)
else -> {
showResult(
this@CameraActivity,
result,
vibrator,
bulkMode
)
// If this app was invoked via a deep link but without
// a return URI, we probably don't want to return to
// the camera screen after scanning, but to the caller.
if (finishAfterShowingResult) {
finish()
}
}
}
if (bulkMode) {
ignoreNext = result.text
toast(result.text)
if (prefs.showToastInBulkMode) {
toast(result.text)
}
detectorView.postDelayed({
decoding = true
}, 500)
}, prefs.bulkModeDelay.toLong())
}
}
}
private fun getMapping() = if (rotate) rotatedMapping else nativeMapping
private fun getMappingMatrix() = if (rotate) {
rotatedMappingMatrix
} else {
nativeMappingMatrix
}
companion object {
private const val PICK_FILE_RESULT_CODE = 1
@@ -660,24 +773,47 @@ class CameraActivity : AppCompatActivity() {
private const val ZOOM_LEVEL = "zoom_level"
private const val FRONT_FACING = "front_facing"
private const val BULK_MODE = "bulk_mode"
private const val RESTRICT_FORMAT = "restrict_format"
}
}
fun showResult(
activity: Activity,
result: Result,
isResult: Boolean = false,
bulkMode: Boolean = false
vibrator: Vibrator,
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)
val scan = result.toScan()
if (prefs.useHistory) {
scan.id = db.insertScan(scan)
}
if (prefs.sendScanActive && prefs.sendScanUrl.isNotEmpty()) {
if (prefs.sendScanType == "4") {
activity.openUrl(
prefs.sendScanUrl + scan.content.urlEncode()
)
return
}
scan.sendAsync(
prefs.sendScanUrl,
prefs.sendScanType
) { code, body ->
if (code == null || code < 200 || code > 299) {
vibrator.error()
if (!activity.isSilent()) {
beepBeepBeep()
}
}
if (body != null && body.isNotEmpty()) {
activity.toast(body)
} else if (code == null || code > 299) {
activity.toast(R.string.background_request_failed)
}
}
}
if (!bulkMode) {
activity.startActivity(
MainActivity.getDecodeIntent(activity, scan)
@@ -696,7 +832,7 @@ fun getReturnIntent(result: Result): Intent {
intent.putExtra("SCAN_RESULT_BYTES", result.rawBytes)
}
result.resultMetadata?.let { metadata ->
metadata[ResultMetadataType.UPC_EAN_EXTENSION]?.let {
metadata[ResultMetadataType.ORIENTATION]?.let {
intent.putExtra(
"SCAN_RESULT_ORIENTATION",
it.toString()
@@ -725,3 +861,43 @@ fun getReturnIntent(result: Result): Intent {
}
return intent
}
private fun String.isReturnUrl() = listOf(
"binaryeye://scan",
"http://markusfisch.de/BinaryEye",
"https://markusfisch.de/BinaryEye"
).firstOrNull { startsWith(it) } != null
private fun completeUrl(urlTemplate: String, result: Result) = Uri.parse(
urlTemplate
.replace("{RESULT}", result.text.urlEncode())
.replace("{RESULT_BYTES}", result.rawBytes?.toHexString() ?: "")
.replace(
"{FORMAT}", result.barcodeFormat.toString().urlEncode()
)
.replace(
"{META}", result.resultMetadata?.toString()?.urlEncode() ?: ""
)
// And support {CODE} from the old ZXing app, too.
.replace("{CODE}", result.text.urlEncode())
)
private fun Context.isSilent(): Boolean {
val am = getSystemService(Context.AUDIO_SERVICE) as AudioManager
return when (am.ringerMode) {
AudioManager.RINGER_MODE_SILENT,
AudioManager.RINGER_MODE_VIBRATE -> true
else -> false
}
}
private fun beepBeepBeep() {
ToneGenerator(AudioManager.STREAM_ALARM, 100).startTone(
ToneGenerator.TONE_CDMA_CONFIRM,
1000
)
}
private fun isPortrait(
orientation: Int
) = orientation == 90 || orientation == 270
@@ -2,23 +2,39 @@ package de.markusfisch.android.binaryeye.activity
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle
import android.support.v4.app.Fragment
import android.support.v7.app.AppCompatActivity
import android.support.v7.widget.Toolbar
import de.markusfisch.android.binaryeye.R
import de.markusfisch.android.binaryeye.app.colorSystemAndToolBars
import de.markusfisch.android.binaryeye.app.initSystemBars
import de.markusfisch.android.binaryeye.app.setFragment
import de.markusfisch.android.binaryeye.app.*
import de.markusfisch.android.binaryeye.database.Scan
import de.markusfisch.android.binaryeye.fragment.DecodeFragment
import de.markusfisch.android.binaryeye.fragment.EncodeFragment
import de.markusfisch.android.binaryeye.fragment.HistoryFragment
import de.markusfisch.android.binaryeye.fragment.PreferencesFragment
import de.markusfisch.android.binaryeye.view.colorSystemAndToolBars
import de.markusfisch.android.binaryeye.view.initSystemBars
import de.markusfisch.android.binaryeye.view.recordToolbarHeight
class MainActivity : AppCompatActivity() {
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<String>,
grantResults: IntArray
) {
when (requestCode) {
PERMISSION_LOCATION, PERMISSION_WRITE -> if (grantResults.isNotEmpty() &&
grantResults[0] == PackageManager.PERMISSION_GRANTED
) {
permissionGrantedCallback?.invoke()
permissionGrantedCallback = null
}
}
}
override fun onSupportNavigateUp(): Boolean {
val fm = supportFragmentManager
if (fm != null && fm.backStackEntryCount > 0) {
@@ -29,6 +45,11 @@ class MainActivity : AppCompatActivity() {
return true
}
override fun attachBaseContext(base: Context?) {
base?.applyLocale(prefs.customLocale)
super.attachBaseContext(base)
}
override fun onCreate(state: Bundle?) {
super.onCreate(state)
setContentView(R.layout.activity_main)
@@ -44,7 +65,7 @@ class MainActivity : AppCompatActivity() {
}
if (state == null) {
setFragment(supportFragmentManager, getFragmentForIntent(intent))
supportFragmentManager?.setFragment(getFragmentForIntent(intent))
}
}
@@ -52,7 +73,7 @@ class MainActivity : AppCompatActivity() {
private const val PREFERENCES = "preferences"
private const val HISTORY = "history"
private const val ENCODE = "encode"
private const val DECODED = "decoded"
const val DECODED = "decoded"
private fun getFragmentForIntent(intent: Intent?): Fragment {
intent ?: return PreferencesFragment()
@@ -4,41 +4,68 @@ 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.colorSystemAndToolBars
import de.markusfisch.android.binaryeye.app.initSystemBars
import de.markusfisch.android.binaryeye.app.vibrate
import de.markusfisch.android.binaryeye.app.applyLocale
import de.markusfisch.android.binaryeye.app.prefs
import de.markusfisch.android.binaryeye.graphics.crop
import de.markusfisch.android.binaryeye.graphics.loadImageUri
import de.markusfisch.android.binaryeye.graphics.mapResult
import de.markusfisch.android.binaryeye.view.doOnApplyWindowInsets
import de.markusfisch.android.binaryeye.graphics.mapResultToView
import de.markusfisch.android.binaryeye.os.getVibrator
import de.markusfisch.android.binaryeye.os.vibrate
import de.markusfisch.android.binaryeye.rs.fixTransparency
import de.markusfisch.android.binaryeye.view.colorSystemAndToolBars
import de.markusfisch.android.binaryeye.view.initSystemBars
import de.markusfisch.android.binaryeye.view.recordToolbarHeight
import de.markusfisch.android.binaryeye.view.setPaddingFromWindowInsets
import de.markusfisch.android.binaryeye.widget.CropImageView
import de.markusfisch.android.binaryeye.widget.DetectorView
import de.markusfisch.android.binaryeye.widget.toast
import de.markusfisch.android.binaryeye.zxing.Zxing
import de.markusfisch.android.scalingimageview.widget.ScalingImageView
import kotlinx.coroutines.*
class PickActivity : AppCompatActivity() {
private val zxing = Zxing()
private val parentJob = Job()
private val scope = CoroutineScope(Dispatchers.IO + parentJob)
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)
zxing.updateHints(true)
vibrator = getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
// Necessary to get the right translation after setting a custom
// locale.
setTitle(R.string.pick_code_to_scan)
zxing.updateHints(
true,
prefs.barcodeFormats
)
rs = RenderScript.create(this)
vibrator = getVibrator()
initSystemBars(this)
val toolbar = findViewById(R.id.toolbar) as Toolbar
@@ -49,68 +76,95 @@ class PickActivity : AppCompatActivity() {
colorSystemAndToolBars(this@PickActivity)
}
val bitmap = if (
intent?.action == Intent.ACTION_SEND &&
intent.type?.startsWith("image/") == true
) {
loadSentImage(intent)
} else if (
intent?.action == Intent.ACTION_VIEW &&
intent.type?.startsWith("image/") == true
) {
loadImageToOpen(intent)
} else {
null
}
val bitmap = fixTransparency(rs, getBitmapFromIntent())
if (bitmap == null) {
applicationContext.toast(R.string.error_no_content)
finish()
return
}
var result: Result? = null
val scope = CoroutineScope(Dispatchers.IO)
val scannedRect = Rect()
fun scanWithinBounds() = crop(
bitmap,
cropImageView.normalizedRectInBounds,
cropImageView.imageRotation
)?.let {
scannedRect.set(0, 0, it.width, it.height)
scope.launch(Dispatchers.IO) {
result = zxing.decodePositiveNegative(it)
result?.let {
withContext(Dispatchers.Main) {
if (!isFinishing) {
vibrator.vibrate()
}
cropImageView.updateResultPoints(
mapResult(
scannedRect.width(),
scannedRect.height(),
0,
cropImageView.getBoundsRect(),
it
)
)
cropImageView = findViewById(R.id.image) as CropImageView
cropImageView.restrictTranslation = false
cropImageView.freeRotation = true
cropImageView.setImageBitmap(bitmap)
cropImageView.onScan = {
scanWithinBounds(bitmap)
}
detectorView = findViewById(R.id.detector_view) as DetectorView
detectorView.onRoiChanged = {
scanWithinBounds(bitmap)
}
detectorView.setPaddingFromWindowInsets()
detectorView.restoreCropHandlePos()
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 = Rect(detectorView.roi)
val rectInView = if (roi.width() < 1) {
cropImageView.getBoundsRect()
} else {
roi
}
val imageRect = cropImageView.mappedRect
val rectInImage = normalizeRoi(imageRect, rectInView)
val cropped = bitmap.crop(
rectInImage,
cropImageView.imageRotation,
cropImageView.pivotX,
cropImageView.pivotY
) ?: return
scope.launch {
result = zxing.decodePositiveNegative(cropped)
result?.let {
withContext(Dispatchers.Main) {
if (isFinishing) {
return@withContext
}
vibrator.vibrate()
val rc = Rect()
imageRect.round(rc)
if (!rectInView.intersect(rc)) {
return@withContext
}
detectorView.update(
mapResultToView(
cropped.width,
cropped.height,
0,
rectInView,
it.resultPoints,
detectorView.coordinates
)
)
}
}
}
}
cropImageView = findViewById(R.id.image) as CropImageView
cropImageView.setImageBitmap(bitmap)
cropImageView.onScan = {
scanWithinBounds()
}
cropImageView.doOnApplyWindowInsets { v, insets ->
(v as CropImageView).windowInsets.set(insets)
}
findViewById(R.id.scan).setOnClickListener {
scanImage(result)
}
override fun onDestroy() {
super.onDestroy()
detectorView.saveCropHandlePos()
rs.destroy()
parentJob.cancel()
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
@@ -136,20 +190,32 @@ class PickActivity : AppCompatActivity() {
val uri = intent.getParcelableExtra<Parcelable>(
Intent.EXTRA_STREAM
) as? Uri ?: return null
return loadImageUri(contentResolver, uri)
return contentResolver.loadImageUri(uri)
}
private fun loadImageToOpen(intent: Intent): Bitmap? {
val uri = intent.data ?: return null
return loadImageUri(contentResolver, uri)
return contentResolver.loadImageUri(uri)
}
private fun scanImage(result: Result?) {
if (result != null) {
showResult(this, result)
private fun showResult() {
val r = result
if (r != null) {
showResult(this, r, vibrator)
finish()
return
} else {
applicationContext.toast(R.string.no_barcode_found)
}
applicationContext.toast(R.string.no_barcode_found)
}
}
private fun normalizeRoi(imageRect: RectF, roi: Rect): RectF {
val w = imageRect.width()
val h = imageRect.height()
return RectF(
(roi.left - imageRect.left) / w,
(roi.top - imageRect.top) / h,
(roi.right - imageRect.left) / w,
(roi.bottom - imageRect.top) / h
)
}
@@ -9,11 +9,11 @@ class SplashActivity : AppCompatActivity() {
override fun onCreate(state: Bundle?) {
super.onCreate(state)
// it's important _not_ to inflate a layout file here
// It's important _not_ to inflate a layout file here
// because that would happen after the app is fully
// initialized what is too late
// initialized what is too late.
setRestartCount(intent)
intent?.setRestartCount()
startActivity(Intent(applicationContext, CameraActivity::class.java))
finish()
}
@@ -9,6 +9,9 @@ import android.widget.CursorAdapter
import android.widget.TextView
import de.markusfisch.android.binaryeye.R
import de.markusfisch.android.binaryeye.database.Database
import java.text.DateFormat
import java.text.SimpleDateFormat
import java.util.*
class ScansAdapter(context: Context, cursor: Cursor) :
CursorAdapter(context, cursor, false) {
@@ -55,38 +58,41 @@ class ScansAdapter(context: Context, cursor: Cursor) :
context: Context,
cursor: Cursor
) {
val holder = getViewHolder(view)
holder.timeView.text = cursor.getString(timeIndex)
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
getViewHolder(view).apply {
timeView.text = formatDateTime(cursor.getString(timeIndex))
val name = cursor.getString(nameIndex)
val content = cursor.getString(contentIndex)
var icon = 0
contentView.text = when {
name?.isNotEmpty() == true -> {
icon = R.drawable.ic_label
name
}
content?.isEmpty() == true -> context.getString(
R.string.binary_data
)
else -> content
}
content?.isEmpty() == true -> context.getString(R.string.binary_data)
else -> content
contentView.setCompoundDrawablesWithIntrinsicBounds(
icon, 0, 0, 0
)
formatView.text = prettifyFormatName(cursor.getString(formatIndex))
}
holder.contentView.setCompoundDrawablesWithIntrinsicBounds(
icon, 0, 0, 0
)
holder.formatView.text = cursor.getString(formatIndex)
// view.isSelected needs to be put on the queue to work
// 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 {
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
}
private fun getViewHolder(
view: View
): ViewHolder = view.tag as ViewHolder? ?: ViewHolder(
view.findViewById(R.id.time),
view.findViewById(R.id.content),
view.findViewById(R.id.format)
).also {
view.tag = it
}
private data class ViewHolder(
@@ -95,3 +101,15 @@ class ScansAdapter(context: Context, cursor: Cursor) :
val formatView: TextView
)
}
fun prettifyFormatName(format: String) = format.replace("_", " ")
private fun formatDateTime(rfc: String): String {
SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.US).parse(rfc)?.let {
return DateFormat.getDateTimeInstance(
DateFormat.LONG,
DateFormat.SHORT
).format(it)
}
return rfc
}
@@ -1,7 +1,7 @@
package de.markusfisch.android.binaryeye.app
import android.content.Context
import android.support.v7.app.AlertDialog
import android.app.AlertDialog
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlin.coroutines.resume
@@ -11,7 +11,7 @@ suspend inline fun <T : Any> alertDialog(
context: Context,
crossinline build: AlertDialog.Builder.(resume: (T?) -> Unit) -> Unit
): T? = withContext(Dispatchers.Main) {
suspendCoroutine<T?> { continuation ->
suspendCoroutine { continuation ->
AlertDialog.Builder(context).apply {
setOnCancelListener {
continuation.resume(null)
@@ -1,6 +1,6 @@
package de.markusfisch.android.binaryeye.app
import android.app.Application
import android.support.multidex.MultiDexApplication;
import android.support.v8.renderscript.RenderScript
import de.markusfisch.android.binaryeye.database.Database
import de.markusfisch.android.binaryeye.preference.Preferences
@@ -8,7 +8,7 @@ import de.markusfisch.android.binaryeye.preference.Preferences
val db = Database()
val prefs = Preferences()
class BinaryEyeApp : Application() {
class BinaryEyeApp : MultiDexApplication() {
override fun onCreate() {
super.onCreate()
db.open(this)
@@ -1,20 +0,0 @@
package de.markusfisch.android.binaryeye.app
import android.annotation.SuppressLint
import android.support.v4.app.Fragment
import android.support.v4.app.FragmentManager
import de.markusfisch.android.binaryeye.R
fun setFragment(fm: FragmentManager?, fragment: Fragment) {
fm?.let { getTransaction(fm, fragment).commit() }
}
fun addFragment(fm: FragmentManager?, fragment: Fragment) {
fm?.let { getTransaction(fm, fragment).addToBackStack(null).commit() }
}
@SuppressLint("CommitTransaction")
private fun getTransaction(
fm: FragmentManager,
fragment: Fragment
) = fm.beginTransaction().replace(R.id.content_frame, fragment)
@@ -2,13 +2,29 @@ package de.markusfisch.android.binaryeye.app
import android.net.Uri
import android.os.Build
import java.io.UnsupportedEncodingException
import java.net.URLEncoder
private val nonPrintable = "[\\x00-\\x08\\x0e-\\x1f]".toRegex()
fun String.hasNonPrintableCharacters() = nonPrintable.containsMatchIn(this)
fun hasNonPrintableCharacters(s: String) = nonPrintable.containsMatchIn(s)
private val nonAlNum = "[^a-zA-Z0-9]".toRegex()
private val multipleDots = "[…]+".toRegex()
fun String.foldNonAlNum() = replace(nonAlNum, "")
.replace(multipleDots, "")
fun parseAndNormalizeUri(input: String): Uri = Uri.parse(input).let {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) it.normalizeScheme() else it
fun String.urlEncode(): String = try {
URLEncoder.encode(this, "UTF-8")
} catch (e: UnsupportedEncodingException) {
this
}
fun String.parseAndNormalizeUri(): Uri = Uri.parse(this).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,19 @@
package de.markusfisch.android.binaryeye.app
import android.annotation.SuppressLint
import android.support.v4.app.Fragment
import android.support.v4.app.FragmentManager
import de.markusfisch.android.binaryeye.R
fun FragmentManager.setFragment(fragment: Fragment) {
getTransaction(fragment).commit()
}
fun FragmentManager.addFragment(fragment: Fragment) {
getTransaction(fragment).addToBackStack(null).commit()
}
@SuppressLint("CommitTransaction")
private fun FragmentManager.getTransaction(
fragment: Fragment
) = beginTransaction().replace(R.id.content_frame, fragment)
@@ -0,0 +1,26 @@
package de.markusfisch.android.binaryeye.app
import android.content.Context
import android.os.Build
import java.util.*
fun Context.applyLocale(localeName: String) {
if (localeName.isEmpty()) {
return
}
val localeParts = localeName.split("-")
val locale = if (localeParts.size == 2) {
Locale(localeParts[0], localeParts[1])
} else {
Locale(localeName)
}
Locale.setDefault(locale)
val conf = resources.configuration
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
@Suppress("DEPRECATION")
conf.locale = locale
} else {
conf.setLocale(locale)
}
resources.updateConfiguration(conf, resources.displayMetrics)
}
@@ -6,44 +6,42 @@ 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): Boolean {
return hasPermission(
activity,
Manifest.permission.CAMERA,
PERMISSION_CAMERA
)
}
fun Activity.hasCameraPermission() = hasPermission(
Manifest.permission.CAMERA,
PERMISSION_CAMERA
)
const val PERMISSION_WRITE = 2
fun hasWritePermission(activity: Activity): Boolean {
fun Activity.hasWritePermission(callback: () -> Any): Boolean {
permissionGrantedCallback = callback
return hasPermission(
activity,
Manifest.permission.WRITE_EXTERNAL_STORAGE,
PERMISSION_WRITE
)
}
const val PERMISSION_LOCATION = 3
fun hasLocationPermission(activity: Activity): Boolean {
fun Activity.hasLocationPermission(callback: () -> Any): Boolean {
permissionGrantedCallback = callback
return hasPermission(
activity,
Manifest.permission.ACCESS_FINE_LOCATION,
PERMISSION_LOCATION
)
}
private fun hasPermission(
activity: Activity,
private fun Activity.hasPermission(
permission: String,
requestCode: Int
) = if (ContextCompat.checkSelfPermission(
activity,
this,
permission
) != PackageManager.PERMISSION_GRANTED
) {
ActivityCompat.requestPermissions(
activity,
this,
arrayOf(permission),
requestCode
)
@@ -8,19 +8,17 @@ private const val RESTART_COUNT = "restart_count"
private var restartCount = 0
fun setRestartCount(intent: Intent?) {
intent?.let {
restartCount = intent.getIntExtra(RESTART_COUNT, restartCount)
}
fun Intent.setRestartCount() {
restartCount = 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()
fun Activity.restartApp() {
if (restartCount < 3) {
startActivity(Intent(this, SplashActivity::class.java).apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
putExtra(RESTART_COUNT, ++restartCount)
})
finish()
}
Runtime.getRuntime().exit(0)
}
@@ -1,52 +0,0 @@
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,24 +0,0 @@
package de.markusfisch.android.binaryeye.app
import android.os.Build
import android.os.VibrationEffect
import android.os.Vibrator
private const val MILLISECONDS = 100L
fun Vibrator.vibrate() {
if (!prefs.vibrate) {
return
}
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
@Suppress("DEPRECATION")
this.vibrate(MILLISECONDS)
} else {
this.vibrate(
VibrationEffect.createOneShot(
MILLISECONDS,
VibrationEffect.DEFAULT_AMPLITUDE
)
)
}
}
@@ -1,4 +1,4 @@
package de.markusfisch.android.binaryeye.app
package de.markusfisch.android.binaryeye.content
import android.content.ClipData
import android.content.Context
@@ -11,7 +11,7 @@ fun Context.copyToClipboard(text: String) {
(service as android.text.ClipboardManager).text = text
} else {
(service as android.content.ClipboardManager).setPrimaryClip(
ClipData.newPlainText("simple text", text)
ClipData.newPlainText("plain text", text)
)
}
}
@@ -0,0 +1,76 @@
package de.markusfisch.android.binaryeye.content
import android.content.ActivityNotFoundException
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.app.parseAndNormalizeUri
import de.markusfisch.android.binaryeye.widget.toast
import java.io.File
fun Context.execShareIntent(intent: Intent): Boolean = if (
!startIntent(intent)
) {
toast(R.string.cannot_resolve_action)
false
} else {
true
}
fun Context.startIntent(intent: Intent): Boolean = try {
// Avoid using `intent.resolveActivity()` at API level 30+ due
// to the new package visibility restrictions. In order for
// `resolveActivity()` to "see" another package, we would need
// to list that package/intent in a `<queries>` block in the
// Manifest. But since we used `resolveActivity()` only to avoid
// an exception if the Intent cannot be resolved, it's much easier
// and more robust to just try and catch that exception if
// necessary.
startActivity(intent)
true
} catch (e: ActivityNotFoundException) {
false
}
fun Context.openUrl(url: String): Boolean = openUri(url.parseAndNormalizeUri())
fun Context.openUri(uri: Uri): Boolean = execShareIntent(
Intent(Intent.ACTION_VIEW, uri)
)
fun Context.shareText(text: String, mimeType: String = "text/plain") {
execShareIntent(Intent(Intent.ACTION_SEND).apply {
putExtra(Intent.EXTRA_TEXT, text)
type = mimeType
})
}
fun Context.shareFile(file: File, mimeType: String) {
getUriForFile(file)?.let {
shareUri(it, mimeType)
}
}
private fun Context.getUriForFile(file: File): Uri? = if (
Build.VERSION.SDK_INT < Build.VERSION_CODES.N
) {
Uri.fromFile(file)
} else {
FileProvider.getUriForFile(
this,
BuildConfig.APPLICATION_ID + ".provider",
file
)
}
private fun Context.shareUri(uri: Uri, mimeType: String) {
execShareIntent(Intent(Intent.ACTION_SEND).apply {
putExtra(Intent.EXTRA_STREAM, uri)
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
type = mimeType
})
}
@@ -3,22 +3,21 @@ package de.markusfisch.android.binaryeye.database
import android.content.Context
import android.database.Cursor
import de.markusfisch.android.binaryeye.app.toHexString
import de.markusfisch.android.binaryeye.app.writeExternalFile
import de.markusfisch.android.binaryeye.io.writeExternalFile
import java.io.ByteArrayOutputStream
import java.io.OutputStream
fun exportCsv(
context: Context,
fun Context.exportCsv(
name: String,
cursor: Cursor,
delimiter: String
) = writeExternalFile(context, name, "text/csv") { outputStream ->
) = writeExternalFile(name, "text/csv") { outputStream ->
exportCsv(outputStream, cursor, delimiter)
}
fun exportCsv(cursor: Cursor, delimiter: String): String? {
fun Cursor.exportCsv(delimiter: String): String {
val outputStream = ByteArrayOutputStream()
exportCsv(outputStream, cursor, delimiter)
exportCsv(outputStream, this, delimiter)
return outputStream.toString()
}
@@ -87,6 +86,8 @@ private fun Cursor.toCsvRecord(
return sb.toString().toByteArray()
}
private fun String.quoteAndEscape() = "\"${this
.replace("\n", " ")
.replace("\"", "\"\"")}\""
private fun String.quoteAndEscape() = "\"${
this
.replace("\n", " ")
.replace("\"", "\"\"")
}\""
@@ -0,0 +1,18 @@
package de.markusfisch.android.binaryeye.database
import android.database.Cursor
fun Cursor.getString(name: String): String {
val idx = getColumnIndex(name)
return if (idx < 0) "" else getString(idx) ?: ""
}
fun Cursor.getBlob(name: String): ByteArray? {
val idx = getColumnIndex(name)
return if (idx < 0) null else getBlob(idx)
}
fun Cursor.getLong(name: String): Long {
val idx = getColumnIndex(name)
return if (idx < 0) 0L else getLong(idx)
}
@@ -5,7 +5,6 @@ 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 {
@@ -25,7 +24,7 @@ class Database {
FROM $SCANS
${getWhereClause(query)}
ORDER BY $SCANS_DATETIME DESC
""", getWhereArguments(query)
""".trimMargin(), getWhereArguments(query)
)
fun getScansDetailed(query: String? = null): Cursor? = db.rawQuery(
@@ -47,7 +46,7 @@ class Database {
FROM $SCANS
${getWhereClause(query)}
ORDER BY $SCANS_DATETIME DESC
""", getWhereArguments(query)
""".trimMargin(), getWhereArguments(query)
)
private fun getWhereClause(
@@ -87,7 +86,7 @@ class Database {
$SCANS_UPC_EAN_EXTENSION
FROM $SCANS
WHERE $SCANS_ID = ?
""", arrayOf("$id")
""".trimMargin(), arrayOf("$id")
)?.use {
if (it.moveToFirst()) {
Scan(
@@ -110,47 +109,48 @@ class Database {
}
}
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
fun insertScan(scan: Scan): Long = db.insert(
SCANS,
null,
ContentValues().apply {
put(SCANS_DATETIME, scan.dateTime)
val isRaw = scan.raw != null
if (isRaw) {
put(SCANS_CONTENT, "")
put(SCANS_RAW, scan.raw)
} else {
put(SCANS_CONTENT, scan.content)
}
put(SCANS_FORMAT, scan.format)
scan.errorCorrectionLevel?.let {
put(SCANS_ERROR_CORRECTION_LEVEL, it)
}
scan.issueNumber?.let { put(SCANS_ISSUE_NUMBER, it) }
scan.orientation?.let { put(SCANS_ORIENTATION, it) }
scan.otherMetaData?.let { put(SCANS_OTHER_META_DATA, it) }
scan.pdf417ExtraMetaData?.let { put(SCANS_PDF417_EXTRA_METADATA, it) }
scan.possibleCountry?.let { put(SCANS_POSSIBLE_COUNTRY, it) }
scan.suggestedPrice?.let { put(SCANS_SUGGESTED_PRICE, it) }
scan.upcEanExtension?.let { put(SCANS_UPC_EAN_EXTENSION, it) }
if (prefs.ignoreConsecutiveDuplicates) {
val id = getIdOfLastScan(
get(SCANS_CONTENT) as String,
if (isRaw) 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
): Long = db.rawQuery(
"""SELECT
$SCANS_ID,
$SCANS_CONTENT,
$SCANS_RAW,
@@ -158,21 +158,20 @@ class Database {
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
}
""".trimMargin(), null
)?.use {
if (it.count > 0 &&
it.moveToFirst() &&
it.getString(SCANS_CONTENT) == content &&
(raw == null || it.getBlob(SCANS_RAW)
?.contentEquals(raw) == true) &&
it.getString(SCANS_FORMAT) == format
) {
it.getLong(SCANS_ID)
} else {
0L
}
} ?: 0L
fun removeScan(id: Long) {
db.delete(SCANS, "$SCANS_ID = ?", arrayOf("$id"))
@@ -230,7 +229,7 @@ class Database {
const val SCANS_UPC_EAN_EXTENSION = "upc_ean_extension"
private fun createScans(db: SQLiteDatabase) {
db.execSQL("DROP TABLE IF EXISTS $SCANS")
db.execSQL("DROP TABLE IF EXISTS $SCANS".trimMargin())
db.execSQL(
"""CREATE TABLE $SCANS (
$SCANS_ID INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -247,39 +246,29 @@ class Database {
$SCANS_POSSIBLE_COUNTRY TEXT,
$SCANS_SUGGESTED_PRICE TEXT,
$SCANS_UPC_EAN_EXTENSION TEXT
)"""
)""".trimMargin()
)
}
private fun addRawColumn(db: SQLiteDatabase) {
db.execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_RAW BLOB")
db.execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_RAW BLOB".trimMargin())
}
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")
db.apply {
execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_ERROR_CORRECTION_LEVEL TEXT".trimMargin())
execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_ISSUE_NUMBER INT".trimMargin())
execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_ORIENTATION INT".trimMargin())
execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_OTHER_META_DATA TEXT".trimMargin())
execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_PDF417_EXTRA_METADATA TEXT".trimMargin())
execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_POSSIBLE_COUNTRY TEXT".trimMargin())
execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_SUGGESTED_PRICE TEXT".trimMargin())
execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_UPC_EAN_EXTENSION TEXT".trimMargin())
}
}
private fun addNameColumn(db: SQLiteDatabase) {
db.execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_NAME TEXT")
db.execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_NAME TEXT".trimMargin())
}
}
}
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)
)
@@ -2,20 +2,19 @@ package de.markusfisch.android.binaryeye.database
import android.app.Activity
import android.os.Environment
import de.markusfisch.android.binaryeye.app.writeExternalFile
import de.markusfisch.android.binaryeye.io.writeExternalFile
import java.io.File
import java.io.FileInputStream
fun exportDatabase(activity: Activity, fileName: String): Boolean {
fun Activity.exportDatabase(fileName: String): Boolean {
val dbFile = File(
Environment.getDataDirectory(),
"//data//${activity.packageName}//databases//${Database.FILE_NAME}"
"//data//${packageName}//databases//${Database.FILE_NAME}"
)
if (!dbFile.exists()) {
return false
}
return writeExternalFile(
activity,
fileName,
"application/vnd.sqlite3"
) {
@@ -3,23 +3,22 @@ package de.markusfisch.android.binaryeye.database
import android.content.Context
import android.database.Cursor
import de.markusfisch.android.binaryeye.app.toHexString
import de.markusfisch.android.binaryeye.app.writeExternalFile
import de.markusfisch.android.binaryeye.io.writeExternalFile
import org.json.JSONArray
import org.json.JSONException
import org.json.JSONObject
fun exportJson(
context: Context,
fun Context.exportJson(
name: String,
cursor: Cursor
) = writeExternalFile(context, name, "application/json") { outputStream ->
exportJson(cursor)?.let {
) = writeExternalFile(name, "application/json") { outputStream ->
cursor.exportJson()?.let {
outputStream.write(it.toByteArray())
}
}
fun exportJson(cursor: Cursor): String? {
if (!cursor.moveToFirst()) {
fun Cursor.exportJson(): String? {
if (!moveToFirst()) {
return null
}
val columns = arrayOf(
@@ -36,21 +35,21 @@ fun exportJson(cursor: Cursor): String? {
Database.SCANS_UPC_EAN_EXTENSION
)
val indices = columns.map {
Pair(cursor.getColumnIndex(it), it)
Pair(getColumnIndex(it), it)
}
val contentIndex = cursor.getColumnIndex(Database.SCANS_CONTENT)
val rawIndex = cursor.getColumnIndex(Database.SCANS_RAW)
val contentIndex = getColumnIndex(Database.SCANS_CONTENT)
val rawIndex = getColumnIndex(Database.SCANS_RAW)
val root = JSONArray()
do {
var deviation: Pair<Int, String>? = null
if (cursor.getString(contentIndex)?.isEmpty() == true) {
if (getString(contentIndex)?.isEmpty() == true) {
deviation = Pair(
contentIndex,
cursor.getBlob(rawIndex).toHexString()
getBlob(rawIndex).toHexString()
)
}
root.put(cursor.toJsonObject(indices, deviation))
} while (cursor.moveToNext())
root.put(toJsonObject(indices, deviation))
} while (moveToNext())
return root.toString()
}
@@ -5,6 +5,7 @@ import android.os.Parcelable
import android.text.format.DateFormat
import com.google.zxing.Result
import com.google.zxing.ResultMetadataType
import de.markusfisch.android.binaryeye.app.hasNonPrintableCharacters
data class Scan(
val content: String,
@@ -18,23 +19,9 @@ data class Scan(
val possibleCountry: String?,
val suggestedPrice: String?,
val upcEanExtension: String?,
val timestamp: String = getDateTime(),
val dateTime: 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 {
@@ -48,7 +35,7 @@ data class Scan(
other as Scan
return id == other.id &&
timestamp == other.timestamp &&
dateTime == other.dateTime &&
content == other.content &&
((raw == null && other.raw == null) ||
(raw != null && other.raw != null && raw.contentEquals(other.raw))) &&
@@ -67,7 +54,7 @@ data class Scan(
// this isn't handled well by Kotlin
override fun hashCode(): Int {
var result = id.hashCode()
result = 31 * result + timestamp.hashCode()
result = 31 * result + dateTime.hashCode()
result = 31 * result + content.hashCode()
result = 31 * result + (raw?.contentHashCode() ?: 0)
result = 31 * result + format.hashCode()
@@ -84,7 +71,7 @@ data class Scan(
private constructor(parcel: Parcel) : this(
content = parcel.readString() ?: "",
raw = readByteArray(parcel),
raw = parcel.readSizedByteArray(),
format = parcel.readString() ?: "",
errorCorrectionLevel = parcel.readString(),
issueNumber = parcel.readString(),
@@ -94,24 +81,26 @@ data class Scan(
possibleCountry = parcel.readString(),
suggestedPrice = parcel.readString(),
upcEanExtension = parcel.readString(),
timestamp = parcel.readString() ?: "",
dateTime = 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)
parcel.apply {
writeString(content)
writeSizedByteArray(raw)
writeString(format)
writeString(errorCorrectionLevel)
writeString(issueNumber)
writeString(orientation)
writeString(otherMetaData)
writeString(pdf417ExtraMetaData)
writeString(possibleCountry)
writeString(suggestedPrice)
writeString(upcEanExtension)
writeString(dateTime)
writeLong(id)
}
}
override fun describeContents() = 0
@@ -130,17 +119,66 @@ private fun getDateTime(time: Long = System.currentTimeMillis()) = DateFormat.fo
time
).toString()
private fun getRawBytes(result: Result): ByteArray? {
val metadata = result.resultMetadata ?: return null
private fun Parcel.writeSizedByteArray(array: ByteArray?) {
val size = array?.size ?: 0
writeInt(size)
if (size > 0) {
writeByteArray(array)
}
}
private fun Parcel.readSizedByteArray(): ByteArray? {
val size = readInt()
return if (size > 0) {
val array = ByteArray(size)
readByteArray(array)
array
} else {
null
}
}
fun Result.toScan(): Scan {
val content: String
val raw: ByteArray?
if (text.hasNonPrintableCharacters()) {
content = ""
// ISO_8859_1 is important to suppress UTF-8 encoding.
raw = getRawData() ?: text.toByteArray(Charsets.ISO_8859_1)
} else {
content = text
raw = null
}
return Scan(
content,
raw,
barcodeFormat.toString(),
getMetaString(ResultMetadataType.ERROR_CORRECTION_LEVEL),
getMetaString(ResultMetadataType.ISSUE_NUMBER),
getMetaString(ResultMetadataType.ORIENTATION),
getMetaString(ResultMetadataType.OTHER),
getMetaString(ResultMetadataType.PDF417_EXTRA_METADATA),
getMetaString(ResultMetadataType.POSSIBLE_COUNTRY),
getMetaString(ResultMetadataType.SUGGESTED_PRICE),
getMetaString(ResultMetadataType.UPC_EAN_EXTENSION)
)
}
private fun Result.getRawData(): ByteArray? {
val metadata = 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
// If the byte segments are shorter than the converted string, the
// content of the QR Code has been encoded with different encoding
// modes (e.g. some parts in alphanumeric, some in byte encoding).
// This is because Zxing only records byte segments for byte encoded
// parts. Please note the byte segments can actually be longer than
// the string because Zxing cuts off prefixes like "WIFI:".
return if (bytes.size >= text.length) bytes else null
}
private fun Result.getMetaString(
@@ -148,22 +186,3 @@ private fun Result.getMetaString(
): 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
}
}
@@ -2,22 +2,31 @@ package de.markusfisch.android.binaryeye.fragment
import android.annotation.SuppressLint
import android.app.AlertDialog
import android.content.Context
import android.graphics.Bitmap
import android.os.Build
import android.os.Bundle
import android.support.v4.app.Fragment
import android.view.*
import android.widget.EditText
import com.google.zxing.BarcodeFormat
import com.google.zxing.EncodeHintType
import de.markusfisch.android.binaryeye.R
import de.markusfisch.android.binaryeye.app.*
import de.markusfisch.android.binaryeye.app.hasWritePermission
import de.markusfisch.android.binaryeye.content.copyToClipboard
import de.markusfisch.android.binaryeye.content.shareFile
import de.markusfisch.android.binaryeye.content.shareText
import de.markusfisch.android.binaryeye.io.addSuffixIfNotGiven
import de.markusfisch.android.binaryeye.io.toSaveResult
import de.markusfisch.android.binaryeye.io.writeExternalFile
import de.markusfisch.android.binaryeye.view.doOnApplyWindowInsets
import de.markusfisch.android.binaryeye.view.setPaddingFromWindowInsets
import de.markusfisch.android.binaryeye.widget.ConfinedScalingImageView
import de.markusfisch.android.binaryeye.widget.toast
import de.markusfisch.android.binaryeye.zxing.Zxing
import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import de.markusfisch.android.binaryeye.zxing.encodeAsBitmap
import de.markusfisch.android.binaryeye.zxing.encodeAsSvg
import de.markusfisch.android.binaryeye.zxing.encodeAsText
import kotlinx.coroutines.*
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
@@ -26,11 +35,15 @@ import java.util.*
class BarcodeFragment : Fragment() {
private enum class FileType {
PNG, SVG
PNG, SVG, TXT
}
private val parentJob = Job()
private val scope = CoroutineScope(Dispatchers.IO + parentJob)
private var barcodeBitmap: Bitmap? = null
private var barcodeSvg: String? = null
private var barcodeTxt: String? = null
private var content: String = ""
private var format: BarcodeFormat? = null
@@ -56,10 +69,12 @@ class BarcodeFragment : Fragment() {
val args = arguments ?: return view
val content = args.getString(CONTENT) ?: return view
val format = args.getSerializable(FORMAT) as BarcodeFormat? ?: return view
val hints = args.getSerializable(HINTS) as EnumMap<EncodeHintType, Any>?
val size = args.getInt(SIZE)
try {
barcodeBitmap = Zxing.encodeAsBitmap(content, format, size, size)
barcodeSvg = Zxing.encodeAsSvg(content, format, size, size)
barcodeBitmap = encodeAsBitmap(content, format, size, size, hints)
barcodeSvg = encodeAsSvg(content, format, hints)
barcodeTxt = encodeAsText(content, format, hints)
} catch (e: Exception) {
var message = e.message
if (message == null || message.isEmpty()) {
@@ -79,14 +94,15 @@ class BarcodeFragment : Fragment() {
)
imageView.setImageBitmap(barcodeBitmap)
imageView.post {
// make sure to invoke this after ScalingImageView.onLayout()
// Make sure to invoke this after ScalingImageView.onLayout().
imageView.minWidth /= 2f
}
view.findViewById<View>(R.id.share).setOnClickListener {
val bitmap = barcodeBitmap
bitmap?.let {
share(bitmap)
it.context?.apply {
pickFileType(R.string.share_as) { fileType ->
shareAs(fileType)
}
}
}
@@ -98,29 +114,55 @@ class BarcodeFragment : Fragment() {
return view
}
override fun onDestroyView() {
super.onDestroyView()
parentJob.cancel()
}
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.export_svg -> {
askForFileNameAndSave(FileType.SVG)
R.id.copy_to_clipboard -> {
barcodeTxt?.let {
context.copyToClipboard(it)
context.toast(R.string.copied_to_clipboard)
}
true
}
R.id.export_png -> {
askForFileNameAndSave(FileType.PNG)
R.id.export_to_file -> {
context.pickFileType(R.string.export_as) {
askForFileNameAndSave(it)
}
true
}
else -> super.onOptionsItemSelected(item)
}
}
// dialogs do not have a parent view
private fun Context.pickFileType(
title: Int,
action: (FileType) -> Unit
) {
val fileTypes = FileType.values()
AlertDialog.Builder(this)
.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)) {
// Write permission is only required before Android Q.
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q &&
!ac.hasWritePermission { askForFileNameAndSave(fileType) }
) {
return
}
val view = ac.layoutInflater.inflate(R.layout.dialog_save_file, null)
@@ -133,18 +175,26 @@ class BarcodeFragment : Fragment() {
when (fileType) {
FileType.PNG -> saveAs(
addSuffixIfNotGiven(fileName, ".png"),
"image/png"
MIME_PNG
) {
barcodeBitmap?.saveAsPng(it)
}
FileType.SVG -> saveAs(
addSuffixIfNotGiven(fileName, ".svg"),
"image/svg+xmg"
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) { _, _ ->
@@ -158,18 +208,26 @@ class BarcodeFragment : Fragment() {
write: (outputStream: OutputStream) -> Unit
) {
val ac = activity ?: return
GlobalScope.launch {
val message = writeExternalFile(ac, fileName, mimeType, write).toSaveResult()
GlobalScope.launch(Main) {
scope.launch(Dispatchers.IO) {
val message = ac.writeExternalFile(fileName, mimeType, write).toSaveResult()
withContext(Dispatchers.Main) {
ac.toast(message)
}
}
}
private fun share(bitmap: Bitmap) {
GlobalScope.launch {
private fun Context.shareAs(fileType: FileType) {
when (fileType) {
FileType.PNG -> barcodeBitmap?.let { share(it) }
FileType.SVG -> barcodeSvg?.let { shareText(it, MIME_SVG) }
FileType.TXT -> barcodeTxt?.let { shareText(it) }
}
}
private fun Context.share(bitmap: Bitmap) {
scope.launch(Dispatchers.IO) {
val file = File(
context.externalCacheDir,
externalCacheDir,
"shared_barcode.png"
)
val success = try {
@@ -180,11 +238,11 @@ class BarcodeFragment : Fragment() {
} catch (e: IOException) {
false
}
GlobalScope.launch(Main) {
withContext(Dispatchers.Main) {
if (success) {
shareFile(context, file, "image/png")
shareFile(file, "image/png")
} else {
activity?.toast(R.string.error_saving_file)
toast(R.string.error_saving_file)
}
}
}
@@ -193,16 +251,24 @@ class BarcodeFragment : Fragment() {
companion object {
private const val CONTENT = "content"
private const val FORMAT = "format"
private const val HINTS = "hints"
private const val SIZE = "size"
private const val MIME_PNG = "image/png"
private const val MIME_SVG = "image/svg+xmg"
private const val MIME_TXT = "text/plain"
fun newInstance(
content: String,
format: BarcodeFormat,
size: Int
size: Int,
hints: EnumMap<EncodeHintType, Any>? = null
): Fragment {
val args = Bundle()
args.putString(CONTENT, content)
args.putSerializable(FORMAT, format)
hints?.let {
args.putSerializable(HINTS, it)
}
args.putInt(SIZE, size)
val fragment = BarcodeFragment()
fragment.arguments = args
@@ -211,10 +277,9 @@ class BarcodeFragment : Fragment() {
}
}
private fun Bitmap.saveAsPng(outputStream: OutputStream, quality: Int = 90) {
this.compress(Bitmap.CompressFormat.PNG, quality, outputStream)
}
private fun Bitmap.saveAsPng(outputStream: OutputStream, quality: Int = 90) =
compress(Bitmap.CompressFormat.PNG, quality, outputStream)
private val fileNameCharacters = "[^A-Za-z0-9]".toRegex()
private fun encodeFileName(name: String): String =
fileNameCharacters.replace(name, "_").take(16).trim('_').toLowerCase(Locale.getDefault())
fileNameCharacters.replace(name, "_").take(16).trim('_').lowercase(Locale.getDefault())
@@ -5,17 +5,29 @@ import android.os.Bundle
import android.support.design.widget.FloatingActionButton
import android.support.v4.app.Fragment
import android.text.Editable
import android.text.Html
import android.text.TextWatcher
import android.text.method.LinkMovementMethod
import android.view.*
import android.widget.EditText
import android.widget.TableLayout
import android.widget.TableRow
import android.widget.TextView
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel
import com.google.zxing.qrcode.encoder.Encoder
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.actions.wifi.WifiConnector
import de.markusfisch.android.binaryeye.activity.MainActivity
import de.markusfisch.android.binaryeye.adapter.prettifyFormatName
import de.markusfisch.android.binaryeye.app.*
import de.markusfisch.android.binaryeye.content.copyToClipboard
import de.markusfisch.android.binaryeye.content.shareText
import de.markusfisch.android.binaryeye.database.Scan
import de.markusfisch.android.binaryeye.io.askForFileName
import de.markusfisch.android.binaryeye.io.toSaveResult
import de.markusfisch.android.binaryeye.io.writeExternalFile
import de.markusfisch.android.binaryeye.view.setPaddingFromWindowInsets
import de.markusfisch.android.binaryeye.widget.toast
import kotlinx.coroutines.CoroutineScope
@@ -26,9 +38,11 @@ import kotlinx.coroutines.launch
class DecodeFragment : Fragment() {
private lateinit var contentView: EditText
private lateinit var formatView: TextView
private lateinit var dataView: TableLayout
private lateinit var metaView: TableLayout
private lateinit var hexView: TextView
private lateinit var format: String
private lateinit var stampView: TextView
private lateinit var fab: FloatingActionButton
private val parentJob = Job()
@@ -36,8 +50,10 @@ class DecodeFragment : Fragment() {
private val content: String
get() = contentView.text.toString()
private var closeAutomatically = false
private var action = ActionRegistry.DEFAULT_ACTION
private var isBinary = false
private var raw: ByteArray = ByteArray(0)
private var id = 0L
override fun onCreate(state: Bundle?) {
@@ -58,18 +74,20 @@ class DecodeFragment : Fragment() {
false
)
closeAutomatically = prefs.closeAutomatically &&
activity?.intent?.hasExtra(MainActivity.DECODED) == true
val scan = arguments?.getParcelable(SCAN) as Scan?
?: throw IllegalArgumentException("DecodeFragment needs a Scan")
id = scan.id
val inputContent = scan.content
isBinary = hasNonPrintableCharacters(
inputContent
) or inputContent.isEmpty()
val raw = scan.raw ?: inputContent.toByteArray()
isBinary = scan.raw != null
raw = scan.raw ?: inputContent.toByteArray()
format = scan.format
contentView = view.findViewById(R.id.content)
stampView = view.findViewById(R.id.stamp)
fab = view.findViewById(R.id.open)
if (!isBinary) {
@@ -102,7 +120,7 @@ class DecodeFragment : Fragment() {
executeAction(content.toByteArray())
}
} else {
contentView.setText(R.string.binary_data)
contentView.setText(String(raw).foldNonAlNum())
contentView.isEnabled = false
fab.setImageResource(R.drawable.ic_action_save)
fab.setOnClickListener {
@@ -110,19 +128,26 @@ class DecodeFragment : Fragment() {
}
}
val trackingLink = generateDpTrackingLink(raw, scan.format)
if (trackingLink != null) {
stampView.apply {
text = Html.fromHtml(trackingLink)
isClickable = true
movementMethod = LinkMovementMethod.getInstance()
}
} else {
stampView.visibility = View.GONE
}
formatView = view.findViewById(R.id.format)
dataView = view.findViewById(R.id.data)
metaView = view.findViewById(R.id.meta)
hexView = view.findViewById(R.id.hex)
updateViewsAndAction(raw)
if (action is WifiAction) {
val password = (action as WifiAction).password
if (password != null) {
activity?.apply {
copyToClipboard(password)
toast(R.string.copied_password_to_clipboard)
}
}
if (!isBinary) {
fillDataView(dataView, inputContent)
}
if (prefs.showMetaData) {
@@ -135,6 +160,11 @@ class DecodeFragment : Fragment() {
return view
}
override fun onDestroy() {
super.onDestroy()
parentJob.cancel()
}
private fun updateViewsAndAction(bytes: ByteArray) {
val prevAction = action
if (!prevAction.canExecuteOn(bytes)) {
@@ -143,7 +173,7 @@ class DecodeFragment : Fragment() {
formatView.text = resources.getQuantityString(
R.plurals.barcode_info,
bytes.size,
format,
prettifyFormatName(format),
bytes.size
)
hexView.text = if (prefs.showHexDump) hexDump(bytes) else ""
@@ -160,11 +190,30 @@ class DecodeFragment : Fragment() {
}
}
private fun fillDataView(tableLayout: TableLayout, content: String) {
val items = LinkedHashMap<Int, String?>()
if (action is WifiAction) {
WifiConnector.parseMap(content)?.let { wifiData ->
items.putAll(
linkedMapOf(
R.string.entry_type to getString(R.string.wifi_network),
R.string.wifi_ssid to wifiData["S"],
R.string.wifi_password to wifiData["P"],
R.string.wifi_type to wifiData["T"],
R.string.wifi_hidden to wifiData["H"],
R.string.wifi_eap to wifiData["E"],
R.string.wifi_identity to wifiData["I"],
R.string.wifi_anonymous_identity to wifiData["A"],
R.string.wifi_phase2 to wifiData["PH2"]
)
)
}
}
fillDataTable(tableLayout, items)
}
private fun fillMetaView(tableLayout: TableLayout, scan: Scan) {
val ctx = tableLayout.context
val spaceBetween = (ctx.resources.displayMetrics.density * 16f).toInt()
var hasMeta = false
sortedMapOf(
val items = linkedMapOf(
R.string.error_correction_level to scan.errorCorrectionLevel,
R.string.issue_number to scan.issueNumber,
R.string.orientation to scan.orientation,
@@ -173,21 +222,38 @@ class DecodeFragment : Fragment() {
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 {
)
if (prefs.showQrVersion && scan.format == "QR_CODE") {
items.putAll(
linkedMapOf(
R.string.qr_version to "${scan.version()}"
)
)
}
fillDataTable(tableLayout, items)
}
private fun fillDataTable(
tableLayout: TableLayout,
items: LinkedHashMap<Int, String?>
) {
val ctx = tableLayout.context
val spaceBetween = (16f * ctx.resources.displayMetrics.density).toInt()
items.forEach { item ->
val text = item.value
if (!text.isNullOrBlank()) {
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
valueView.text = text
tr.addView(keyView)
tr.addView(valueView)
tableLayout.addView(tr)
hasMeta = true
}
}
if (hasMeta) {
if (tableLayout.childCount > 0) {
tableLayout.setPadding(0, 0, 0, spaceBetween)
}
}
@@ -201,21 +267,32 @@ class DecodeFragment : Fragment() {
if (id > 0L) {
menu.findItem(R.id.remove).isVisible = true
}
if (action is WifiAction) {
menu.findItem(R.id.copy_password).isVisible = true
}
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.copy_password -> {
copyPasswordToClipboard()
maybeBackOrFinish()
true
}
R.id.copy_to_clipboard -> {
copyToClipboard(content)
copyToClipboard(textOrHex())
maybeBackOrFinish()
true
}
R.id.share -> {
context?.also { shareText(it, content) }
context?.apply {
shareText(textOrHex())
maybeBackOrFinish()
}
true
}
R.id.create -> {
addFragment(
fragmentManager,
fragmentManager?.addFragment(
EncodeFragment.newInstance(content, format)
)
true
@@ -229,6 +306,24 @@ class DecodeFragment : Fragment() {
}
}
private fun textOrHex() = if (isBinary) {
raw.toHexString()
} else {
content
}
private fun copyPasswordToClipboard() {
val ac = action
if (ac is WifiAction) {
ac.password?.let { password ->
activity?.apply {
copyToClipboard(password)
toast(R.string.copied_password_to_clipboard)
}
}
}
}
private fun copyToClipboard(text: String) {
activity?.apply {
copyToClipboard(text)
@@ -236,6 +331,12 @@ class DecodeFragment : Fragment() {
}
}
private fun maybeBackOrFinish() {
if (closeAutomatically) {
backOrFinish()
}
}
private fun backOrFinish() {
val fm = fragmentManager
if (fm != null && fm.backStackEntryCount > 0) {
@@ -250,25 +351,28 @@ class DecodeFragment : Fragment() {
if (content.isNotEmpty()) {
if (action is WifiAction &&
Build.VERSION.SDK_INT < Build.VERSION_CODES.Q &&
!hasLocationPermission(ac)
!ac.hasLocationPermission { executeAction(content) }
) {
return
}
scope.launch {
action.execute(ac, content)
maybeBackOrFinish()
}
}
}
private fun askForFileNameAndSave(raw: ByteArray) {
val ac = activity ?: return
if (!hasWritePermission(ac)) {
// Write permission is only required before Android Q.
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q &&
!ac.hasWritePermission { askForFileNameAndSave(raw) }
) {
return
}
scope.launch(Dispatchers.Main) {
val name = ac.askForFileName() ?: return@launch
val message = writeExternalFile(
ac,
val message = ac.writeExternalFile(
name,
"application/octet-stream"
) {
@@ -278,11 +382,6 @@ class DecodeFragment : Fragment() {
}
}
override fun onDestroyView() {
parentJob.cancel()
super.onDestroyView()
}
companion object {
private const val SCAN = "scan"
@@ -296,7 +395,7 @@ class DecodeFragment : Fragment() {
}
}
fun hexDump(bytes: ByteArray, charsPerLine: Int = 33): String {
private fun hexDump(bytes: ByteArray, charsPerLine: Int = 33): String {
if (charsPerLine < 4 || bytes.isEmpty()) {
return ""
}
@@ -309,7 +408,7 @@ fun hexDump(bytes: ByteArray, charsPerLine: Int = 33): String {
while (true) {
val ord = bytes[i]
hex.append(String.format("%02X ", ord))
ascii.append(if (ord > 31) ord.toChar() else " ")
ascii.append(if (ord > 31) ord.toInt().toChar() else " ")
++i
val posInLine = i % itemsPerLine
val atEnd = i >= len
@@ -334,3 +433,75 @@ fun hexDump(bytes: ByteArray, charsPerLine: Int = 33): String {
}
return dump.toString()
}
private fun generateDpTrackingLink(raw: ByteArray, format: String): String? {
// Check for Deutsche Post Matrixcode stamp.
var isStamp = false
var rawData = raw
if (format == "DATA_MATRIX" &&
raw.toString(Charsets.ISO_8859_1).startsWith("DEA5")
) {
if (raw.size == 47) {
isStamp = true
} else if (raw.size > 47) {
// Transform back to original data.
rawData = raw.toString(Charsets.UTF_8).toByteArray(
Charsets.ISO_8859_1
)
if (rawData.size == 47) {
isStamp = true
}
}
}
if (!isStamp) {
return null
}
val hex = StringBuilder()
hex.append(String.format("%02X", rawData[9]))
hex.append(String.format("%02X", rawData[10]))
hex.append(String.format("%02X", rawData[11]))
hex.append(String.format("%02X", rawData[12]))
hex.append(String.format("%02X", rawData[13]))
hex.append(String.format("%X", (rawData[4].toInt() and 0x0f).toByte()))
hex.append(String.format("%02X", rawData[5]))
hex.append(String.format("%02X", rawData[6]))
hex.append(String.format("%02X", rawData[7]))
hex.append(String.format("%02X", rawData[8]))
val hexString = hex.toString()
val trackingNumber = hexString + String.format(
"%X",
crc4(hexString.toByteArray(Charsets.ISO_8859_1))
)
return "<a href=\"https://www.deutschepost.de/de/s/sendungsverfolgung/verfolgen.html?piececode=$trackingNumber\">Deutsche Post: $trackingNumber</a>"
}
// CRC-4 with polynomial x^4 + x + 1.
private fun crc4(input: ByteArray): Int {
var crc = 0
var i = 0
while (i < input.size) {
val c = input[i].toInt()
var j = 0x80
while (j != 0) {
var bit = crc and 0x8
crc = crc shl 1
if (c and j != 0) {
bit = bit xor 0x8
}
if (bit != 0) {
crc = crc xor 0x3
}
j = j ushr 1
}
++i
}
crc = crc and 0xF
return crc
}
private fun Scan.version(): Int = Encoder.encode(
content,
ErrorCorrectionLevel.valueOf(errorCorrectionLevel ?: "L")
).version.versionNumber
@@ -2,17 +2,25 @@ package de.markusfisch.android.binaryeye.fragment
import android.os.Bundle
import android.support.v4.app.Fragment
import android.view.*
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.*
import com.google.zxing.BarcodeFormat
import com.google.zxing.EncodeHintType
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel
import de.markusfisch.android.binaryeye.R
import de.markusfisch.android.binaryeye.adapter.prettifyFormatName
import de.markusfisch.android.binaryeye.app.addFragment
import de.markusfisch.android.binaryeye.app.hideSoftKeyboard
import de.markusfisch.android.binaryeye.app.prefs
import de.markusfisch.android.binaryeye.view.hideSoftKeyboard
import de.markusfisch.android.binaryeye.view.setPaddingFromWindowInsets
import java.util.*
class EncodeFragment : Fragment() {
private lateinit var formatView: Spinner
private lateinit var errorCorrectionLabel: TextView
private lateinit var errorCorrectionLevel: Spinner
private lateinit var sizeView: TextView
private lateinit var sizeBarView: SeekBar
private lateinit var contentView: EditText
@@ -31,11 +39,6 @@ class EncodeFragment : Fragment() {
BarcodeFormat.UPC_A
)
override fun onCreate(state: Bundle?) {
super.onCreate(state)
setHasOptionsMenu(true)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
@@ -51,11 +54,38 @@ class EncodeFragment : Fragment() {
)
formatView = view.findViewById(R.id.format)
formatView.adapter = ArrayAdapter(
val formatAdapter = ArrayAdapter(
ac,
android.R.layout.simple_list_item_1,
writers.map { it.name }
android.R.layout.simple_spinner_item,
writers.map { prettifyFormatName(it.name) }
)
formatAdapter.setDropDownViewResource(
android.R.layout.simple_spinner_dropdown_item
)
formatView.adapter = formatAdapter
formatView.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(
parentView: AdapterView<*>?,
selectedItemView: View?,
position: Int,
id: Long
) {
val visibility = if (
writers[position] == BarcodeFormat.QR_CODE
) {
View.VISIBLE
} else {
View.GONE
}
errorCorrectionLabel.visibility = visibility
errorCorrectionLevel.visibility = visibility
}
override fun onNothingSelected(parentView: AdapterView<*>?) {}
}
errorCorrectionLabel = view.findViewById(R.id.error_correction_label)
errorCorrectionLevel = view.findViewById(R.id.error_correction_level)
sizeView = view.findViewById(R.id.size_display)
sizeBarView = view.findViewById(R.id.size_bar)
@@ -94,22 +124,17 @@ class EncodeFragment : Fragment() {
prefs.indexOfLastSelectedFormat = formatView.selectedItemPosition
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.fragment_encode, menu)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.next -> {
encode()
true
}
else -> super.onOptionsItemSelected(item)
}
}
private fun encode() {
val hints = EnumMap<EncodeHintType, Any>(EncodeHintType::class.java)
val format = writers[formatView.selectedItemPosition]
if (format == BarcodeFormat.QR_CODE) {
hints[EncodeHintType.ERROR_CORRECTION] = arrayListOf(
ErrorCorrectionLevel.L,
ErrorCorrectionLevel.M,
ErrorCorrectionLevel.Q,
ErrorCorrectionLevel.H
)[errorCorrectionLevel.selectedItemPosition]
}
val size = getSize(sizeBarView.progress)
val content = contentView.text.toString()
if (content.isEmpty()) {
@@ -120,9 +145,8 @@ class EncodeFragment : Fragment() {
).show()
} else {
activity?.hideSoftKeyboard(contentView)
addFragment(
fragmentManager,
BarcodeFragment.newInstance(content, format, size)
fragmentManager?.addFragment(
BarcodeFragment.newInstance(content, format, size, hints)
)
}
}
@@ -21,12 +21,16 @@ import android.widget.ListView
import de.markusfisch.android.binaryeye.R
import de.markusfisch.android.binaryeye.adapter.ScansAdapter
import de.markusfisch.android.binaryeye.app.*
import de.markusfisch.android.binaryeye.content.copyToClipboard
import de.markusfisch.android.binaryeye.content.shareText
import de.markusfisch.android.binaryeye.database.Database
import de.markusfisch.android.binaryeye.database.getString
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.io.askForFileName
import de.markusfisch.android.binaryeye.io.toSaveResult
import de.markusfisch.android.binaryeye.view.*
import de.markusfisch.android.binaryeye.widget.toast
import kotlinx.coroutines.*
@@ -151,7 +155,7 @@ class HistoryFragment : Fragment() {
}
true
}
listView.setOnScrollListener(systemBarScrollListener)
listView.setOnScrollListener(systemBarListViewScrollListener)
fab = view.findViewById(R.id.share)
fab.setOnClickListener { v ->
@@ -221,7 +225,7 @@ class HistoryFragment : Fragment() {
true
}
R.id.export_history -> {
askToExportToFile(context)
askToExportToFile()
true
}
else -> super.onOptionsItemSelected(item)
@@ -262,7 +266,7 @@ class HistoryFragment : Fragment() {
View.GONE
}
cursor?.let { cursor ->
// close previous cursor
// Close previous cursor.
scansAdapter?.also { it.changeCursor(null) }
scansAdapter = ScansAdapter(ac, cursor)
listView.adapter = scansAdapter
@@ -290,21 +294,18 @@ class HistoryFragment : Fragment() {
private fun showScan(id: Long) = db.getScan(id)?.also { scan ->
closeActionMode()
try {
addFragment(
fragmentManager,
DecodeFragment.newInstance(scan)
)
fragmentManager?.addFragment(DecodeFragment.newInstance(scan))
} catch (e: IllegalArgumentException) {
// ignore, can never happen
// Ignore, can never happen.
}
}
private fun getScanName(position: Int): String? {
val cursor = scansAdapter?.getItem(position) as Cursor?
return cursor?.getString(cursor.getColumnIndex(Database.SCANS_NAME))
return cursor?.getString(Database.SCANS_NAME)
}
// dialogs don't have a parent layout
// Dialogs don't have a parent layout.
@SuppressLint("InflateParams")
private fun askForName(context: Context, id: Long, text: String?) {
val view = LayoutInflater.from(context).inflate(
@@ -357,41 +358,46 @@ class HistoryFragment : Fragment() {
.show()
}
private fun askToExportToFile(context: Context) = scope.launch {
val ac = activity ?: return@launch
progressView.useVisibility {
if (!hasWritePermission(ac)) {
return@useVisibility
}
val options = context.resources.getStringArray(
R.array.export_options_values
)
val delimiter = alertDialog<String>(context) { resume ->
setTitle(R.string.export_as)
setItems(R.array.export_options_names) { _, which ->
resume(options[which])
private fun askToExportToFile() {
scope.launch {
val ac = activity ?: return@launch
progressView.useVisibility {
// Write permission is only required before Android Q.
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q &&
!ac.hasWritePermission { askToExportToFile() }
) {
return@useVisibility
}
} ?: return@useVisibility
val name = withContext(Dispatchers.Main) {
ac.askForFileName(
when (delimiter) {
"db" -> ".db"
"json" -> ".json"
else -> ".csv"
}
val options = ac.resources.getStringArray(
R.array.export_options_values
)
} ?: return@useVisibility
val message = when (delimiter) {
"db" -> exportDatabase(ac, name)
else -> db.getScansDetailed(filter)?.use {
when (delimiter) {
"json" -> exportJson(context, name, it)
else -> exportCsv(context, name, it, delimiter)
val delimiter = alertDialog<String>(ac) { resume ->
setTitle(R.string.export_as)
setItems(R.array.export_options_names) { _, which ->
resume(options[which])
}
} ?: false
}.toSaveResult()
withContext(Dispatchers.Main) {
ac.toast(message)
} ?: return@useVisibility
val name = withContext(Dispatchers.Main) {
ac.askForFileName(
when (delimiter) {
"db" -> ".db"
"json" -> ".json"
else -> ".csv"
}
)
} ?: return@useVisibility
val message = when (delimiter) {
"db" -> ac.exportDatabase(name)
else -> db.getScansDetailed(filter)?.use {
when (delimiter) {
"json" -> ac.exportJson(name, it)
else -> ac.exportCsv(name, it, delimiter)
}
} ?: false
}.toSaveResult()
withContext(Dispatchers.Main) {
ac.toast(message)
}
}
}
}
@@ -414,31 +420,31 @@ class HistoryFragment : Fragment() {
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" -> cursor.exportText(details[1])
"csv" -> cursor.exportCsv(details[1])
else -> cursor.exportJson()
}
}
text?.let {
withContext(Dispatchers.Main) {
shareText(context, it)
context.shareText(it)
}
}
}
}
}
private fun exportText(cursor: Cursor, separator: String): String {
private fun Cursor.exportText(separator: String): String {
val sb = StringBuilder()
val contentIndex = cursor.getColumnIndex(Database.SCANS_CONTENT)
if (cursor.moveToFirst()) {
val contentIndex = getColumnIndex(Database.SCANS_CONTENT)
if (contentIndex > -1 && moveToFirst()) {
do {
val content = cursor.getString(contentIndex)
val content = getString(contentIndex)
if (content?.isNotEmpty() == true) {
sb.append(content)
sb.append(separator)
}
} while (cursor.moveToNext())
} while (moveToNext())
}
return sb.toString()
}
@@ -1,114 +1,154 @@
package de.markusfisch.android.binaryeye.fragment
import android.app.Activity
import android.app.AlertDialog
import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
import android.content.SharedPreferences.OnSharedPreferenceChangeListener
import android.net.wifi.WifiManager
import android.os.Build
import android.os.Bundle
import android.support.v4.app.Fragment
import android.support.v7.widget.SwitchCompat
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.EditText
import android.support.annotation.RequiresApi
import android.support.v14.preference.MultiSelectListPreference
import android.support.v7.preference.ListPreference
import android.support.v7.preference.Preference
import android.support.v7.preference.PreferenceFragmentCompat
import android.support.v7.preference.PreferenceGroup
import de.markusfisch.android.binaryeye.R
import de.markusfisch.android.binaryeye.actions.wifi.removeNetworkSuggestions
import de.markusfisch.android.binaryeye.activity.SplashActivity
import de.markusfisch.android.binaryeye.app.prefs
import de.markusfisch.android.binaryeye.preference.UrlPreference
import de.markusfisch.android.binaryeye.view.setPaddingFromWindowInsets
import de.markusfisch.android.binaryeye.view.systemBarRecyclerViewScrollListener
import de.markusfisch.android.binaryeye.widget.toast
class PreferencesFragment : Fragment() {
private lateinit var showCropHandleSwitch: SwitchCompat
private lateinit var zoomBySwipingSwitch: SwitchCompat
private lateinit var autoResizeSwitch: SwitchCompat
private lateinit var tryHarderSwitch: SwitchCompat
private lateinit var vibrateSwitch: SwitchCompat
private lateinit var useHistorySwitch: SwitchCompat
private lateinit var ignoreConsecutiveDuplicatesSwitch: SwitchCompat
private lateinit var openImmediatelySwitch: SwitchCompat
private lateinit var showMetaDataSwitch: SwitchCompat
private lateinit var showHexDumpSwitch: SwitchCompat
private lateinit var openWithUrlInput: EditText
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
state: Bundle?
): View? {
activity?.setTitle(R.string.preferences)
val view = inflater.inflate(
R.layout.fragment_preferences,
container,
false
)
showCropHandleSwitch = view.findViewById(R.id.show_crop_handle)
if (prefs.showCropHandle) {
showCropHandleSwitch.toggle()
class PreferencesFragment : PreferenceFragmentCompat() {
private val changeListener = object : OnSharedPreferenceChangeListener {
override fun onSharedPreferenceChanged(
sharedPreferences: SharedPreferences,
key: String
) {
val preference = findPreference(key) ?: return
prefs.update()
if (preference.key == "custom_locale") {
activity?.restartApp()
} else {
setSummary(preference)
}
}
zoomBySwipingSwitch = view.findViewById(R.id.zoom_by_swiping)
if (prefs.zoomBySwiping) {
zoomBySwipingSwitch.toggle()
}
autoResizeSwitch = view.findViewById(R.id.auto_rotate)
if (prefs.autoRotate) {
autoResizeSwitch.toggle()
}
tryHarderSwitch = view.findViewById(R.id.try_harder)
if (prefs.tryHarder) {
tryHarderSwitch.toggle()
}
vibrateSwitch = view.findViewById(R.id.vibrate)
if (prefs.vibrate) {
vibrateSwitch.toggle()
}
useHistorySwitch = view.findViewById(R.id.use_history)
if (prefs.useHistory) {
useHistorySwitch.toggle()
}
ignoreConsecutiveDuplicatesSwitch = view.findViewById(
R.id.ignore_consecutive_duplicates
)
if (prefs.ignoreConsecutiveDuplicates) {
ignoreConsecutiveDuplicatesSwitch.toggle()
}
openImmediatelySwitch = view.findViewById(R.id.open_immediately)
if (prefs.openImmediately) {
openImmediatelySwitch.toggle()
}
showMetaDataSwitch = view.findViewById(R.id.show_meta_data)
if (prefs.showMetaData) {
showMetaDataSwitch.toggle()
}
showHexDumpSwitch = view.findViewById(R.id.show_hex_dump)
if (prefs.showHexDump) {
showHexDumpSwitch.toggle()
}
openWithUrlInput = view.findViewById(R.id.open_with_url)
openWithUrlInput.setText(prefs.openWithUrl)
(view.findViewById(R.id.scroll_view) as View).setPaddingFromWindowInsets()
return view
}
override fun onStop() {
super.onStop()
prefs.showCropHandle = showCropHandleSwitch.isChecked
prefs.zoomBySwiping = zoomBySwipingSwitch.isChecked
prefs.autoRotate = autoResizeSwitch.isChecked
prefs.tryHarder = tryHarderSwitch.isChecked
prefs.vibrate = vibrateSwitch.isChecked
prefs.useHistory = useHistorySwitch.isChecked
prefs.ignoreConsecutiveDuplicates = ignoreConsecutiveDuplicatesSwitch.isChecked
prefs.openImmediately = openImmediatelySwitch.isChecked
prefs.showMetaData = showMetaDataSwitch.isChecked
prefs.showHexDump = showHexDumpSwitch.isChecked
prefs.openWithUrl = openWithUrlInput.text.toString()
override fun onCreatePreferences(state: Bundle?, rootKey: String?) {
addPreferencesFromResource(R.xml.preferences)
activity?.setTitle(R.string.preferences)
wireClearNetworkPreferences()
}
private fun wireClearNetworkPreferences() {
val pref = findPreference("clear_network_suggestions") ?: return
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
pref.isVisible = false
} else {
pref.setOnPreferenceClickListener {
askToClearNetworkSuggestions(context)
true
}
}
}
@RequiresApi(Build.VERSION_CODES.Q)
private fun askToClearNetworkSuggestions(context: Context) {
AlertDialog.Builder(context)
.setMessage(R.string.really_remove_all_networks)
.setPositiveButton(android.R.string.ok) { _, _ ->
clearNetworkSuggestions(context)
}
.setNegativeButton(android.R.string.cancel) { _, _ -> }
.show()
}
@RequiresApi(Build.VERSION_CODES.Q)
private fun clearNetworkSuggestions(context: Context) {
context.toast(
if (
removeNetworkSuggestions(context) == WifiManager.STATUS_NETWORK_SUGGESTIONS_SUCCESS
) {
R.string.clear_network_suggestions_success
} else {
R.string.clear_network_suggestions_nothing_to_remove
}
)
}
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 MultiSelectListPreference -> {
preference.setSummary(
preference.values.joinToString(", ") {
it.replace(Regex("_"), " ")
}
)
}
is PreferenceGroup -> {
setSummaries(preference)
}
}
}
}
private fun Activity.restartApp() {
val intent = Intent(this, SplashActivity::class.java)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
intent.addFlags(
Intent.FLAG_ACTIVITY_NEW_TASK or
Intent.FLAG_ACTIVITY_CLEAR_TASK
)
}
startActivity(intent)
finish()
// Restart to begin with an unmodified Locale to follow system settings.
Runtime.getRuntime().exit(0)
}
@@ -0,0 +1,96 @@
package de.markusfisch.android.binaryeye.fragment
import android.os.Bundle
import android.support.v7.preference.PreferenceDialogFragmentCompat
import android.view.View
import android.widget.TextView
import de.markusfisch.android.binaryeye.R
import de.markusfisch.android.binaryeye.app.prefs
import de.markusfisch.android.binaryeye.database.Scan
import de.markusfisch.android.binaryeye.net.sendAsync
import de.markusfisch.android.binaryeye.preference.UrlPreference
class UrlDialogFragment : PreferenceDialogFragmentCompat() {
private var urlView: TextView? = null
private var testButton: TextView? = null
override fun onBindDialogView(view: View?) {
super.onBindDialogView(view)
urlView = view?.findViewById(R.id.url)
testButton = view?.findViewById(R.id.test_url)
testButton?.setOnClickListener {
testUrl(testButton)
}
urlView?.text = urlPreference().getUrl()
urlView?.hint = getString(
when (prefs.sendScanType) {
"0" -> R.string.url_hint_add_content
else -> R.string.url_hint
}
)
}
override fun onDialogClosed(positiveResult: Boolean) {
if (positiveResult) {
urlPreference().setUrl(getUrl())
}
}
private fun getUrl() = completeUrl(urlView?.text.toString())
private fun urlPreference() = preference as UrlPreference
private fun testUrl(textView: TextView?) {
val url = getUrl()
if (url.isEmpty()) {
return
}
textView ?: return
textView.text = ""
Scan(
"test",
null,
"none",
null,
null,
null,
null,
null,
null,
null,
null
).sendAsync(url, prefs.sendScanType) { code, body ->
textView.text = when {
code != null -> "$code"
body != null -> body
else -> getString(R.string.background_request_failed)
}
}
}
companion object {
fun newInstance(key: String): UrlDialogFragment {
val args = Bundle()
args.putString(ARG_KEY, key)
val fragment = UrlDialogFragment()
fragment.arguments = args
return fragment
}
}
}
private fun completeUrl(template: String): String {
var s = template.trim()
if (s.isEmpty()) {
return ""
}
if (!s.startsWith("http")) {
s = "http://${s}"
}
if (prefs.sendScanType == "0" &&
!s.matches(".*/[a-zA-Z._-]*\\?[a-zA-Z0-9&=_-]+=$".toRegex())
) {
s = "${s}?content="
}
return s
}
@@ -7,11 +7,13 @@ 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 {
fun ContentResolver.loadImageUri(uri: Uri): Bitmap? = try {
val options = BitmapFactory.Options()
cr.openInputStream(uri)?.use {
openInputStream(uri)?.use {
options.inJustDecodeBounds = true
BitmapFactory.decodeStream(it, null, options)
options.inSampleSize = calculateInSampleSize(
@@ -20,7 +22,7 @@ fun loadImageUri(cr: ContentResolver, uri: Uri): Bitmap? = try {
)
options.inJustDecodeBounds = false
}
cr.openInputStream(uri)?.use {
openInputStream(uri)?.use {
BitmapFactory.decodeStream(it, null, options)
}
} catch (e: IOException) {
@@ -47,16 +49,23 @@ private fun calculateInSampleSize(
return inSampleSize
}
fun crop(bitmap: Bitmap, rect: RectF, rotation: Float) = try {
val erected = erect(bitmap, rotation)
fun Bitmap.crop(
rect: RectF,
rotation: Float,
pivotX: Float,
pivotY: Float
) = try {
val erected = erect(rotation, pivotX, pivotY)
val w = erected.width
val h = erected.height
val x = max(0, (rect.left * w).roundToInt())
val y = max(0, (rect.top * h).roundToInt())
Bitmap.createBitmap(
erected,
(rect.left * w).roundToInt(),
(rect.top * h).roundToInt(),
(rect.width() * w).roundToInt(),
(rect.height() * h).roundToInt()
x,
y,
min(w - x, (rect.right * w).roundToInt() - x),
min(h - y, (rect.bottom * h).roundToInt() - y)
)
} catch (e: OutOfMemoryError) {
null
@@ -64,20 +73,24 @@ fun crop(bitmap: Bitmap, rect: RectF, rotation: Float) = try {
null
}
private fun erect(bitmap: Bitmap, rotation: Float): Bitmap = if (
private fun Bitmap.erect(
rotation: Float,
pivotX: Float,
pivotY: Float
): Bitmap = if (
rotation % 360f != 0f
) {
val matrix = Matrix()
matrix.setRotate(rotation)
Bitmap.createBitmap(
bitmap,
this,
0,
0,
bitmap.width,
bitmap.height,
matrix,
width,
height,
Matrix().apply {
setRotate(rotation, pivotX, pivotY)
},
true
)
} else {
bitmap
this
}
@@ -1,29 +0,0 @@
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
)
}
}
}
@@ -1,85 +1,62 @@
package de.markusfisch.android.binaryeye.graphics
import android.graphics.Point
import android.graphics.Matrix
import android.graphics.Rect
import com.google.zxing.Result
import com.google.zxing.ResultPoint
import kotlin.math.roundToInt
fun mapResult(
fun mapResultToView(
frameWidth: Int,
frameHeight: Int,
frameOrientation: Int,
viewRect: Rect,
result: Result
): List<Point> = frameToView(
resultPoints: Array<ResultPoint?>,
targetArray: FloatArray
): Int = getFrameToViewMatrix(
frameWidth,
frameHeight,
frameOrientation,
viewRect
).map(
result.resultPoints
resultPoints,
targetArray
)
fun frameToView(
fun getFrameToViewMatrix(
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 -> {
}
}
) = Matrix().apply {
// Normalize to frame dimensions for rotation.
postScale(
1f / frameWidth,
1f / frameHeight
)
// Rotate around center.
postRotate(frameOrientation.toFloat(), 0.5f, 0.5f)
// Scale up to view size.
postScale(viewRect.width().toFloat(), viewRect.height().toFloat())
// Apply view displacement.
postTranslate(viewRect.left.toFloat(), viewRect.top.toFloat())
}
fun Matrix.map(
resultPoints: Array<ResultPoint?>,
targetArray: FloatArray
): Int {
val max = targetArray.size
var i = 0
for (resultPoint in resultPoints) {
// Because ZXing apparently returns null in this array sometimes.
if (resultPoint == null) {
continue
}
targetArray[i++] = resultPoint.x
targetArray[i++] = resultPoint.y
if (i >= max) {
break
}
}
mapPoints(targetArray, 0, targetArray, 0, i)
return i
}
@@ -1,4 +1,4 @@
package de.markusfisch.android.binaryeye.app
package de.markusfisch.android.binaryeye.io
import android.app.Activity
import android.content.ContentValues
@@ -9,6 +9,7 @@ import android.provider.MediaStore
import android.support.annotation.MainThread
import android.widget.EditText
import de.markusfisch.android.binaryeye.R
import de.markusfisch.android.binaryeye.app.alertDialog
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
@@ -45,20 +46,18 @@ fun Boolean.toSaveResult() = if (this) {
R.string.error_saving_file
}
fun writeExternalFile(
context: Context,
fun Context.writeExternalFile(
fileName: String,
mimeType: String,
write: (outputStream: OutputStream) -> Unit
): Boolean = try {
openExternalOutputStream(context, fileName, mimeType).use { write(it) }
openExternalOutputStream(fileName, mimeType).use { write(it) }
true
} catch (e: IOException) {
false
}
private fun openExternalOutputStream(
context: Context,
private fun Context.openExternalOutputStream(
fileName: String,
mimeType: String
): OutputStream = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
@@ -74,7 +73,7 @@ private fun openExternalOutputStream(
}
FileOutputStream(file)
} else {
val resolver = context.contentResolver
val resolver = contentResolver
val uri = resolver.insert(
MediaStore.Downloads.EXTERNAL_CONTENT_URI,
ContentValues().apply {
@@ -0,0 +1,124 @@
package de.markusfisch.android.binaryeye.net
import de.markusfisch.android.binaryeye.app.toHexString
import de.markusfisch.android.binaryeye.app.urlEncode
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
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 + content.urlEncode()
)
}
}
private fun Scan.asUrlArguments(): String = getMap().map { (k, v) ->
"${k}=${v.urlEncode()}"
}.joinToString("&")
private fun Scan.asJson() = JSONObject().apply {
getMap().forEach { (k, v) -> put(k, v) }
}
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 dateTime
).filterNullValues()
private fun Map<String, String?>.filterNullValues() =
filterValues { it != null }.mapValues { it.value as String }
private fun request(
url: String,
writer: ((HttpURLConnection) -> Any)? = null
): Response {
var con: HttpURLConnection? = null
return try {
con = URL(url).openConnection() as HttpURLConnection
con.connectTimeout = 5000
writer?.invoke(con)
val body = con.inputStream.readHead()
Response(con.responseCode, body)
} catch (e: ProtocolException) {
Response(null, e.message)
} catch (e: IOException) {
try {
val body = con?.errorStream?.readHead() ?: e.message
Response(con?.responseCode, body)
} catch (e: IOException) {
Response(null, e.message)
}
} finally {
con?.disconnect()
}
}
private fun InputStream.readHead(): String {
val sb = StringBuilder()
val br = BufferedReader(InputStreamReader(this))
var line = br.readLine()
var i = 0
while (line != null && sb.length < 240) {
sb.append(line)
line = br.readLine()
++i
}
br.close()
return sb.toString()
}
private data class Response(val code: Int?, val body: String?)
@@ -0,0 +1,62 @@
package de.markusfisch.android.binaryeye.os
import android.content.Context
import android.os.Build
import android.os.VibrationEffect
import android.os.Vibrator
import android.os.VibratorManager
import de.markusfisch.android.binaryeye.app.prefs
private const val DURATION_IN_MS = 100L
private val beforeO = Build.VERSION.SDK_INT < Build.VERSION_CODES.O
private val errorPatternTimings = longArrayOf(
50L, 100L, 50L, 100L, 50L
)
private val errorPatternAmplitudes = intArrayOf(
255, 0, 255, 0, 255
)
fun Context.getVibrator(): Vibrator = if (
Build.VERSION.SDK_INT < Build.VERSION_CODES.S
) {
@Suppress("DEPRECATION")
getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
} else {
(getSystemService(
Context.VIBRATOR_MANAGER_SERVICE
) as VibratorManager).defaultVibrator
}
fun Vibrator.vibrate() {
if (!prefs.vibrate) {
return
} else if (beforeO) {
@Suppress("DEPRECATION")
vibrate(DURATION_IN_MS)
} else {
vibrate(
VibrationEffect.createOneShot(
DURATION_IN_MS,
VibrationEffect.DEFAULT_AMPLITUDE
)
)
}
}
fun Vibrator.error() {
if (!prefs.vibrate) {
return
} else if (beforeO) {
@Suppress("DEPRECATION")
vibrate(errorPatternTimings, -1)
} else {
vibrate(
VibrationEffect.createWaveform(
errorPatternTimings,
errorPatternAmplitudes,
-1
)
)
}
}
@@ -2,17 +2,43 @@ package de.markusfisch.android.binaryeye.preference
import android.content.Context
import android.content.SharedPreferences
import android.os.Build
import android.preference.PreferenceManager
import android.support.annotation.RequiresApi
class Preferences {
lateinit var preferences: SharedPreferences
var cropHandleX = -1
var barcodeFormats = setOf(
"AZTEC",
"CODABAR",
"CODE_39",
"CODE_93",
"CODE_128",
"DATA_MATRIX",
"EAN_8",
"EAN_13",
"ITF",
"MAXICODE",
"PDF_417",
"QR_CODE",
"RSS_14",
"RSS_EXPANDED",
"UPC_A",
"UPC_E",
"UPC_EAN_EXTENSION"
)
@RequiresApi(Build.VERSION_CODES.HONEYCOMB)
set(value) {
apply(BARCODE_FORMATS, value)
field = value
}
var cropHandleX = -2 // -2 means set default roi.
set(value) {
apply(CROP_HANDLE_X, value)
field = value
}
var cropHandleY = -1
var cropHandleY = -2
set(value) {
apply(CROP_HANDLE_Y, value)
field = value
@@ -34,18 +60,39 @@ class Preferences {
}
var autoRotate = false
set(value) {
// immediately save this setting before it shouldn't change
// on the fly while scanning
// 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
// Immediately save this setting because it's only ever read
// before the camera is opened.
commit(TRY_HARDER, value)
field = value
}
var bulkMode = false
set(value) {
// Immediately save this setting because it's only ever read
// before the camera is opened.
commit(BULK_MODE, value)
field = value
}
var bulkModeDelay = "500"
set(value) {
// Immediately save this setting because it's only ever read
// before the camera is opened.
commit(BULK_MODE_DELAY, value)
field = value
}
var showToastInBulkMode = true
set(value) {
// Immediately save this setting because it's only ever read
// before the camera is opened.
commit(SHOW_TOAST_IN_BULK_MODE, value)
field = value
}
var vibrate = true
set(value) {
apply(VIBRATE, value)
@@ -61,6 +108,11 @@ class Preferences {
apply(IGNORE_CONSECUTIVE_DUPLICATES, value)
field = value
}
var copyImmediately = false
set(value) {
apply(COPY_IMMEDIATELY, value)
field = value
}
var openImmediately = false
set(value) {
apply(OPEN_IMMEDIATELY, value)
@@ -71,25 +123,60 @@ class Preferences {
apply(SHOW_META_DATA, value)
field = value
}
var showQrVersion = false
set(value) {
apply(SHOW_QR_VERSION, 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 defaultSearchUrl = ""
set(value) {
apply(DEFAULT_SEARCH_URL, value)
field = value
}
var openWithUrl: String = ""
set(value) {
apply(OPEN_WITH_URL, value)
field = value
}
var sendScanActive = true
set(value) {
apply(SEND_SCAN_ACTIVE, 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
var forceCompat = false
set(value) {
// since the app may be about to crash when forceCompat is set,
// it's necessary to `commit()` this synchronously
// Since the app may be about to crash when forceCompat is set,
// it's necessary to `commit()` this synchronously.
commit(FORCE_COMPAT, value)
field = value
}
@@ -100,31 +187,73 @@ class Preferences {
}
fun update() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
preferences.getStringSet(BARCODE_FORMATS, barcodeFormats)?.let {
barcodeFormats = it
}
}
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)
showCropHandle = preferences.getBoolean(
SHOW_CROP_HANDLE,
showCropHandle
)
zoomBySwiping = preferences.getBoolean(ZOOM_BY_SWIPING, zoomBySwiping)
autoRotate = preferences.getBoolean(AUTO_ROTATE, autoRotate)
tryHarder = preferences.getBoolean(TRY_HARDER, tryHarder)
bulkMode = preferences.getBoolean(BULK_MODE, bulkMode)
bulkModeDelay = preferences.getString(
BULK_MODE_DELAY,
bulkModeDelay
) ?: bulkModeDelay
showToastInBulkMode = preferences.getBoolean(
SHOW_TOAST_IN_BULK_MODE,
showToastInBulkMode
)
vibrate = preferences.getBoolean(VIBRATE, vibrate)
useHistory = preferences.getBoolean(USE_HISTORY, useHistory)
ignoreConsecutiveDuplicates = preferences.getBoolean(
IGNORE_CONSECUTIVE_DUPLICATES,
ignoreConsecutiveDuplicates
)
copyImmediately = preferences.getBoolean(
COPY_IMMEDIATELY,
copyImmediately
)
openImmediately = preferences.getBoolean(
OPEN_IMMEDIATELY,
openImmediately
)
showMetaData = preferences.getBoolean(SHOW_META_DATA, showMetaData)
showQrVersion = preferences.getBoolean(SHOW_QR_VERSION, showQrVersion)
showHexDump = preferences.getBoolean(SHOW_HEX_DUMP, showHexDump)
closeAutomatically = preferences.getBoolean(
CLOSE_AUTOMATICALLY,
closeAutomatically
)
preferences.getString(DEFAULT_SEARCH_URL, defaultSearchUrl)?.also {
defaultSearchUrl = it
}
preferences.getString(OPEN_WITH_URL, openWithUrl)?.also {
openWithUrl = it
}
sendScanActive = preferences.getBoolean(
SEND_SCAN_ACTIVE,
sendScanActive
)
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
@@ -135,10 +264,17 @@ class Preferences {
private fun put(label: String, value: Boolean) =
preferences.edit().putBoolean(label, value)
private fun put(label: String, value: String) =
preferences.edit().putString(label, value)
private fun commit(label: String, value: Boolean) {
put(label, value).commit()
}
private fun commit(label: String, value: String) {
put(label, value).commit()
}
private fun apply(label: String, value: Boolean) {
put(label, value).apply()
}
@@ -151,22 +287,39 @@ class Preferences {
preferences.edit().putInt(label, value).apply()
}
@RequiresApi(Build.VERSION_CODES.HONEYCOMB)
private fun apply(label: String, value: Set<String>) {
preferences.edit().putStringSet(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 SHOW_HEX_DUMP = "show_hex_dump"
const val SHOW_META_DATA = "show_meta_data"
const val OPEN_WITH_URL = "open_with_url"
const val INDEX_OF_LAST_SELECTED_FORMAT = "index_of_last_selected_format"
const val FORCE_COMPAT = "force_compat"
private const val BARCODE_FORMATS = "barcode_formats"
private const val CROP_HANDLE_X = "crop_handle_x"
private const val CROP_HANDLE_Y = "crop_handle_y"
private const val CROP_HANDLE_ORIENTATION = "crop_handle_orientation"
private const val SHOW_CROP_HANDLE = "show_crop_handle"
private const val ZOOM_BY_SWIPING = "zoom_by_swiping"
private const val AUTO_ROTATE = "auto_rotate"
private const val TRY_HARDER = "try_harder"
private const val BULK_MODE = "bulk_mode"
private const val BULK_MODE_DELAY = "bulk_mode_delay"
private const val SHOW_TOAST_IN_BULK_MODE = "show_toast_in_bulk_mode"
private const val VIBRATE = "vibrate"
private const val USE_HISTORY = "use_history"
private const val IGNORE_CONSECUTIVE_DUPLICATES = "ignore_consecutive_duplicates"
private const val OPEN_IMMEDIATELY = "open_immediately"
private const val COPY_IMMEDIATELY = "copy_immediately"
private const val SHOW_META_DATA = "show_meta_data"
private const val SHOW_QR_VERSION = "show_qr_version"
private const val SHOW_HEX_DUMP = "show_hex_dump"
private const val CLOSE_AUTOMATICALLY = "close_automatically"
private const val DEFAULT_SEARCH_URL = "default_search_url"
private const val OPEN_WITH_URL = "open_with_url"
private const val SEND_SCAN_ACTIVE = "send_scan_active"
private const val SEND_SCAN_URL = "send_scan_url"
private const val SEND_SCAN_TYPE = "send_scan_type"
private const val CUSTOM_LOCALE = "custom_locale"
private const val INDEX_OF_LAST_SELECTED_FORMAT = "index_of_last_selected_format"
private 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,6 +1,5 @@
package de.markusfisch.android.binaryeye.rs
import android.content.Context
import android.graphics.Rect
import android.support.v8.renderscript.*
import kotlin.math.max
@@ -9,7 +8,7 @@ import kotlin.math.roundToInt
private const val SCALE_FACTOR = .75f
class Preprocessor(
context: Context,
rs: RenderScript,
width: Int,
height: Int,
private val roi: Rect?
@@ -17,7 +16,6 @@ class Preprocessor(
var outWidth = 0
var outHeight = 0
private val rs = RenderScript.create(context)
private val resizeScript = ScriptIntrinsicResize.create(rs)
private val rotateScript = ScriptC_rotate(rs)
@@ -62,11 +60,11 @@ class Preprocessor(
outWidth = (roiWidth * SCALE_FACTOR).roundToInt()
outHeight = (roiHeight * SCALE_FACTOR).roundToInt()
// make sure the dimensions are always a multiple of 4
// 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
// Make sure the dimensions are always greater than 4.
outWidth = max(4, outWidth)
outHeight = max(4, outHeight)
} else {
@@ -118,7 +116,6 @@ class Preprocessor(
rotatedAlloc = null
resizeScript.destroy()
rotateScript.destroy()
rs.destroy()
}
fun resizeOnly(frame: ByteArray) {
@@ -133,7 +130,7 @@ class Preprocessor(
rotateScript._inWidth = t.x
rotateScript._inHeight = t.y
rotateScript.forEach_rotate90(
rotatedAlloc, // ignored in kernel, just to satisfy forEach
rotatedAlloc, // Ignored in kernel, just to satisfy forEach.
rotatedAlloc
)
rotatedAlloc?.copyTo(frame)
@@ -147,7 +144,7 @@ class Preprocessor(
0, 0,
roi.width(), roi.height(),
yuvAlloc,
roi.left, roi.top
max(0, roi.left), max(0, roi.top)
)
roiAlloc
} else {
@@ -0,0 +1,18 @@
package de.markusfisch.android.binaryeye.service
import android.content.Intent
import android.os.Build
import android.service.quicksettings.TileService
import android.support.annotation.RequiresApi
import de.markusfisch.android.binaryeye.activity.CameraActivity
@RequiresApi(Build.VERSION_CODES.N)
class ScanTileService : TileService() {
@RequiresApi(Build.VERSION_CODES.N)
override fun onClick() {
super.onClick()
val intent = Intent(applicationContext, CameraActivity::class.java)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
startActivityAndCollapse(intent)
}
}
@@ -1,4 +1,4 @@
package de.markusfisch.android.binaryeye.app
package de.markusfisch.android.binaryeye.view
import android.content.Context
import android.view.View
@@ -1,4 +1,4 @@
package de.markusfisch.android.binaryeye.app
package de.markusfisch.android.binaryeye.view
import android.content.Context
import android.content.ContextWrapper
@@ -6,35 +6,44 @@ import android.graphics.drawable.ColorDrawable
import android.os.Build
import android.support.v4.content.ContextCompat
import android.support.v7.app.AppCompatActivity
import android.support.v7.widget.LinearLayoutManager
import android.support.v7.widget.RecyclerView
import android.view.View
import android.view.WindowManager
import android.widget.AbsListView
import de.markusfisch.android.binaryeye.R
val systemBarScrollListener = object : AbsListView.OnScrollListener {
val systemBarListViewScrollListener = object : AbsListView.OnScrollListener {
override fun onScroll(
view: AbsListView,
firstVisibleItem: Int,
visibleItemCount: Int,
totalItemCount: Int
) {
// give Android some time to settle down before running this,
// not putting it on the queue makes it only work sometimes
// Give Android some time to settle down before running this,
// not putting it on the queue makes it only work sometimes.
view.post {
val scrolled = firstVisibleItem > 0 ||
(totalItemCount > 0 && firstChildScrolled(view))
val scrollable = if (scrolled) {
true
} else {
totalItemCount > 0 && lastChildOutOfView(view)
}
val scrollable = scrolled || totalItemCount > 0 && lastChildOutOfView(view)
colorSystemAndToolBars(view.context, scrolled, scrollable)
}
}
override fun onScrollStateChanged(
view: AbsListView,
scrollState: Int
) {
override fun onScrollStateChanged(view: AbsListView, scrollState: Int) {
}
}
val systemBarRecyclerViewScrollListener = object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
val layoutManager = recyclerView.layoutManager as LinearLayoutManager
val scrolled = layoutManager.findFirstCompletelyVisibleItemPosition() != 0
val scrollable = scrolled || layoutManager.findLastVisibleItemPosition() <
recyclerView.adapter.itemCount - 1
colorSystemAndToolBars(recyclerView.context, scrolled, scrollable)
}
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
}
}
@@ -50,7 +59,12 @@ private fun lastChildOutOfView(listView: AbsListView): Boolean {
fun initSystemBars(activity: AppCompatActivity) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
activity.window.decorView.systemUiVisibility =
// Keeps the soft keyboard from repositioning the layout.
val window = activity.window
window.setSoftInputMode(
WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE
)
window.decorView.systemUiVisibility =
View.SYSTEM_UI_FLAG_LAYOUT_STABLE or
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
@@ -95,11 +109,11 @@ fun colorSystemAndToolBars(
}
activity.supportActionBar?.setBackgroundDrawable(
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
// avoid allocation on Honeycomb and better
// Avoid allocation on Honeycomb and better.
actionBarBackground.color = topColor
actionBarBackground
} else {
// ColorDrawable.setColor() doesn't exist pre Honeycomb
// ColorDrawable.setColor() doesn't exist pre Honeycomb.
ColorDrawable(topColor)
}
)
@@ -29,10 +29,10 @@ fun View.doOnApplyWindowInsets(f: (View, Rect) -> Unit) {
f(v, insetsWithToolbar(insets))
insets
}
// it's important to explicitly request the insets (again) in
// It's important to explicitly request the insets (again) in
// case the view was created in Fragment.onCreateView() because
// setOnApplyWindowInsetsListener() won't fire when the view
// isn't attached
// isn't attached.
requestApplyInsetsWhenAttached()
}
}
@@ -4,7 +4,7 @@ import android.content.Context
import android.graphics.Canvas
import android.graphics.Rect
import android.util.AttributeSet
import de.markusfisch.android.binaryeye.app.colorSystemAndToolBars
import de.markusfisch.android.binaryeye.view.colorSystemAndToolBars
import de.markusfisch.android.scalingimageview.widget.ScalingImageView
open class ConfinedScalingImageView : ScalingImageView {
@@ -3,7 +3,7 @@ package de.markusfisch.android.binaryeye.widget
import android.content.Context
import android.util.AttributeSet
import android.widget.ScrollView
import de.markusfisch.android.binaryeye.app.colorSystemAndToolBars
import de.markusfisch.android.binaryeye.view.colorSystemAndToolBars
class ConfinedScrollView : ScrollView {
private var scrollable = false
@@ -23,8 +23,8 @@ class ConfinedScrollView : ScrollView {
) {
super.onLayout(changed, left, top, right, bottom)
if (changed) {
// give Android some time to settle down before running this,
// not putting it on the queue makes it only work sometimes
// Give Android some time to settle down before running this,
// not putting it on the queue makes it only work sometimes.
post {
getChildAt(0)?.also { child ->
scrollable = height < child.height + paddingTop + paddingBottom
@@ -2,53 +2,18 @@ package de.markusfisch.android.binaryeye.widget
import android.content.Context
import android.graphics.Canvas
import android.graphics.Point
import android.graphics.Rect
import android.graphics.RectF
import android.util.AttributeSet
import de.markusfisch.android.binaryeye.graphics.Dots
import de.markusfisch.android.binaryeye.graphics.getDashedBorderPaint
import kotlin.math.roundToInt
class CropImageView(context: Context, attr: AttributeSet) :
ConfinedScalingImageView(context, attr) {
val windowInsets = Rect()
var onScan: (() -> Unit)? = null
private val dots = Dots(context)
private val boundsPaint = context.getDashedBorderPaint()
private val lastMappedRect = RectF()
private val padding: Int = (24f * context.resources.displayMetrics.density).roundToInt()
private val onScanRunnable = Runnable { onScan?.invoke() }
private var resultPoints: List<Point>? = null
init {
scaleType = ScaleType.CENTER_CROP
}
fun updateResultPoints(points: List<Point>) {
resultPoints = points
invalidate()
}
override fun onLayout(
changed: Boolean,
left: Int,
top: Int,
right: Int,
bottom: Int
) {
super.onLayout(changed, left, top, right, bottom)
setBoundsWithPadding(
left + windowInsets.left,
top + windowInsets.top,
right - windowInsets.right,
bottom - windowInsets.bottom
)
}
fun getBoundsRect() = Rect(
bounds.left.roundToInt(),
bounds.top.roundToInt(),
@@ -56,40 +21,12 @@ class CropImageView(context: Context, attr: AttributeSet) :
bounds.bottom.roundToInt()
)
private fun setBoundsWithPadding(
left: Int,
top: Int,
right: Int,
bottom: Int
) {
val width = right - left
val height = bottom - top
val size = if (width < height)
width - padding * 2
else
height - padding * 2
val hpad = (width - size) / 2
val vpad = (height - size) / 2
setBounds(
left.toFloat() + hpad,
top.toFloat() + vpad,
right.toFloat() - hpad,
bottom.toFloat() - vpad
)
center(bounds)
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
canvas.drawRect(bounds, boundsPaint)
resultPoints?.let {
dots.draw(canvas, it)
}
resultPoints = null
val mr = mappedRect ?: return
if (mr != lastMappedRect) {
removeCallbacks(onScanRunnable)
postDelayed(onScanRunnable, 500)
postDelayed(onScanRunnable, 300)
lastMappedRect.set(mr)
}
}
@@ -6,12 +6,12 @@ import android.graphics.*
import android.os.Build
import android.os.Parcel
import android.os.Parcelable
import android.support.v4.content.ContextCompat
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
@@ -20,19 +20,23 @@ import kotlin.math.min
import kotlin.math.roundToInt
class DetectorView : View {
val currentOrientation = resources.configuration.orientation
val coordinates = FloatArray(32)
val roi = Rect()
var onRoiChange: (() -> Unit)? = null
var onRoiChanged: (() -> Unit)? = null
private val orientation = resources.configuration.orientation
private val dots = Dots(context)
private val currentOrientation = resources.configuration.orientation
private val invalidateRunnable: Runnable = Runnable {
marks = null
coordinatesLast = 0
invalidate()
}
private val roiPaint = context.getDashedBorderPaint()
private val dotPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = ContextCompat.getColor(context, R.color.dot)
style = Paint.Style.FILL
}
private val dotRadius: Float
private val handleBitmap = resources.getBitmapFromDrawable(
R.drawable.button_crop
)
@@ -48,7 +52,7 @@ class DetectorView : View {
private val fabHeight: Int
private val padding: Int
private var marks: List<Point>? = null
private var coordinatesLast = 0
private var handleGrabbed = false
private var handleActive = false
private var minY = 0
@@ -58,6 +62,7 @@ class DetectorView : View {
init {
val dp = context.resources.displayMetrics.density
dotRadius = 8f * dp
distToFull = (24f * dp).roundToInt()
val minMoveThreshold = (8f * dp).roundToInt()
minMoveThresholdSq = minMoveThreshold * minMoveThreshold
@@ -73,25 +78,41 @@ class DetectorView : View {
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) :
super(context, attrs, defStyleAttr)
fun setCropHandlePos(x: Int, y: Int, orientation: Int) {
if (orientation == currentOrientation) {
handlePos.set(x, y)
} else {
handlePos.set(y, x)
}
if (x > -1) {
handleActive = true
}
fun saveCropHandlePos() {
val pos = getCropHandlePos()
prefs.cropHandleX = pos.x
prefs.cropHandleY = pos.y
prefs.cropHandleOrientation = currentOrientation
}
fun getCropHandlePos() = if (handleActive) {
private fun getCropHandlePos() = if (handleActive) {
handlePos
} else {
Point(-1, -1)
}
fun mark(points: List<Point>) {
marks = points
fun restoreCropHandlePos() {
setCropHandlePos(
prefs.cropHandleX,
prefs.cropHandleY,
prefs.cropHandleOrientation
)
}
private fun setCropHandlePos(x: Int, y: Int, orientation: Int) {
// Always set handlePos even if it's invalid because
// handlePos.x may be -2 which is a signal to set the
// default ROI.
if (orientation == currentOrientation) {
handlePos.set(x, y)
} else {
handlePos.set(y, x)
}
handleActive = handlePos.x > -1
}
fun update(numberOfCoordinates: Int) {
coordinatesLast = numberOfCoordinates
invalidate()
removeCallbacks(invalidateRunnable)
postDelayed(invalidateRunnable, 500)
@@ -144,11 +165,12 @@ class DetectorView : View {
MotionEvent.ACTION_MOVE -> {
if (handleGrabbed) {
handlePos.set(x, y)
if (distSq(handlePos, touchDown) > minMoveThresholdSq) {
handleActive = true
handleActive = handleActive ||
distSq(handlePos, touchDown) > minMoveThresholdSq
if (handleActive) {
updateClipRect()
invalidate()
}
updateClipRect()
invalidate()
true
} else {
false
@@ -164,17 +186,14 @@ class DetectorView : View {
MotionEvent.ACTION_UP -> {
if (handleGrabbed) {
if (!handleActive) {
val mn = min(center.x, center.y) * .8f
handlePos.set(
(center.x + mn).roundToInt(),
(center.y + mn).roundToInt()
)
handleActive = true
invalidate()
setHandleToDefaultRoi()
} else {
snap(x, y)
}
updateClipRect()
if (handleActive) {
updateClipRect()
invalidate()
}
onRoiChanged?.invoke()
handleGrabbed = false
}
@@ -184,9 +203,26 @@ class DetectorView : View {
}
}
private fun setHandleToDefaultRoi() {
val mn = min(center.x, center.y) * .8f
handlePos.set(
(center.x + mn).roundToInt(),
(center.y + mn).roundToInt()
)
handleActive = true
}
private fun snap(x: Int, y: Int) {
if (abs(x - center.x) < distToFull ||
abs(y - center.y) < distToFull
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()
@@ -201,9 +237,6 @@ class DetectorView : View {
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
super.onLayout(changed, left, top, right, bottom)
if (!changed) {
return
}
val width = right - left
val height = bottom - top
center.set(
@@ -216,6 +249,9 @@ class DetectorView : View {
width - handleXRadius - paddingRight - padding,
height - handleYRadius - paddingBottom - fabHeight
)
if (handlePos.x == -2) {
setHandleToDefaultRoi()
}
if (handleActive) {
updateClipRect()
} else {
@@ -224,32 +260,36 @@ class DetectorView : View {
}
override fun onDraw(canvas: Canvas) {
canvas.drawColor(0, PorterDuff.Mode.CLEAR)
if (handleActive) {
drawClip(canvas)
}
marks?.let {
dots.draw(canvas, it)
canvas.drawClip()
}
canvas.drawDots()
if (prefs.showCropHandle) {
canvas.drawBitmap(
handleBitmap,
(handlePos.x - handleXRadius).toFloat(),
(handlePos.y - handleYRadius).toFloat(),
null
canvas.drawHandle()
}
}
private fun Canvas.drawDots() {
var i = 0
while (i < coordinatesLast) {
drawCircle(
coordinates[i++],
coordinates[i++],
dotRadius,
dotPaint
)
}
}
private fun drawClip(canvas: Canvas) {
private fun Canvas.drawClip() {
if (minDist < 1) {
return
}
// canvas.clipRect() doesn't work reliably below KITKAT
// 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(
save()
clipOutPathCompat(
calculateRoundedRectPath(
roi.left.toFloat(),
roi.top.toFloat(),
@@ -259,13 +299,22 @@ class DetectorView : View {
radius
)
)
canvas.drawColor(shadeColor, PorterDuff.Mode.SRC)
canvas.restore()
drawColor(shadeColor)
restore()
} else {
canvas.drawRect(roi, roiPaint)
drawRect(roi, roiPaint)
}
}
private fun Canvas.drawHandle() {
drawBitmap(
handleBitmap,
(handlePos.x - handleXRadius).toFloat(),
(handlePos.y - handleYRadius).toFloat(),
null
)
}
private fun updateClipRect() {
clampHandlePos()
val dx = abs(handlePos.x - center.x)
@@ -284,10 +333,14 @@ class DetectorView : View {
}
private fun clampHandlePos() {
handlePos.x = min(center.x * 2, max(0, handlePos.x))
handlePos.y = min(maxY, max(minY, handlePos.y))
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()
@@ -4,13 +4,19 @@ import android.content.Context
import android.widget.Toast
fun Context.toast(message: Int) = Toast.makeText(
this,
applicationContext,
message,
Toast.LENGTH_LONG
).show()
fun Context.toast(message: String) = Toast.makeText(
this,
message,
applicationContext,
message.ellipsize(128),
Toast.LENGTH_LONG
).show()
private fun String.ellipsize(max: Int) = if (length < max) {
this
} else {
"${take(max)}"
}
@@ -2,6 +2,7 @@ package de.markusfisch.android.binaryeye.zxing
import android.graphics.Bitmap
import com.google.zxing.*
import com.google.zxing.common.BitMatrix
import com.google.zxing.common.HybridBinarizer
import java.util.*
@@ -10,44 +11,30 @@ class Zxing(possibleResultPoint: ResultPointCallback? = null) {
private val multiFormatReader = MultiFormatReader()
init {
val decodeFormats = EnumSet.noneOf<BarcodeFormat>(
BarcodeFormat::class.java
)
decodeFormats.addAll(
EnumSet.copyOf(
listOf(
BarcodeFormat.AZTEC,
BarcodeFormat.CODABAR,
BarcodeFormat.CODE_39,
BarcodeFormat.CODE_93,
BarcodeFormat.CODE_128,
BarcodeFormat.DATA_MATRIX,
BarcodeFormat.EAN_8,
BarcodeFormat.EAN_13,
BarcodeFormat.ITF,
BarcodeFormat.MAXICODE,
BarcodeFormat.PDF_417,
BarcodeFormat.QR_CODE,
BarcodeFormat.RSS_14,
BarcodeFormat.RSS_EXPANDED,
BarcodeFormat.UPC_A,
BarcodeFormat.UPC_E,
BarcodeFormat.UPC_EAN_EXTENSION
)
)
)
hints[DecodeHintType.POSSIBLE_FORMATS] = decodeFormats
possibleResultPoint?.let {
hints[DecodeHintType.NEED_RESULT_POINT_CALLBACK] = it
}
}
fun updateHints(tryHarder: Boolean) {
fun updateHints(tryHarder: Boolean, formats: Set<String>) {
if (tryHarder) {
hints[DecodeHintType.TRY_HARDER] = java.lang.Boolean.TRUE
} else {
hints.remove(DecodeHintType.TRY_HARDER)
}
setDecodeFormats(formats)
}
private fun setDecodeFormats(formats: Set<String>) {
hints[DecodeHintType.POSSIBLE_FORMATS] = EnumSet.noneOf(
BarcodeFormat::class.java
).apply {
addAll(
EnumSet.copyOf(
formats.map { BarcodeFormat.valueOf(it) }
)
)
}
}
fun decode(
@@ -55,8 +42,8 @@ class Zxing(possibleResultPoint: ResultPointCallback? = null) {
width: Int,
height: Int,
invert: Boolean = false
): Result? {
val source = PlanarYUVLuminanceSource(
): Result? = decodeLuminanceSource(
PlanarYUVLuminanceSource(
yuvData,
width,
height,
@@ -65,25 +52,20 @@ class Zxing(possibleResultPoint: ResultPointCallback? = null) {
width,
height,
false
)
return decodeLuminanceSource(source, invert)
}
),
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
val pixels = IntArray(width * height)
if (bitmap.config != Bitmap.Config.ARGB_8888) {
bitmap.copy(Bitmap.Config.ARGB_8888, true)
} else {
@@ -98,100 +80,117 @@ class Zxing(possibleResultPoint: ResultPointCallback? = null) {
private fun decodeLuminanceSource(
source: LuminanceSource,
invert: Boolean
): Result? {
return decodeLuminanceSource(
if (invert) {
source.invert()
} else {
source
}
): Result? = decodeLuminanceSource(
if (invert) {
source.invert()
} else {
source
}
)
private fun decodeLuminanceSource(source: LuminanceSource): Result? = try {
multiFormatReader.decode(
BinaryBitmap(HybridBinarizer(source)),
hints
)
}
private fun decodeLuminanceSource(source: LuminanceSource): Result? {
val bitmap = BinaryBitmap(HybridBinarizer(source))
return try {
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()
fun encodeAsBitmap(
text: String,
format: BarcodeFormat,
width: Int,
height: Int
): Bitmap? {
val hints = EnumMap<EncodeHintType, Any>(EncodeHintType::class.java)
hints[EncodeHintType.CHARACTER_SET] = "utf-8"
val result = MultiFormatWriter().encode(
text,
format,
width,
height,
hints
)
val w = result.width
val h = result.height
val pixels = IntArray(w * h)
var offset = 0
for (y in 0 until h) {
for (x in 0 until w) {
pixels[offset + x] = if (result.get(x, y)) {
BLACK
} else {
WHITE
}
}
offset += w
}
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,
width: Int,
height: Int
): String {
val hints = EnumMap<EncodeHintType, Any>(EncodeHintType::class.java)
hints[EncodeHintType.CHARACTER_SET] = "utf-8"
val result = MultiFormatWriter().encode(
text,
format,
0,
0,
hints
)
val sb = StringBuilder()
sb.append("<svg width=\"$width\" height=\"$height\"")
sb.append(" viewBox=\"0 0 $width $height\"")
sb.append(" xmlns=\"http://www.w3.org/2000/svg\">\n")
val w = result.width
val h = result.height
val xf = width.toFloat() / w
val yf = height.toFloat() / h
for (y in 0 until h) {
for (x in 0 until w) {
if (result.get(x, y)) {
val ox = x * xf
val oy = y * yf
sb.append("<rect x=\"$ox\" y=\"$oy\"")
sb.append(" width=\"$xf\" height=\"$yf\"/>\n")
}
}
}
sb.append("</svg>\n")
return sb.toString()
}
} catch (e: Exception) {
// Usually it's bad practice to blindly catch *all* exceptions
// because this can hide problems we want to know about. But
// since ZXing has some errors (like all software) and I don't
// want this app to break in the hands of my users, which won't
// see the stack trace anyway, it's better to just catch all
// exceptions here and live on.
null
} finally {
multiFormatReader.reset()
}
}
private const val BLACK = 0xff000000.toInt()
private const val WHITE = 0xffffffff.toInt()
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")
}
}
}
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 = MultiFormatWriter().encode(
text, format, width, height,
(encodeHints ?: EnumMap<EncodeHintType, Any>(EncodeHintType::class.java)).apply {
if (!contains(EncodeHintType.CHARACTER_SET)) {
this[EncodeHintType.CHARACTER_SET] = "utf-8"
}
}
)
@@ -0,0 +1,4 @@
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/accent"/>
<foreground android:drawable="@drawable/ic_shortcut_decode_foreground"/>
</adaptive-icon>
@@ -0,0 +1,4 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="108dp" android:height="108dp" android:viewportWidth="108" android:viewportHeight="108">
<path android:pathData="M48.281 55.787a5.719 5.719 0 1 1 11.438 0 5.719 5.719 0 1 1-11.438 0" android:fillColor="#ffffff"/>
<path android:pathData="M48.639 37.916l-3.271 3.575h-5.665c-1.965 0-3.574 1.608-3.574 3.574v21.444c0 1.966 1.609 3.575 3.574 3.575h28.594c1.965 0 3.574-1.609 3.574-3.575V45.065c0-1.966-1.609-3.574-3.574-3.574h-5.665l-3.271-3.575zM54 64.722c-4.932 0-8.935-4.003-8.935-8.935s4.003-8.935 8.935-8.935 8.935 4.003 8.935 8.935-4.003 8.935-8.935 8.935z" android:fillColor="#ffffff"/>
</vector>
@@ -0,0 +1,4 @@
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/accent"/>
<foreground android:drawable="@drawable/ic_shortcut_encode_foreground"/>
</adaptive-icon>
@@ -0,0 +1,3 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="108dp" android:height="108dp" android:viewportWidth="108" android:viewportHeight="108">
<path android:pathData="M69.75 56.25h-13.5v13.5h-4.5v-13.5h-13.5v-4.5h13.5v-13.5h4.5v13.5h13.5z" android:fillColor="#FFFFFF"/>
</vector>
@@ -0,0 +1,4 @@
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/accent"/>
<foreground android:drawable="@drawable/ic_shortcut_preferences_foreground"/>
</adaptive-icon>
@@ -0,0 +1,3 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="108dp" android:height="108dp" android:viewportWidth="108" android:viewportHeight="108">
<path android:pathData="M70.057 56.114c0.09-0.675 0.135-1.372 0.135-2.114 0-0.719-0.045-1.439-0.158-2.114l4.566-3.553c0.405-0.315 0.517-0.922 0.27-1.372l-4.318-7.466C70.282 39 69.72 38.843 69.225 39l-5.375 2.159c-1.124-0.854-2.316-1.574-3.643-2.114l-0.81-5.712c-0.09-0.54-0.539-0.922-1.079-0.922h-8.636c-0.54 0-0.967 0.382-1.057 0.922l-0.809 5.712c-1.327 0.54-2.542 1.282-3.644 2.114L38.798 39c-0.495-0.18-1.057 0-1.327 0.495l-4.296 7.466c-0.269 0.472-0.179 1.057 0.27 1.372l4.566 3.553c-0.113 0.675-0.203 1.417-0.203 2.114 0 0.697 0.045 1.439 0.158 2.114L33.4 59.667c-0.404 0.315-0.517 0.922-0.27 1.372l4.318 7.466C37.718 69 38.28 69.158 38.775 69l5.375-2.159c1.124 0.855 2.316 1.574 3.643 2.114l0.81 5.712c0.112 0.54 0.539 0.922 1.079 0.922h8.636c0.54 0 0.989-0.382 1.057-0.922l0.809-5.712c1.327-0.54 2.542-1.259 3.644-2.114L69.202 69c0.495 0.18 1.057 0 1.327-0.495l4.318-7.466c0.27-0.495 0.158-1.057-0.27-1.372zM54 62.096c-4.453 0-8.096-3.643-8.096-8.096s3.643-8.096 8.096-8.096 8.096 3.643 8.096 8.096-3.643 8.096-8.096 8.096z" android:fillColor="#ffffff"/>
</vector>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.8 KiB

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 16 KiB

@@ -0,0 +1,3 @@
<vector android:height="24dp" android:tint="#FFFFFF" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FFFFFF" android:pathData="M12.65 10C11.83 7.67 9.61 6 7 6c-3.31 0-6 2.69-6 6s2.69 6 6 6c2.61 0 4.83-1.67 5.65-4H17v4h4v-4h2v-4H12.65zM7 14c-1.1 0-2-0.9-2-2s0.9-2 2-2 2 0.9 2 2-0.9 2-2 2z"/>
</vector>
@@ -0,0 +1,3 @@
<vector android:height="24dp" android:tint="#FFFFFF" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M3 11h8V3H3v8zm2-6h4v4H5V5zM3 21h8v-8H3v8zm2-6h4v4H5v-4zm8-12v8h8V3h-8zm6 6h-4V5h4v4zm0 10h2v2h-2zm-6-6h2v2h-2zm2 2h2v2h-2zm-2 2h2v2h-2zm2 2h2v2h-2zm2-2h2v2h-2zm0-4h2v2h-2zm2 2h2v2h-2z"/>
</vector>
@@ -1,3 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24">
<path android:fillColor="#FFFFFFFF" android:pathData="M4.413 15.067h-0.88v1.93h-1.37v-5.69h2.27c0.433 0 0.817 0.08 1.15 0.24 0.34 0.16 0.6 0.39 0.78 0.69 0.187 0.293 0.28 0.627 0.28 1 0 0.553-0.2 0.997-0.6 1.33-0.4 0.333-0.943 0.5-1.63 0.5zm-0.88-2.7v1.64h0.9c0.267 0 0.47-0.067 0.61-0.2s0.21-0.32 0.21-0.56c0-0.267-0.073-0.48-0.22-0.64-0.14-0.16-0.337-0.24-0.59-0.24h-0.91zm8.587-1.06v5.69h-1.36l-2.02-3.54v3.54H7.37v-5.69h1.37l2.01 3.54v-3.54h1.37zm5.527 2.65v2.34c-0.213 0.233-0.523 0.42-0.93 0.56-0.407 0.147-0.853 0.22-1.34 0.22-0.74 0-1.333-0.23-1.78-0.69-0.447-0.453-0.687-1.087-0.72-1.9v-0.49c0-0.56 0.1-1.05 0.3-1.47 0.193-0.413 0.477-0.733 0.85-0.96 0.367-0.227 0.793-0.34 1.28-0.34 0.707 0 1.257 0.163 1.65 0.49 0.387 0.32 0.613 0.803 0.68 1.45h-1.32c-0.047-0.32-0.147-0.547-0.3-0.68-0.16-0.133-0.383-0.2-0.67-0.2-0.34 0-0.603 0.143-0.79 0.43-0.193 0.293-0.29 0.71-0.29 1.25v0.35c0 0.567 0.097 0.993 0.29 1.28 0.193 0.28 0.5 0.42 0.92 0.42 0.353 0 0.617-0.077 0.79-0.23v-0.89h-0.95v-0.94h2.33zm4.733-6.991L18.481 6.96l1.463 1.464-2.065 2.066 0.975 0.975L20.921 9.4l1.463 1.464-0.005-3.898z"/>
</vector>
@@ -1,3 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24">
<path android:fillColor="#FFFFFFFF" android:pathData="M5.45 15.487c0-0.2-0.07-0.357-0.21-0.47-0.14-0.113-0.39-0.23-0.75-0.35-0.353-0.12-0.643-0.237-0.87-0.35-0.74-0.36-1.11-0.857-1.11-1.49 0-0.313 0.093-0.59 0.28-0.83 0.18-0.24 0.44-0.427 0.78-0.56 0.333-0.14 0.71-0.21 1.13-0.21 0.407 0 0.773 0.073 1.1 0.22 0.327 0.147 0.577 0.357 0.75 0.63 0.18 0.267 0.27 0.573 0.27 0.92H5.46c0-0.233-0.073-0.413-0.22-0.54-0.14-0.127-0.33-0.19-0.57-0.19-0.247 0-0.44 0.053-0.58 0.16-0.14 0.107-0.21 0.243-0.21 0.41 0 0.147 0.077 0.28 0.23 0.4 0.153 0.12 0.427 0.243 0.82 0.37 0.393 0.127 0.717 0.263 0.97 0.41 0.613 0.353 0.92 0.84 0.92 1.46 0 0.5-0.187 0.89-0.56 1.17-0.373 0.287-0.887 0.43-1.54 0.43-0.46 0-0.877-0.083-1.25-0.25-0.373-0.167-0.657-0.393-0.85-0.68-0.187-0.287-0.28-0.617-0.28-0.99h1.38c0 0.3 0.077 0.523 0.23 0.67 0.16 0.147 0.417 0.22 0.77 0.22 0.227 0 0.407-0.05 0.54-0.15 0.127-0.1 0.19-0.237 0.19-0.41zm3.158-4.18l1.13 4.14 1.13-4.14h1.53l-1.91 5.69h-1.5l-1.9-5.69h1.52zm8.814 2.65v2.34c-0.213 0.233-0.523 0.42-0.93 0.56-0.407 0.147-0.853 0.22-1.34 0.22-0.74 0-1.333-0.23-1.78-0.69-0.447-0.453-0.687-1.087-0.72-1.9v-0.49c0-0.56 0.1-1.05 0.3-1.47 0.193-0.413 0.477-0.733 0.85-0.96 0.367-0.227 0.793-0.34 1.28-0.34 0.707 0 1.257 0.163 1.65 0.49 0.387 0.32 0.613 0.803 0.68 1.45h-1.32c-0.047-0.32-0.147-0.547-0.3-0.68-0.16-0.133-0.383-0.2-0.67-0.2-0.34 0-0.603 0.143-0.79 0.43-0.193 0.293-0.29 0.71-0.29 1.25v0.35c0 0.567 0.097 0.993 0.29 1.28 0.193 0.28 0.5 0.42 0.92 0.42 0.353 0 0.617-0.077 0.79-0.23v-0.89h-0.95v-0.94h2.33zm4.164-7.051L17.69 6.9l1.463 1.464-2.065 2.066 0.975 0.975 2.065-2.066 1.464 1.464-0.005-3.898z"/>
</vector>
+2 -2
View File
@@ -1,3 +1,3 @@
<vector android:height="24dp" android:tint="#FFFFFF" android:viewportHeight="24.0" android:viewportWidth="24.0" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M15.5 14h-0.79l-0.28-0.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-0.59 4.23-1.57L14 14.71v0.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"/>
<vector android:height="24dp" android:tint="#FFFFFF" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M3 11h8V3H3v8zm2-6h4v4H5V5zM3 21h8v-8H3v8zm2-6h4v4H5v-4zm8-12v8h8V3h-8zm6 6h-4V5h4v4zm0 10h2v2h-2zm-6-6h2v2h-2zm2 2h2v2h-2zm-2 2h2v2h-2zm2 2h2v2h-2zm2-2h2v2h-2zm0-4h2v2h-2zm2 2h2v2h-2z"/>
</vector>
@@ -1,3 +0,0 @@
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="#b6d46f"/>
</shape>
@@ -1,14 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="108dp" android:height="108dp" android:viewportWidth="108" android:viewportHeight="108">
<path android:fillColor="#FF000000" android:pathData="M52-884.362h29v71H52z"/>
<path android:fillColor="#FF000000" android:pathData="M52-884.362h29v71H52z"/>
<path android:fillColor="#FF000000" android:pathData="M52-884.362h29v71H52zM32.002-24.974h3.127v7.656h-3.127z"/>
<path android:fillColor="#FF000000" android:pathData="M52-884.362h29v71H52z"/>
<path android:fillColor="#FF000000" android:pathData="M52-884.362h29v71H52z"/>
<path android:fillColor="#FF000000" android:pathData="M52-884.362h29v71H52z"/>
<path android:fillColor="#FF000000" android:pathData="M52-884.362h29v71H52z"/>
<path android:fillColor="#FF000000" android:pathData="M52-884.362h29v71H52z"/>
<path android:pathData="M54.826 27.893c-6.851 0-13.701 2.581-18.924 7.745-10.445 10.328-10.413 27.062 0 37.423l36.218 36.036 37.879 0.902-0.07-38.363-36.18-35.998c-5.207-5.18-12.073-7.745-18.924-7.745z" android:strokeWidth="8.60725021" android:fillColor="#000000" android:fillAlpha="0.1254902"/>
<path android:pathData="M28.001 54.118a26.525 26.227 0 1 0 53.049 0 26.525 26.227 0 1 0-53.049 0z" android:strokeWidth="4" android:fillColor="#ffffff"/>
<path android:pathData="M39.26 54.988a14.509 14.346 0 1 0 29.019 0 14.509 14.346 0 1 0-29.019 0z" android:strokeWidth="4" android:fillColor="#000000"/>
<path android:pathData="M54.034 44.513a9.535 9.428 0 1 0 19.07 0 9.535 9.428 0 1 0-19.07 0z" android:strokeWidth="10" android:fillColor="#ffffff"/>
<path android:pathData="M54.723 28.295c-6.85 0-13.701 2.581-18.923 7.745-10.446 10.328-10.414 27.062 0 37.423l36.218 36.036L108 108l1.827-35.962-36.18-35.998c-5.207-5.18-12.073-7.745-18.924-7.745z" android:strokeWidth="8.60725021" android:fillColor="#000000" android:fillAlpha="0.1254902"/>
<path android:pathData="M80.5 54a26.5 26.5 0 1 1-53 0 26.5 26.5 0 1 1 53 0" android:strokeWidth="4" android:fillColor="#FFFFFF"/>
<path android:pathData="M35.179 54c0-4.605 1.656-8.824 4.402-12.095v24.19c-2.746-3.271-4.402-7.49-4.402-12.095zM54 35.181c0.539 0 1.074 0.023 1.601 0.066v37.506c-0.527 0.044-1.062 0.066-1.601 0.066-0.54 0-1.074-0.022-1.602-0.066V35.247c0.528-0.043 1.062-0.066 1.602-0.066zM72.819 54c0 4.605-1.654 8.824-4.401 12.095v-24.19c2.747 3.271 4.401 7.49 4.401 12.095zM45.99 36.965c1.02-0.481 2.093-0.873 3.204-1.166v36.402c-1.111-0.293-2.184-0.685-3.204-1.166v-34.07zm12.816-1.166c2.347 0.619 4.514 1.679 6.408 3.086v30.23c-1.894 1.408-4.061 2.467-6.408 3.086V35.799z" android:strokeWidth="4" android:fillColor="#000000"/>
</vector>
@@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="90dp" android:height="90dp" android:viewportWidth="90" android:viewportHeight="90">
<path android:fillColor="#FF000000" android:pathData="M27 45a18 18 0 1 1 36 0 18 18 0 1 1-36 0" android:strokeWidth="4"/>
<path android:pathData="M31.738 45a13.262 13.262 0 1 1 26.524 0 13.262 13.262 0 1 1-26.524 0" android:strokeWidth="4" android:fillColor="#ffffff"/>
<path android:pathData="M54.41 45c0 2.303-0.827 4.412-2.201 6.048V38.952c1.374 1.636 2.201 3.745 2.201 6.048zM45 54.41c-0.27 0-0.537-0.012-0.801-0.034V35.624C44.463 35.602 44.73 35.59 45 35.59s0.537 0.012 0.801 0.034v18.752C45.537 54.398 45.27 54.41 45 54.41zM35.59 45c0-2.303 0.827-4.412 2.201-6.048v12.096C36.417 49.412 35.59 47.303 35.59 45zm13.415 8.517c-0.511 0.241-1.046 0.437-1.602 0.583V35.9c0.556 0.146 1.091 0.342 1.602 0.583v17.034zM42.597 54.1c-1.173-0.309-2.257-0.839-3.204-1.542V37.442c0.947-0.703 2.031-1.233 3.204-1.542v18.2z" android:strokeWidth="4" android:fillColor="#000000"/>
</vector>
+6 -4
View File
@@ -17,10 +17,12 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom|start"
android:layout_marginBottom="8dp"
android:layout_marginRight="80dp"
android:layout_marginEnd="80dp"
android:padding="16dp"/>
android:layout_marginBottom="16dp"
android:layout_marginRight="88dp"
android:layout_marginEnd="88dp"
android:layout_marginLeft="16dp"
android:layout_marginStart="16dp"
android:padding="24dp"/>
<android.support.design.widget.FloatingActionButton
style="@style/FAB"
android:id="@+id/flash"
+5 -1
View File
@@ -4,6 +4,10 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:contentDescription="@string/view_barcode"/>
<de.markusfisch.android.binaryeye.widget.DetectorView
android:id="@+id/detector_view"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
<android.support.design.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
@@ -13,6 +17,6 @@
style="@style/FAB"
android:id="@+id/scan"
android:contentDescription="@string/scan_code"
android:src="@drawable/ic_action_scan"/>
android:src="@drawable/ic_action_search"/>
</android.support.design.widget.CoordinatorLayout>
</merge>
+21
View File
@@ -0,0 +1,21 @@
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<EditText
tools:targetApi="o"
android:id="@+id/url"
android:inputType="textUri"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/url_hint"
android:importantForAutofill="no"/>
<Button
android:id="@+id/test_url"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/test_url"/>
</LinearLayout>
+22 -1
View File
@@ -38,7 +38,7 @@
android:layout_marginBottom="16dp"
android:gravity="end|top"/>
<TableLayout
android:id="@+id/meta"
android:id="@+id/data"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/format"
@@ -46,6 +46,15 @@
android:layout_marginStart="16dp"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"/>
<TableLayout
android:id="@+id/meta"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/data"
android:layout_marginEnd="16dp"
android:layout_marginStart="16dp"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"/>
<TextView
android:id="@+id/hex"
android:layout_width="match_parent"
@@ -59,6 +68,18 @@
android:typeface="monospace"
android:textSize="12sp"
tools:text="54 65 73 74 20 51 52 20 Test QR\n43 6F 64 65 Code"/>
<TextView
android:id="@+id/stamp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/hex"
android:layout_marginStart="16dp"
android:layout_marginLeft="16dp"
android:layout_marginEnd="16dp"
android:layout_marginRight="16dp"
android:layout_marginBottom="16dp"
android:textSize="12sp"
android:typeface="monospace"/>
</RelativeLayout>
</de.markusfisch.android.binaryeye.widget.ConfinedScrollView>
<android.support.design.widget.CoordinatorLayout
@@ -23,6 +23,18 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"/>
<TextView
android:id="@+id/error_correction_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:text="@string/error_correction_level"/>
<Spinner
android:id="@+id/error_correction_level"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:entries="@array/error_correction_levels"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"

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