Compare commits

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

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

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

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

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

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

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

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

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

This makes it easier to use a generated barcode in place of a small
printed barcode. There are scanners out there that expect a barcode
to be of a certain (small) size.
2022-11-08 20:28:41 +01:00
Markus Fisch c7c1e74240 Update to latest ZXing C++ wrapper
To fix handling of tab characters.
2022-11-08 20:27:23 +01:00
Markus Fisch c793e1399b Fix view/frame transformation matrix
The view offset needs to be translated first - not last, where the
matrix is no longer in view space.
2022-11-08 20:27:23 +01:00
zmniandGitHub c550a6ff51 Update indonesian translation 2022-11-08 20:27:00 +01:00
solokotandGitHub 05b0f719e0 Updated Russian translation 2022-11-07 18:22:34 +01:00
Markus Fisch 4326df5069 Advance version number to 1.56.0 2022-11-03 19:05:06 +01:00
Markus Fisch ba24287f04 Update to latest ZXing C++ wrapper
To fix getting UTF-8 strings from the JVM.
2022-11-03 19:00:49 +01:00
Markus Fisch bd8f1b17dd Remove double action item for forward/next 2022-11-02 21:57:32 +01:00
Markus Fisch 2c21aeb83a Set FAB background to @null
To silence this warning:

> I/FloatingActionButton: Setting a custom background is not supported.

Details:
https://github.com/material-components/material-components-android/issues/283
2022-11-03 12:30:05 +01:00
Markus Fisch 9ccff808bc Replace properties with functions in Barcode
We should only prefer read-only properties over of functions if the
underlying algorithm…

* does not throw
* is cheap to calculate (or cached on the first run)
* returns the same result over invocations if the object state
  hasn't changed

See:
https://kotlinlang.org/docs/coding-conventions.html#functions-vs-properties

But `ZXing.encodeAs*` may throw and may also not be that cheap to
calculate. So functions are a better fit here.

Also, because these functions will always return the same result,
it makes sense to cache their outputs.
2022-11-02 21:49:08 +01:00
Markus Fisch 32e936dd71 Optimize new drawables with avocado 2022-11-02 21:06:17 +01:00
Markus Fisch 58eebacd4f Use forward arrow for FAB in picking activity
Hopefully this makes it more clear that you have to tap on the
FAB to actually read the found barcode.
2022-11-02 21:06:17 +01:00
Markus Fisch 75b945e842 Remove unused forceCompat preference
Obsolete since we use the ZXing C++ fork.
2022-11-02 21:06:17 +01:00
Markus Fisch a9ea77f74f Add an action item to lock/unlock free rotation
When scanning a barcode from an image.

Free rotation may not always be desirable.
2022-11-02 21:06:17 +01:00
Markus Fisch 3df658165b Support setting fore and background colors
When creating AZTEC, DATA MATRIX or QR Codes.
2022-11-02 21:06:17 +01:00
Markus Fisch 40a6a77b8d Advance version number to 1.55.0 2022-10-30 14:21:01 +01:00
Markus Fisch 301a33a176 Update to latest ZXing C++ wrapper 2022-10-30 14:17:18 +01:00
GiovanniandGitHub 8d6623786c Update an Italian string 2022-10-27 22:59:07 +02:00
Markus Fisch a225eab6f2 Make recreated barcode interactive
This way the barcode can easily be exported, too.

Also live update the data views and clean up the DecodeFragment.
2022-10-26 21:45:06 +02:00
Markus Fisch 9f2efeb001 Advance version number to 1.54.1 2022-10-25 22:51:11 +02:00
Markus Fisch 105c81d409 Treat GS1 and ISO15434 content types as text
And use the HRI formatted text ZxingCpp provides.
2022-10-25 22:05:29 +02:00
Markus Fisch 4e1eb28ca4 Enable auto rotate by default
With ZXingCpp this is hardly noticeable. So enable it for new users.
2022-10-24 22:07:00 +02:00
Markus Fisch b72ed746bf Go back to VIBRATOR_SERVICE for Android 4
Use the deprecated `VIBRATOR_SERVICE` on all versions to fix a
`VerifyError` on Android 4.

For reasons beyond my interests, compiling with `VIBRATOR_MANAGER_SERVICE`
leads to a very cryptic `java.lang.VerifyError: r0/b` on Android 4.
Even though this symbol is never actually used under SDK 31.

This is probably a Proguard/R8 issue, but I don't want to waste more
time just to get rid of the deprecation warning. So let's use the old
`VIBRATOR_SERVICE` and wait for Google to fix their stuff.
2022-10-24 21:57:00 +02:00
solokotandGitHub 1aaee3c85a Update Russian translation 2022-10-24 20:49:37 +02:00
Oğuz ErsenandGitHub eb0218e733 Update Turkish translation 2022-10-23 20:42:01 +02:00
GiovanniandGitHub bef7365f28 Update Italian translation 2022-10-22 21:25:50 +02:00
Markus Fisch c72b6a2e5f Advance version number to 1.54.0 2022-10-21 23:39:30 +02:00
Markus Fisch a7e358852c Complete search engines for fa locale 2022-10-21 23:33:34 +02:00
Markus Fisch 7cf8cbe6d8 Recreate barcode on result fragment
It's sometimes very helpful to easily recover read barcodes.
2022-10-21 23:31:30 +02:00
Markus Fisch e379e55570 Use new Html.fromHtml() on Nougat+ 2022-10-21 22:41:53 +02:00
Markus Fisch a55795a7b6 Use new getParcelable() method on Tiramisu+ 2022-10-21 22:40:34 +02:00
Markus Fisch 83c0cd0293 Only use EXTRA_IS_SENSITIVE on Tiramisu+
And clean up that Pyramid of Death.
2022-10-21 21:23:30 +02:00
Markus Fisch d138148cb8 Refactor string extension functions
And put them where they belong instead of having a dump file.

According to the Kotlin Coding Conventions we should avoid creating
files just to hold extensions.
2022-10-21 21:14:52 +02:00
Markus Fisch c3c08879e0 Move ellipsize() from Toast to Data
Because it's used in more than one place now.
2022-10-21 17:25:17 +02:00
Markus Fisch d0f02faca7 Add german domains for search engines
To open the .de domains for Google, ebay, Amazon, etc.
2022-10-21 19:33:01 +02:00
Markus Fisch c9db3514d1 Add ebay, Amazon and barcodelookup.com
To the list of search engines.
2022-10-21 19:31:35 +02:00
Markus Fisch f2a0e1a392 Add changelogs for F-Droid 2022-10-20 21:39:40 +02:00
Markus Fisch f4de041407 Advance version number to 1.53.1 2022-10-20 09:38:36 +02:00
Markus Fisch 73ba2914e8 Fix proguard configuration for ZXing C++ 2022-10-20 09:37:45 +02:00
Markus Fisch 2907c8c49c Advance version number to 1.53.0 2022-10-19 22:27:21 +02:00
Markus Fisch f28f956a8d Only generate barcode output when required 2022-10-19 21:45:15 +02:00
Markus Fisch 972eb6a0b0 Update to latest ZXing C++ wrapper 2022-10-19 21:43:48 +02:00
Markus Fisch 08bb3f2127 Replace RelativeLayout with LinearLayout
In decoding fragment.

All children were layed out linearly anyway and it's simpler
and more clear this way.
2022-10-19 19:54:33 +02:00
Markus Fisch b28e939f58 Refactor resolveFormat() and make it an extension 2022-10-19 13:04:48 +02:00
solokotandGitHub 2edb8112b7 Update Russian translation 2022-10-19 13:04:06 +02:00
Markus Fisch cf5a6dc8b8 Hide setting a custom locale from API 33 on
Android 13 allows to set per-app language preferences, so this
setting is no longer required from there on.

See:
https://developer.android.com/guide/topics/resources/app-languages
2022-10-15 18:43:40 +02:00
Markus Fisch 3ec6f18fe0 Hide sensitive content from clipboard
At the moment, this affects just the WiFi password.

See:
https://developer.android.com/about/versions/13/behavior-changes-all#copy-sensitive-content
2022-10-15 18:38:14 +02:00
Markus Fisch 3a8f6cb827 Target SDK 33, Android 13 2022-10-15 18:37:49 +02:00
Markus Fisch 65c7fe52ee Fix closing Cursor's below JELLY BEAN
Unfortunately, Cursor cannot be cast to Closeable below API level 16
JELLY BEAN. So we can't just use Kotlin's ".use" extension function
because it would implicitly cast Cursor to a Closeable.

So this adds a simple drop-in replacement that does exactly what ".use"
is doing but without casting Cursor.

Of course, this should be removed as soon as minSDK is increased to at
least JELLY_BEAN.
2022-10-13 23:02:32 +02:00
Markus Fisch 5d3b3eb195 Fix generating a date time string below API 18
Before API level 18, there was no 'H' but only 'k', which was
incorrectly implemented to produce what we now know as 'H'.
2022-10-13 22:39:52 +02:00
Markus Fisch 0e4c2963a5 Catch ParseException when parsing scan dates 2022-10-13 22:38:04 +02:00
Markus Fisch 791c50cd4a Update build tools 2022-10-13 22:37:00 +02:00
Markus Fisch c04b08ab87 Update to latest ZXing C++ wrapper
Which adds a ContentType so we can distinguish between text
and binary content.
2022-10-12 21:47:54 +02:00
Markus Fisch f07c4ac0f4 Update to latest Kotlin and coroutines version 2022-10-11 23:43:30 +02:00
Markus Fisch 05d63bd73f Fix vector image fill color for API < 21
`fillColor` cannot have Android resources below API 21.
2022-10-11 23:37:29 +02:00
Markus Fisch 558fd5212d Remove double support annotation 2022-10-11 23:34:54 +02:00
Markus Fisch b918be76e4 Fix getting version number from database 2022-10-11 23:33:59 +02:00
Markus Fisch 2136a8c73e Show version code for all supported barcodes
And the module count if it's a QR Code.
2022-10-11 23:29:59 +02:00
Markus Fisch 959c6e7d4d Always show QR Code version and remove setting
This comes no longer with a cost so it can be always there.
2022-10-11 23:02:47 +02:00
Markus Fisch 50930d07a6 Show binary data placeholder in edit name dialog
To show something instead of nothing in the dialog title.
Still not perfect, of course.
2022-10-11 22:47:14 +02:00
Markus Fisch 36e7aab27c Update to latest ZXing C++ AAR
Fixes getting HRI formatted result text instead of the real thing.
2022-10-11 22:39:14 +02:00
Markus Fisch bb4fc8ed08 Enable multi-select in history listing 2022-10-11 21:53:15 +02:00
Markus Fisch 7a33ebd1a3 Remove MicroQR from list of writable codes
ZXing C++ doesn't contain a writer for this format, yet.
2022-10-11 20:41:18 +02:00
solokotandGitHub c8444b8935 Update Russian translation 2022-10-10 12:22:06 +02:00
Markus Fisch 4c6d66a4aa Migrate to ZXing-C++
Replace the ZXing Java implementation with the C++ fork.

The C++ fork is faster, actively maintained and supports the MicroQR
Code barcode format.

The original Java implementation is in "Maintenance Mode Only",
what means that no new features or barcodes will be accepted.

Because the C++ fork is much faster, we can now drop RenderScript
which has been deprecated by Google anyway. Also, this removes a lot
of complications. RenderScript was never very robust.
2022-10-09 18:46:50 +02:00
Markus Fisch 39f95a1c04 Remove shortcut extra from EncodeFragment
And make it possible to open the EncodeFragment without content.
2022-10-04 21:19:47 +02:00
Markus Fisch 9e30b108e7 Reformat code and optimize imports
Let Android Studio clean up.
2022-09-29 18:42:00 +02:00
Markus Fisch 1b6b1cefe3 Convert charset for PDF417 in an extension
And run it from PickActivity too to apply this conversion when
scanning from still images too.
2022-09-29 18:39:01 +02:00
Mladen MilinkovicandGitHub b634370a0b Fix PDF-417 UTF-8 text encoding
This fixes reading PDF-417 barcodes containing international UTF-8 characters.
2022-09-29 18:23:07 +02:00
gonandGitHub f32d44c3a1 Update spanish translation 2022-09-27 20:59:12 +02:00
Markus Fisch 2e060d1ca8 Add Persian to list of custom languages
That can be changed in the app independently from the system settings.
2022-09-11 23:01:31 +02:00
Andreas KorbandGitHub 6acdce32f2 Add multidex dependency
Required on some systems/Android Studio versions.
2022-09-10 21:33:04 +02:00
Markus Fisch 092ec694f6 Fix terminating string tag in persian translation 2022-09-10 19:12:03 +02:00
7fed7877a0 Add persian translation
Co-authored-by: eshagh79 <eshagh094@gmail.com>
2022-09-10 19:09:05 +02:00
Markus Fisch b17393d5ef Invert plain text output
Because plain text will most likely end up being black text on a
white background on a mobile device.

But with night mode we can never be sure. So there's an "inverted"
parameter now if the text is bright on a dark background.
2022-08-21 00:36:49 +02:00
Markus Fisch 319ae53b3b Make conditional an argument
To avoid the repetition of the function invocation.

Also make bt a constant variable because it can be.
2022-08-20 19:03:29 +02:00
Markus Fisch 6a47a7e477 Fix indent of new code for generating text output
We're not mixing tabs and spaces.
2022-08-20 18:56:50 +02:00
Tom XinandGitHub 19fb733182 Improve plain text output of generated barcodes
Optimize the 'save as text' function to make its results more recognizable when
printing on the console.
2022-08-20 18:53:43 +02:00
Markus Fisch ce6c6f26d2 Advance version number to 1.52.0 2022-08-10 23:25:46 +02:00
Markus Fisch 3633af5e0a Fix parsing timezone in calendar dates
And add patterns for RFC 822 and ISO 8601 formats.

Before this 'Z' was quoted and therefore just a string in the pattern.
So the time zone was actually never really applied to the resulting date.
Now, it's pattern letter and the resulting date is adjusted accordingly.

This also fixes using dates without a time zone.
2022-08-10 23:07:17 +02:00
87502f7226 Update translation (#280)
* Syntax correction (omitted point)

* Update french translation

Co-authored-by: Grena <grena@grenabox.fr>
2022-06-22 13:30:53 +02:00
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
Markus Fisch 565643f19e Advance version number to 1.32.0 2020-06-23 21:31:37 +02:00
Markus Fisch 4c92045563 Clean up permission handling 2020-06-23 21:12:38 +02:00
Markus Fisch 7116aa7278 Add an action to remove a scan from result view
So there's no need for a long tap to remove individual scans.
2020-06-23 21:10:21 +02:00
Markus Fisch 860e867ad4 Fix restoring crop handle position 2020-06-23 16:57:16 +02:00
Markus Fisch 3b509cd09c Make tests fail if there are no/wrong samples 2020-06-22 14:54:53 +02:00
Markus Fisch 51ec228fd8 Remove unused quantities from translations 2020-06-22 14:35:48 +02:00
Markus Fisch 9d1dec146d Add bulk mode
Scan continuously to add items to the history (if enabled)
instead of showing the result screen.
2020-06-22 14:30:51 +02:00
Markus Fisch 7bd9fe36fe Handle storing/restoring crop handle like zoom 2020-06-22 13:21:40 +02:00
Markus Fisch 85dba0604c Make ROI handle position permanent
So a user doesn't have to restore it every time.
2020-06-22 11:17:38 +02:00
Markus Fisch 56c70f5974 Make default ROI bigger and always a square 2020-06-22 10:27:31 +02:00
Markus Fisch 098b7740a0 Give ROI handle precedence over other controls
Because it can easily overlap with other controls and would
then be inaccessible.
2020-06-22 10:19:42 +02:00
Markus Fisch 7d92481fa6 Update ROI immediately when handle is tapped 2020-06-22 10:09:18 +02:00
zmniandGitHub 2e285a5b1d Update Indonesian translation (#123) 2020-06-21 16:36:45 +02:00
Markus Fisch ad25c4681f Calculate default ROI according to orientation
So it's probably a better fit.
2020-06-18 17:08:36 +02:00
Markus Fisch c00c87ceca Add missing changelog files for f-droid 2020-06-18 09:55:08 +02:00
Markus Fisch e001a92870 Run detection in pick activity in background too
Because the TRY_HARDER flag is always set when scanning still
images, ZXing may take quite some time on slow devices. So it's
better to run it in a background thread.
2020-06-15 16:53:03 +02:00
Markus Fisch 7a32b00e1f Add a menu item to export generated barcode as SVG
And use clear icons for PNG and SVG export.
2020-06-11 16:04:00 +02:00
Markus Fisch 50cedf782d Automatically convert JPEG test samples to YUV
And convert the existing test samples from YUV to JPEG.

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

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

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

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

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

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

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

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

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

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

On a fast device, this is hardly noticeable. But on a low-end device
it makes a slight difference which is why the setting is unset by
default.
2020-05-19 20:21:27 +02:00
Markus Fisch b2cc1c3c34 Move posting result into its on function
Easier to read.
2020-05-15 13:46:08 +02:00
Markus Fisch e2effb613f Add a rule to dump meminfo of debug version 2020-05-15 13:38:51 +02:00
Markus Fisch ae422d9a02 Advance version number to 1.28.0 2020-05-15 10:14:50 +02:00
zmniandGitHub ace841814a Update Indonesian translation 2020-05-15 09:55:16 +02:00
Markus Fisch 21c3206464 Reset preprocessor to fit current orientation
Important for landscape to landscape orientation changes.
2020-05-14 20:28:05 +02:00
Markus Fisch f392baca8b Remove special RenderScript handling for LineageOS
Always running `RenderScript.forceCompat()` interferes with the
`forceCompat` flag that is set/unset if there is a RSRuntimeException.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Also load the transparent primary color only once from Resources.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Makes it easier to use the same format multiple times.
2019-11-24 17:11:54 +01:00
Markus Fisch cb16220ebb Update Kotlin and SDK version to 29 2019-11-22 10:55:20 +01:00
Markus Fisch 7f3af23773 Allow scaling a new barcode to half its size
Apparently some barcodes aren't created with enough "quiet space"
inside the generated image so scanning them from a screen can be
a little tough.
2019-11-19 21:52:20 +01:00
433 changed files with 12633 additions and 5616 deletions
+404 -12
View File
@@ -1,23 +1,415 @@
# Change Log
# 1.18.0
## 1.58.2
* Fix parsing WiFi network suggestions
## 1.58.1
* Update zh-rCn translation
* Fix monochrome launcher icon
## 1.58.0
* Improve detection of inverted barcodes
* Optionally allow duplicates in bulk scan
* Add Danish translation
* Update Italian translation
* Update Japanese translation
* Update Russian translation
* Fix charset decoding for PDF 417
* Fix monochrome icon
## 1.57.0
* Add optional audio feedback for scanning
* Allow removing individual WiFi network suggestions
* Recognize incomplete and malformed WiFi barcodes
* Fix using shared text for encoding
* Add Korean language
* Update Chinese translation
* Update Português/Brasil translation
* Update Russian translation
## 1.56.3
* Fix parsing URLs in barcodes
* Update French, Portuguese, Turkish and Taiwan translation
## 1.56.2
* Fix generating 1D barcode text output
## 1.56.1
* Allow pinching a barcode to its generated size
* Fix view/frame transformation matrix
* Fix handling of tab characters
* Update Indonesian translation
* Update Russian translation
## 1.56.0
* Add an action item to lock/unlock free rotation
* Support setting fore and background colors when creating barcodes
* Fix creating barcodes with UTF-8 content
## 1.55.0
* Make recreated barcode interactive
* Update to latest ZXing C++
* Update Italian translation
## 1.54.1
* Update Russian translation
* Update Turkish translation
* Update Italian translation
* Fix handling GS1 and ISO15434 content types
* Fix VerifyError on Android 4
## 1.54.0
* Recreate and show barcode from read contents if possible
* Add ebay, Amazon and barcodelookup.com to search engines
## 1.53.1
* Fix proguard configuration
## 1.53.0
* Migrate to ZXing-C++ and add support for Micro QR Codes
* Multi-select scans in history listing
* Remove preset content for Encode shortcut
* Always show QR Code version and remove setting
* Fix PDF-417 UTF-8 text encoding
* Hide sensitive content from clipboard
* Remove setting a custom language on Android 13+
* Condense text output of generated barcodes
* Update Russian translation
* Update Spanish translation
* Add Persian translation
## 1.52.0
* Fix parsing timezone in calendar dates
* Update French translation
## 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
* Export generated barcode as SVG
* Run detection on images in background
* Remove a scan from result view
* Update Indonesian translation
## 1.31.0
* Stop detection while region of interest is modified
* Fix resetting region of interest
* Add Polish translation
## 1.30.1
* Improve usability of cropping limiter
* Draw round corners around region of interest
* Update Indonesian translation
## 1.30.0
* Add a handle to define a region of interest
* Add a setting to show/hide cropping limiter
* Add support for VCALENDAR types
* Add spanish translation
* Add copy to clipboard button to context menu in history listing
* Make history actions work on current listing only
* Keep camera selection over orientation changes
## 1.29.0
* Add a setting to enable reading of vertical 1D barcodes
## 1.28.0
* Share history as CSV and JSON too
* Export history in JSON format
* Update Indonesian translation
* Fix initializing of RenderScript automatically
* Fix highlighting after landscape to landscape rotations
## 1.27.1
* Update Russian translation
* Force compat mode if native RenderScript crashed
## 1.27.0
* Export SQLite database
* Improve order of preferences
* Update Italian translation
## 1.26.0
* Zoom in & out by swiping vertically in camera view
* Add a setting for swipe to zoom feature
* Optimize vector drawables
* Update Indonesian translation
## 1.25.1
* Fix Proguard configuration
## 1.25.0
* Add giving scans individual names in history listing
* Add searching the history
* Add a setting to search harder for barcodes
* Add a setting to control vibration on detection
* Add app shortcuts for launchers that support them
* Remove saving barcodes for intents from other apps
* Show latest possible result point in camera view
* Suggest a WiFi network on Android Q+
* Always put parsed WiFi password into clipboard
* Use native image processing where available
* Ignore consecutive duplicates by default
* Fix saving external files on Android Q+
* Fix order of metadata items
## 1.24.0
* Show barcode metadata
* Add a setting to show/hide metadata
* Include metadata in CSV export
* Update italian translation
* Fix resetting flash button
* Fix CSV export for Android Q
## 1.23.0
* Add a setting to not save consecutive double scans
* Add a setting to show/hide hex dump
* Limit history items to just one line
* Fix window insets for consecutive screens
## 1.22.3
* Fix broken RenderScript built
## 1.22.2
* Fix getting window insets for Android Lollipop and above
## 1.22.1
* Ensure non-printable content is saved as raw data
## 1.22.0
* Highlight detected barcodes in camera view
* Add bing and yandex search engines
* Update Chinese Translation
* Update Italian translation
* Update Indonesian translation
## 1.21.0
* Save and restore history list state
* Improve scanning from large images
* Fix opening image files by "Open with"
## 1.20.1
* Fix window insets below Android Lollipop
## 1.20.0
* Scroll content below tool and system bars
* Preset file name to save with format and content
* Add encode menu item to make encoding easier when keyboard is visible
* Increase font size of barcode contents in history
* Update Hungarian translation
* Update Indonesian translation
* Fix encoding for old Androids
* Fix returning result for ZXing's SCAN intent again
* Fix transfering barcode format from history
## 1.19.0
* Allow scaling a new barcode to half its size
* Remember last selected barcode format
## 1.18.0
* Make open URL default action after decoding a barcode
* Add a setting to open contents without inspection
* Highlight codes for picking in shared/loaded images
* Improve visability of crop border
* Fix returning result for ZXing's SCAN intent
# 1.17.1
## 1.17.1
* Fix navigating back without choosing an image
# 1.17.0
## 1.17.0
* Pinch/Zoom shared/loaded images to scan just a section
* Add a load file menu item to load an image file
* Add a save file menu item to the barcode view
* Use discrete style for size seek bar
* Fix bar transparency when zooming/pinching
# 1.16.0
## 1.16.0
* Improve displaying scanned barcodes
* Show a message if a barcode cannot be shared
* Fix setting system bar transparency when double taping a barcode
@@ -26,36 +418,36 @@
* Update Indonesian translation
* Update Italian translation
# 1.15.0
## 1.15.0
* Additionally export history as CSV file
* Color tool and system bars when a view is scrolled
* Add an intent filter for opening images
* Add Russian translation
# 1.14.0
## 1.14.0
* Add OTP support
* Update Indonesian translation
# 1.13.1
## 1.13.1
* Fix crash caused by missing support for Regex on Android < 6
# 1.13.0
## 1.13.0
* Add missing extras for ZXing result intents
# 1.12.0
## 1.12.0
* Add preferences dialog and the option to use a custom URL with read contents
* Add Brazilian Portuguese translation
* Add Chinese translation
* Encode text using UTF-8 encoding when creating a barcode
# 1.11.2
## 1.11.2
* Downgrade ZXing version
* Improve getting system UI metrics
# 1.11.1
## 1.11.1
* Fix tap to focus on some devices
# 1.11.0
## 1.11.0
* Add support for VCard and VEvent barcodes contents
* Generated barcodes are now zoomable
* Fix creating PDF417 barcodes
+2 -2
View File
@@ -13,8 +13,8 @@ to squash when merging into master:
$ git merge cool_feature --squash
Then write a [good commit messages][commit_messages] to keep the history
Then write a [good commit message][commit_messages] to keep the history
meaningful and useful. One feature, one commit.
[android_best_practices]: https://developer.android.com/distribute/best-practices/develop/
[commit_messages]: https://juffalow.com/other/write-good-git-commit-message
[commit_messages]: https://git-scm.com/book/en/v2/Distributed-Git-Contributing-to-a-Project#_commit_guidelines
+20 -1
View File
@@ -18,7 +18,20 @@ infer: clean
infer -- ./gradlew assembleDebug
test:
./gradlew test cAT
./gradlew test
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
@@ -30,8 +43,14 @@ start:
uninstall:
adb $(TARGET) uninstall $(PACKAGE).debug
meminfo:
adb shell dumpsys meminfo $(PACKAGE).debug
images:
svg/update.sh
avocado:
avocado $(shell fgrep -rl '<vector' app/src/main/res)
clean:
./gradlew clean
+59
View File
@@ -4,3 +4,62 @@ 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][getConfiguredNetworks]
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][getConfiguredNetworks]
which requires `ACCESS_WIFI_STATE`.
From Android R, `ACCESS_WIFI_STATE` is required for
[WifiManager.getNetworkSuggestions][getNetworkSuggestions]
which is used to remove individual network suggestions.
[ACCESS_FINE_LOCATION]: https://developer.android.com/reference/android/Manifest.permission#ACCESS_FINE_LOCATION
[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
[getConfiguredNetworks]: https://developer.android.com/reference/android/net/wifi/WifiManager#getConfiguredNetworks()
[addNetworkSuggestions]: https://developer.android.com/reference/android/net/wifi/WifiManager#addNetworkSuggestions(java.util.List%3Candroid.net.wifi.WifiNetworkSuggestion%3E)
[getNetworkSuggestions]: https://developer.android.com/reference/android/net/wifi/WifiManager#getNetworkSuggestions()
+144 -18
View File
@@ -2,32 +2,158 @@
Yet another barcode scanner for Android. As if there weren't [enough][play].
This one is totally free, open source and does use the [ZXing][zxing] barcode
This one is free, without any ads and open source.
Works in portrait and landscape orientation, can read inverted codes,
comes in Material Design and can also generate barcodes.
Binary Eye uses the [ZXing-C++][zxing_cpp] ("Zebra Crossing") barcode
scanning library.
Unlike many others, it works in portrait and landscape orientation, can read
inverted codes, comes in Material Design and doesn't do much else but decoding
a barcode.
And it's written in [Kotlin][kotlin] because I wanted to explore that a
bit more.
## Screenshots
<img
src="https://raw.githubusercontent.com/markusfisch/BinaryEye/gh-pages/screencap-scanning.png"
alt="Screenshot Scanning" width="160"/>
<img
src="https://raw.githubusercontent.com/markusfisch/BinaryEye/gh-pages/screencap-decoded.png"
alt="Screenshot Result" width="160"/>
<img
src="https://raw.githubusercontent.com/markusfisch/BinaryEye/gh-pages/screencap-compose-barcode.png"
alt="Screenshot Compose Barcode" width="160"/>
<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/screencap-scanning.png"
alt="Screenshot Gallery" width="160"/>
<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/screencap-scanning-cropped.png"
alt="Screenshot Gallery" width="160"/>
<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/screencap-decoded.png"
alt="Screenshot Theme" width="160"/>
<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/screencap-compose-barcode.png"
alt="Screenshot Editor" width="160"/>
## Download
<a href="https://f-droid.org/en/packages/de.markusfisch.android.binaryeye/"><img alt="Get it on F-Droid" src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png" height="80"/></a> <a href="https://play.google.com/store/apps/details?id=de.markusfisch.android.binaryeye"><img alt="Get it on Google Play" src="https://play.google.com/intl/en_us/badges/images/generic/en_badge_web_generic.png" height="80"/></a>
## Supported Barcode Formats
### Read
ZXing can read the following barcode formats:
* [AZTEC][aztec]
* [CODABAR][codabar]
* [CODE 39][code_39]
* [CODE 93][code_93]
* [CODE 128][code_128]
* [DATA MATRIX][data_matrix]
* [EAN 8][ean_8]
* [EAN 13][ean_13]
* [ITF][itf]
* [MAXICODE][maxicode] (partial)
* [PDF417][pdf417]
* [QR CODE][qr_code]
* [Micro QR Code][micro_qr_code]
* [RSS 14][rss]
* [RSS EXPANDED][rss]
* [UPC A][upc_a]
* [UPC E][upc_e]
* [UPC EAN EXTENSION][upc_ean]
### Generate
ZXing can generate the following barcode formats:
* [AZTEC][aztec]
* [CODABAR][codabar]
* [CODE 39][code_39]
* [CODE 128][code_128]
* [DATA MATRIX][data_matrix]
* [EAN 8][ean_8]
* [EAN 13][ean_13]
* [ITF][itf]
* [PDF 417][pdf417]
* [QR CODE][qr_code]
* [UPC A][upc_a]
## 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
### 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"))
}
}
```
[play]: https://play.google.com/store/search?q=barcode%20scanner&c=apps
[zxing]: https://github.com/zxing/zxing
[zxing_cpp]: https://github.com/zxing-cpp/zxing-cpp
[kotlin]: http://kotlinlang.org/
[aztec]: https://en.wikipedia.org/wiki/Aztec_Code
[codabar]: https://en.wikipedia.org/wiki/Codabar
[code_39]: https://en.wikipedia.org/wiki/Code_39
[code_93]: https://en.wikipedia.org/wiki/Code_93
[code_128]: https://en.wikipedia.org/wiki/Code_128
[data_matrix]: https://en.wikipedia.org/wiki/Data_Matrix
[ean_8]: https://en.wikipedia.org/wiki/EAN-8
[ean_13]: https://en.wikipedia.org/wiki/International_Article_Number
[itf]: https://en.wikipedia.org/wiki/Interleaved_2_of_5
[maxicode]: https://en.wikipedia.org/wiki/MaxiCode
[pdf417]: https://en.wikipedia.org/wiki/PDF417
[qr_code]: https://en.wikipedia.org/wiki/QR_code
[micro_qr_code]: https://en.wikipedia.org/wiki/QR_code#Micro_QR_code
[rss]: https://en.wikipedia.org/wiki/GS1_DataBar
[upc_a]: https://en.wikipedia.org/wiki/Universal_Product_Code
[upc_e]: https://en.wikipedia.org/wiki/Universal_Product_Code#UPC-E
[upc_ean]: https://en.wikipedia.org/wiki/Universal_Product_Code#EAN-13
[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
+22 -30
View File
@@ -2,26 +2,19 @@ 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 46
versionName '1.18.0'
// it's recommended to set this value to the lowest API level
// able to provide all the functionality
renderscriptTargetApi 24
renderscriptSupportModeEnabled true
testInstrumentationRunner 'android.support.test.runner.AndroidJUnitRunner'
versionCode 115
versionName '1.58.2'
}
signingConfigs {
config {
release {
keyAlias System.getenv('ANDROID_KEY_ALIAS')
keyPassword System.getenv('ANDROID_KEY_PASSWORD')
storePassword System.getenv('ANDROID_STORE_PASSWORD')
@@ -45,34 +38,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
// of RenderScript is raising this error no matter if
// renderscriptTargetApi matches targetSdkVersion
disable 'GradleCompatible'
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
}
}
}
dependencies {
androidTestImplementation ('com.android.support.test:runner:1.0.2') {
exclude module: 'support-annotations'
}
androidTestImplementation ('com.android.support.test:rules:1.0.2') {
exclude module: 'support-annotations'
}
testImplementation 'junit:junit:4.12'
testImplementation 'junit:junit:4.13.2'
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.6.1"
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.github.markusfisch:CameraView:1.8.4'
implementation 'com.github.markusfisch:ScalingImageView:1.1.4'
implementation "com.android.support:preference-v7:$support_version"
implementation "com.android.support:preference-v14:$support_version"
implementation 'com.github.markusfisch:CameraView:1.9.1'
implementation 'com.github.markusfisch:ScalingImageView:1.4.1'
implementation 'com.github.markusfisch:zxing-cpp:v2.0.0.0'
}
+12 -2
View File
@@ -3,10 +3,20 @@
<!-- Lint can't understand Kotlin sources and reports all resources
as unused -->
<issue id="UnusedResources">
<ignore path="src/main/res/"/>
<ignore path="src/main/res/" />
</issue>
<!-- Ignore RenderScript output -->
<issue id="UnusedResources">
<ignore path="*/res/rs/debug/raw/bc*"/>
<ignore path="*/res/rs/debug/raw/bc*" />
</issue>
<!-- 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 -1
View File
@@ -1,4 +1,7 @@
-dontwarn com.google.**
-dontwarn android.support.v7.**
-dontwarn android.support.v8.**
-keep class android.support.v8.renderscript.** { *; }
-keep class android.support.v7.widget.SearchView { *; }
-keep class de.markusfisch.android.zxingcpp.** { *; }
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,51 +0,0 @@
package de.markusfisch.android.binaryeye
import android.support.test.InstrumentationRegistry
import android.support.test.runner.AndroidJUnit4
import de.markusfisch.android.binaryeye.rs.Preprocessor
import de.markusfisch.android.binaryeye.zxing.Zxing
import org.junit.Test
import org.junit.runner.RunWith
import java.util.regex.Pattern
@RunWith(AndroidJUnit4::class)
class PreprocessorTest {
@Test
fun processSamples() {
val zxing = Zxing()
val assets = InstrumentationRegistry.getInstrumentation()
.context.assets
val pattern = Pattern.compile(
"[0-9]+-([0-9]+)x([0-9]+)-([0-9]+)deg.yuv"
)
val files = assets.list("yuv") ?: return
for (file in files) {
val m = pattern.matcher(file)
if (!m.find() || m.groupCount() < 3) {
continue
}
val frameWidth = m.group(1) ?: return
val frameHeight = m.group(2) ?: return
val frameOrientation = m.group(3) ?: return
val frameData = assets.open("yuv/$file").readBytes()
val preprocessor = Preprocessor(
InstrumentationRegistry.getTargetContext(),
frameWidth.toInt(),
frameHeight.toInt(),
frameOrientation.toInt()
)
preprocessor.process(frameData)
val result = zxing.decode(
frameData,
preprocessor.outWidth,
preprocessor.outHeight
)
preprocessor.destroy()
checkNotNull(result) { "no barcode found in $file" }
}
}
}
@@ -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

+46 -13
View File
@@ -1,38 +1,59 @@
<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"
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-feature android:name="android.hardware.camera"/>
<uses-feature android:name="android.hardware.camera.autofocus"/>
<uses-sdk tools:overrideLibrary="android.support.v14.preference"/>
<queries>
<intent>
<action android:name="android.intent.action.VIEW"/>
<data android:scheme="https"/>
</intent>
<intent>
<action android:name="android.intent.action.VIEW"/>
<data android:scheme="http"/>
</intent>
</queries>
<application
tools:ignore="UnusedAttribute"
android:name=".app.BinaryEyeApp"
android:usesCleartextTraffic="true"
android:allowBackup="true"
android:fullBackupContent="true"
android:fullBackupContent="@xml/backup_rules"
android:dataExtractionRules="@xml/extraction_rules"
android:supportsRtl="true"
android:resizeableActivity="true"
android:label="@string/app_name"
android:icon="@mipmap/ic_launcher"
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"/>
</intent-filter>
<meta-data
android:name="android.app.shortcuts"
android:resource="@xml/shortcuts"/>
</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"/>
@@ -46,15 +67,14 @@
<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:scheme="binaryeye" android:host="scan"/>
</intent-filter>
</activity>
<activity
android:name=".activity.PickActivity"
android:label="@string/pick_code_to_scan">
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"/>
<category android:name="android.intent.category.DEFAULT"/>
@@ -63,12 +83,22 @@
<intent-filter android:label="@string/decode_barcode">
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<data android:mimeType="image/*" android:scheme="file"/>
<data android:mimeType="image/*"/>
</intent-filter>
</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"
@@ -78,5 +108,8 @@
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths"/>
</provider>
<meta-data
android:name="android.app.searchable"
android:resource="@xml/searchable"/>
</application>
</manifest>
@@ -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
@@ -13,15 +14,18 @@ import de.markusfisch.android.binaryeye.actions.wifi.WifiAction
object ActionRegistry {
val DEFAULT_ACTION: IAction = OpenOrSearchAction
val REGISTRY: Set<IAction> = setOf(
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,9 +2,9 @@ package de.markusfisch.android.binaryeye.actions
import android.content.Context
import android.content.Intent
import android.widget.Toast
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 {
val iconResId: Int
@@ -18,34 +18,38 @@ abstract class IntentAction : IAction {
abstract val errorMsg: Int
final override suspend fun execute(context: Context, data: ByteArray) {
val intent =
createIntent(context, data) ?: return Toast.makeText(
context,
errorMsg,
Toast.LENGTH_LONG
).show()
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 android.widget.Toast
import de.markusfisch.android.binaryeye.R
import de.markusfisch.android.binaryeye.actions.IAction
import de.markusfisch.android.binaryeye.app.alertDialog
import de.markusfisch.android.binaryeye.app.execShareIntent
import de.markusfisch.android.binaryeye.app.parseAndNormalizeUri
import de.markusfisch.android.binaryeye.app.prefs
import java.net.URLEncoder
import de.markusfisch.android.binaryeye.content.openUrl
import de.markusfisch.android.binaryeye.net.urlEncode
object OpenOrSearchAction : IAction {
override val iconResId: Int = R.drawable.ic_action_search
@@ -18,32 +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 -> Toast.makeText(
context,
R.string.cannot_resolve_action,
Toast.LENGTH_SHORT
).show()
private suspend fun view(
context: Context,
url: String,
search: Boolean
) {
if (!context.openUrl(url, silent = true) && 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)
@@ -51,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)
}
}
}
}
@@ -4,10 +4,12 @@ import android.content.Context
import android.content.Intent
import de.markusfisch.android.binaryeye.R
import de.markusfisch.android.binaryeye.actions.IntentAction
import de.markusfisch.android.binaryeye.app.parseAndNormalizeUri
import de.markusfisch.android.binaryeye.content.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()
)
}
@@ -6,19 +6,27 @@ object VTypeParser {
private const val BACKSLASH_R_LEGACY =
"(?:\u000D\u000A|[\u000A\u000B\u000C\u000D\u0085\u2028\u2029])"
private val vTypeRegex =
"""^BEGIN:(VCARD|VEVENT)(?:$BACKSLASH_R_LEGACY.+?:[\s\S]+?)+?${BACKSLASH_R_LEGACY}END:\1$BACKSLASH_R_LEGACY?$""".toRegex(
"""^BEGIN:(V.+)(?:$BACKSLASH_R_LEGACY.+?:[\s\S]+?)+?${BACKSLASH_R_LEGACY}END:\1$BACKSLASH_R_LEGACY?$""".toRegex(
RegexOption.IGNORE_CASE
)
private val propertyRegex = """^(.+?):([\s\S]*?)$""".toRegex(RegexOption.MULTILINE)
private val propertyRegex = """^(.+?):([\s\S]*?)$""".toRegex(
RegexOption.MULTILINE
)
fun parseVType(data: String): String? =
vTypeRegex.matchEntire(data)?.groupValues?.get(1)?.toUpperCase(Locale.US)
fun parseVType(data: String): String? = vTypeRegex.matchEntire(
data
)?.groupValues?.get(1)?.uppercase(Locale.US)
fun parseMap(data: String): Map<String, List<VTypeProperty>> = propertyRegex.findAll(data).map {
fun parseMap(
data: String
): Map<String, List<VTypeProperty>> = propertyRegex.findAll(data).map {
it.groupValues[1].split(';').let { typeAndInfo ->
typeAndInfo[0] to VTypeProperty(typeAndInfo.drop(1), it.groupValues[2])
typeAndInfo[0] to VTypeProperty(
typeAndInfo.drop(1),
it.groupValues[2]
)
}
}.groupBy({ it.first.toUpperCase(Locale.US) }) { it.second }
}.groupBy({ it.first.uppercase(Locale.US) }) { it.second }
}
data class VTypeProperty(
@@ -9,7 +9,6 @@ import de.markusfisch.android.binaryeye.R
import de.markusfisch.android.binaryeye.actions.IntentAction
import de.markusfisch.android.binaryeye.actions.vtype.VTypeParser
import java.util.*
import kotlin.collections.ArrayList
object VCardAction : IntentAction() {
override val iconResId: Int
@@ -20,10 +19,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 +58,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 ->
@@ -120,27 +128,29 @@ object VCardAction : IntentAction() {
}
private val String.locationFormat: String
get() = """^([^;]*?);([^;]*?);([^;]*?);([^;]*?);([^;]*?);([^;]*?);([^;]*?)$""".toRegex().matchEntire(
this
)?.groupValues?.let {
listOf(
it[1],
it[2],
it[3],
"${it[4]} ${it[6]}",
"${it[5]} ${it[7]}"
).filter { line -> line.isNotBlank() }
.joinToString("\n").takeIf { location -> location.isNotBlank() }
} ?: this
get() = """^([^;]*?);([^;]*?);([^;]*?);([^;]*?);([^;]*?);([^;]*?);([^;]*?)$""".toRegex()
.matchEntire(
this
)?.groupValues?.let {
listOf(
it[1],
it[2],
it[3],
"${it[4]} ${it[6]}",
"${it[5]} ${it[7]}"
).filter { line -> line.isNotBlank() }
.joinToString("\n").takeIf { location -> location.isNotBlank() }
} ?: this
private val String.nameFormat: String
get() = """^([^;]*?);([^;]*?);([^;]*?);([^;]*?);([^;]*?)$""".toRegex().matchEntire(this)?.groupValues?.let {
listOf(it[4], it[2], it[3], it[1], it[5]).joinToString(" ")
.takeIf { name -> name.isNotBlank() }
} ?: this
get() = """^([^;]*?);([^;]*?);([^;]*?);([^;]*?);([^;]*?)$""".toRegex()
.matchEntire(this)?.groupValues?.let {
listOf(it[4], it[2], it[3], it[1], it[5]).joinToString(" ")
.takeIf { name -> name.isNotBlank() }
} ?: this
private val String.mailType: Int?
get() = when (this.toUpperCase(Locale.US)) {
get() = when (this.uppercase(Locale.US)) {
"HOME" -> ContactsContract.CommonDataKinds.Email.TYPE_HOME
"WORK" -> ContactsContract.CommonDataKinds.Email.TYPE_WORK
"OTHER" -> ContactsContract.CommonDataKinds.Email.TYPE_OTHER
@@ -149,7 +159,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
@@ -1,6 +1,5 @@
package de.markusfisch.android.binaryeye.actions.vtype.vevent
import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
import android.os.Build
@@ -21,8 +20,11 @@ object VEventAction : IntentAction() {
get() = R.string.vevent_failed
override fun canExecuteOn(data: ByteArray): Boolean {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH
&& VTypeParser.parseVType(String(data)) == "VEVENT"
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
return false
}
val type = VTypeParser.parseVType(String(data))
return type == "VEVENT" || type == "VCALENDAR"
}
@RequiresApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
@@ -42,37 +44,49 @@ 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.time
)
}
}
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.time
)
}
}
}
}
@SuppressLint("SimpleDateFormat") // we definitely don't wan't the local format
private val dateFormats = listOf(
SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"),
SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss"),
SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'"),
SimpleDateFormat("yyyyMMdd'T'HHmmss"),
SimpleDateFormat("yyyy-MM-dd"),
SimpleDateFormat("yyyyMMdd")
)
private fun List<SimpleDateFormat>.simpleFindParse(data: String): Date? {
for (simpleDataFormat in this) {
return simpleDataFormat.simpleParse(data) ?: continue
}
return null
}
private fun SimpleDateFormat.simpleParse(data: String): Date? = try {
this.parse(data)
} catch (e: Exception) {
null
}
}
private val dateFormats = listOf(
"yyyy-MM-dd'T'HH:mm:ssXXX",
"yyyy-MM-dd'T'HH:mm:ssZ",
"yyyy-MM-dd'T'HH:mm:ssz",
"yyyy-MM-dd'T'HH:mm:ss",
"yyyyMMdd'T'HHmmssXXX",
"yyyyMMdd'T'HHmmssZ",
"yyyyMMdd'T'HHmmssz",
"yyyyMMdd'T'HHmmss",
"yyyy-MM-dd",
"yyyyMMdd"
)
private fun List<String>.simpleFindParse(date: String): Date? {
for (pattern in this) {
return SimpleDateFormat(
pattern,
Locale.getDefault()
).simpleParse(date) ?: continue
}
return null
}
private fun SimpleDateFormat.simpleParse(date: String): Date? = try {
parse(date)
} catch (e: Exception) {
null
}
@@ -1,11 +1,26 @@
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-Za-z0-9]{3,}\\.[A-Za-z]{2,}[^ \t\r\n]*$".toRegex()
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)
}
}
@@ -1,90 +1,33 @@
package de.markusfisch.android.binaryeye.actions.wifi
import android.content.Context
import android.content.Context.WIFI_SERVICE
import android.net.wifi.WifiConfiguration
import android.net.wifi.WifiManager
import android.widget.Toast
import de.markusfisch.android.binaryeye.R
import de.markusfisch.android.binaryeye.actions.IAction
import de.markusfisch.android.binaryeye.widget.toast
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeoutOrNull
object WifiAction : IAction {
override val iconResId = R.drawable.ic_action_wifi
override val titleResId = R.string.connect_to_wifi
override fun canExecuteOn(data: ByteArray): Boolean =
WifiConfigurationFactory.parse(String(data)) != null
var password: String? = null
override suspend fun execute(context: Context, data: ByteArray) = withContext(Dispatchers.IO) {
val wifiConfig = WifiConfigurationFactory.parse(
override fun canExecuteOn(data: ByteArray): Boolean =
WifiConnector.parse(String(data)) {
password = it
} != null
override suspend fun execute(
context: Context,
data: ByteArray
) = withContext(Dispatchers.IO) {
val wifiConfig = WifiConnector.parse(
String(data)
) ?: return@withContext
val wifiManager = context.applicationContext.getSystemService(
WIFI_SERVICE
) as WifiManager
wifiManager.enableWifi(context)
wifiManager.mayRemoveOldNetwork(wifiConfig)
wifiManager.enableNewNetwork(wifiConfig)
val message = WifiConnector.addNetwork(context, wifiConfig)
withContext(Dispatchers.Main) {
Toast.makeText(
context,
R.string.wifi_added,
Toast.LENGTH_LONG
).show()
context.toast(message)
}
}
private suspend fun WifiManager.enableWifi(context: Context): Boolean {
if (!isWifiEnabled) {
if (!setWifiEnabled(true)) {
withContext(Dispatchers.Main) {
Toast.makeText(
context,
R.string.wifi_config_failed,
Toast.LENGTH_LONG
).show()
}
return false
}
// wait for wifi to really be enabled, takes some time
withTimeoutOrNull(10000) {
while (!isWifiEnabled) delay(500)
} ?: if (!isWifiEnabled) {
withContext(Dispatchers.Main) {
Toast.makeText(
context,
R.string.wifi_config_failed,
Toast.LENGTH_LONG
).show()
}
return false
}
}
return true
}
private suspend fun WifiManager.mayRemoveOldNetwork(
wifiConfig: WifiConfiguration
) {
configuredNetworks?.firstOrNull {
it.SSID == wifiConfig.SSID &&
it.allowedKeyManagement == wifiConfig.allowedKeyManagement
}?.networkId?.also {
removeNetwork(it)
}
}
private suspend fun WifiManager.enableNewNetwork(
wifiConfig: WifiConfiguration
) {
val id = addNetwork(wifiConfig)
disconnect()
enableNetwork(id, true)
reconnect()
}
}
@@ -1,8 +1,13 @@
package de.markusfisch.android.binaryeye.actions.wifi
import android.content.Context
import android.net.wifi.WifiConfiguration
import android.net.wifi.WifiEnterpriseConfig
import android.net.wifi.WifiManager
import android.net.wifi.WifiNetworkSuggestion
import android.os.Build
import android.support.annotation.RequiresApi
import de.markusfisch.android.binaryeye.R
import java.util.*
/**
@@ -29,26 +34,59 @@ import java.util.*
*
* The fields can appear in any order. Only "S:" is required.
*/
object WifiConfigurationFactory {
fun parse(input: String): WifiConfiguration? {
object WifiConnector {
fun parse(
input: String,
passwordBlock: ((password: String?) -> Unit)? = null
): Any? {
val inputMap = parseMap(input) ?: return null
val parsedData = SimpleDataAccessor.of(inputMap) ?: return null
return WifiConfiguration().apply(parsedData)
passwordBlock?.apply {
invoke(parsedData.password)
}
return if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q) {
// WifiConfiguration is deprecated in Android Q but
// because it's only possible in R to query network
// suggestions, we still use the deprecated API on Q.
@Suppress("DEPRECATION")
WifiConfiguration().apply(parsedData)
} else {
WifiNetworkSuggestion.Builder().apply(parsedData)
}
}
internal fun parseMap(string: String): Map<String, String>? {
// normally those codes should have the last semicolon, but many
// generators don't add it
val wifiRegex = """^WIFI:((?:.+?:(?:[^\\;]|\\.)*;)+);?$""".toRegex()
// should be: ^(.+):((?:[^\\;,":]|\\.)*);$ but allows unescaped , "
// and : because many qr creator doesn't escape
val pairRegex = """(.+?):((?:[^\\;]|\\.)*);""".toRegex()
fun addNetwork(context: Context, config: Any): Int {
val wifiManager = context.applicationContext.getSystemService(
Context.WIFI_SERVICE
) as WifiManager
// WifiConfiguration is deprecated in Android Q.
@Suppress("DEPRECATION")
return if ((config is WifiConfiguration &&
wifiManager.enableWifi() &&
wifiManager.removeOldNetwork(config) &&
wifiManager.enableNewNetwork(config)) ||
(Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q &&
config is WifiNetworkSuggestion.Builder &&
wifiManager.addNetworkFromBuilder(config))
) {
R.string.wifi_added
} else {
R.string.wifi_config_failed
}
}
internal fun parseMap(input: String): Map<String, String>? {
// Normally those codes should have the last semicolon, but many
// generators don't add it.
val wifiRegex = """^WIFI:((?:.+?:(?:[^\\;]|\\.)*;)+);?$""".toRegex()
// Should be: ^(.+):((?:[^\\;,":]|\\.)*);$ but allows unescaped , "
// and : because many QR Code creators don't escape properly.
val pairRegex = """(.+?):((?:[^\\;]|\\.)*);""".toRegex()
return wifiRegex.matchEntire(
string
input.trimAndTerminate()
)?.groupValues?.get(1)?.let { pairs ->
pairRegex.findAll(pairs).map { pair ->
pair.groupValues[1].toUpperCase(Locale.US) to pair.groupValues[2]
pair.groupValues[1].uppercase(Locale.US) to pair.groupValues[2]
}.toMap()
}
}
@@ -56,32 +94,25 @@ object WifiConfigurationFactory {
internal class SimpleDataAccessor private constructor(
private val inputMap: Map<String, String>
) {
private val hexRegex = """^[0-9a-f]+$""".toRegex(
RegexOption.IGNORE_CASE
)
// keep possibility of wrongly not escaped \ by explicitly searching
// for special chars
private val escapedRegex = """\\([\\;,":])""".toRegex()
// 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").quotedUnlessHex.unescaped
get() = inputMap.getValue("S").unescape
internal val securityType: String
get() = inputMap["T"]?.unescaped ?: ""
get() = inputMap["T"]?.unescape ?: ""
internal val password: String?
get() = inputMap["P"]?.quotedUnlessHex?.unescaped
get() = inputMap["P"]?.unescape
internal val hidden: Boolean
get() = inputMap["H"]?.unescaped == "true"
get() = inputMap["H"]?.unescape == "true"
internal val anonymousIdentity: String
get() = inputMap["AI"]?.unescaped ?: ""
get() = inputMap["AI"]?.unescape ?: inputMap["A"]?.unescape ?: ""
internal val identity: String
get() = inputMap["I"]?.unescaped ?: ""
get() = inputMap["I"]?.unescape ?: ""
internal val eapMethod: Int?
get() = if (inputMap["E"].isNullOrEmpty()) {
requireSdk(Build.VERSION_CODES.JELLY_BEAN_MR2) {
WifiEnterpriseConfig.Eap.NONE
}
} else when (inputMap["E"]) {
} else when (inputMap["E"]?.uppercase()) {
"AKA" -> requireSdk(Build.VERSION_CODES.LOLLIPOP) { WifiEnterpriseConfig.Eap.AKA }
"AKA_PRIME" -> requireSdk(Build.VERSION_CODES.M) { WifiEnterpriseConfig.Eap.AKA_PRIME }
"NONE" -> requireSdk(Build.VERSION_CODES.JELLY_BEAN_MR2) { WifiEnterpriseConfig.Eap.NONE }
@@ -98,7 +129,7 @@ object WifiConfigurationFactory {
requireSdk(Build.VERSION_CODES.JELLY_BEAN_MR2) {
WifiEnterpriseConfig.Phase2.NONE
}
} else when (inputMap["PH2"]) {
} else when (inputMap["PH2"]?.uppercase()) {
"AKA" -> requireSdk(Build.VERSION_CODES.O) { WifiEnterpriseConfig.Phase2.AKA }
"AKA_PRIME" -> requireSdk(Build.VERSION_CODES.O) { WifiEnterpriseConfig.Phase2.AKA_PRIME }
"GTC" -> requireSdk(Build.VERSION_CODES.JELLY_BEAN_MR2) { WifiEnterpriseConfig.Phase2.GTC }
@@ -110,16 +141,6 @@ object WifiConfigurationFactory {
else -> null
}
private val String.unescaped: String
get() = this.replace(escapedRegex) { escaped ->
escaped.groupValues[1]
}
private val String.quotedUnlessHex: String
get() = if (matches(hexRegex) || (startsWith("\"") &&
endsWith("\""))
) this else "\"$this\""
internal companion object {
internal fun of(inputMap: Map<String, String>): SimpleDataAccessor? {
return SimpleDataAccessor(inputMap).takeUnless {
@@ -129,19 +150,75 @@ object WifiConfigurationFactory {
}
}
@RequiresApi(Build.VERSION_CODES.Q)
private fun WifiNetworkSuggestion.Builder.apply(
data: SimpleDataAccessor
): WifiNetworkSuggestion.Builder? {
fun WifiNetworkSuggestion.Builder.applyCommon(
data: SimpleDataAccessor
): WifiNetworkSuggestion.Builder {
setSsid(data.ssid)
return this
}
fun WifiNetworkSuggestion.Builder.applySecurity(
data: SimpleDataAccessor
): WifiNetworkSuggestion.Builder? {
try {
when (data.securityType.uppercase()) {
"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 }
})
}
}
} catch (e: IllegalArgumentException) {
// Don't accept passphrases that aren't ASCII encodeable.
return null
}
return this
}
return this.applyCommon(data).applySecurity(data)
}
// WifiConfiguration is deprecated in Android Q.
@Suppress("DEPRECATION")
private fun WifiConfiguration.apply(
data: SimpleDataAccessor
): WifiConfiguration? {
val ssidWithQuotes = data.ssid.quote
val passwordWithQuotes = data.password?.quoteUnlessHex
fun WifiConfiguration.applyCommon(
data: SimpleDataAccessor
): WifiConfiguration? {
): WifiConfiguration {
allowedAuthAlgorithms.clear()
allowedGroupCiphers.clear()
allowedKeyManagement.clear()
allowedPairwiseCiphers.clear()
allowedProtocols.clear()
SSID = data.ssid
SSID = ssidWithQuotes
hiddenSSID = data.hidden
return this
}
@@ -149,10 +226,10 @@ object WifiConfigurationFactory {
fun WifiConfiguration.applySecurity(
data: SimpleDataAccessor
): WifiConfiguration? {
when (data.securityType) {
"", "nopass" -> allowedKeyManagement.set(WifiConfiguration.KeyMgmt.NONE)
when (data.securityType.uppercase()) {
"", "NOPASS" -> allowedKeyManagement.set(WifiConfiguration.KeyMgmt.NONE)
"WEP" -> @Suppress("DEPRECATION") /* WEP as insecure */ {
data.password?.also {
passwordWithQuotes?.let {
wepKeys[0] = it
} ?: return null
wepTxKeyIndex = 0
@@ -164,7 +241,7 @@ object WifiConfigurationFactory {
allowedGroupCiphers.set(WifiConfiguration.GroupCipher.WEP104)
}
"WPA" -> {
data.password?.also {
passwordWithQuotes?.let {
preSharedKey = it
} ?: return null
allowedAuthAlgorithms.set(WifiConfiguration.AuthAlgorithm.OPEN)
@@ -180,13 +257,13 @@ object WifiConfigurationFactory {
allowedGroupCiphers.set(WifiConfiguration.GroupCipher.CCMP)
}
"WPA2-EAP" -> requireSdk(Build.VERSION_CODES.JELLY_BEAN_MR2) {
data.password?.also {
passwordWithQuotes?.let {
preSharedKey = it
} ?: return null
allowedAuthAlgorithms.set(WifiConfiguration.AuthAlgorithm.OPEN)
allowedProtocols.set(WifiConfiguration.Protocol.RSN) // WPA2
allowedKeyManagement.set(WifiConfiguration.KeyMgmt.WPA_EAP)
@Suppress("DEPRECATION") // TKIP is insecure and has bad performance
@Suppress("DEPRECATION") // TKIP is insecure and has bad performance.
allowedPairwiseCiphers.set(WifiConfiguration.PairwiseCipher.TKIP)
allowedPairwiseCiphers.set(WifiConfiguration.PairwiseCipher.CCMP)
allowedGroupCiphers.set(WifiConfiguration.GroupCipher.TKIP)
@@ -203,10 +280,101 @@ object WifiConfigurationFactory {
}
return this
}
return this.applyCommon(data)?.applySecurity(data)
return this.applyCommon(data).applySecurity(data)
}
private inline fun <T> requireSdk(version: Int, block: () -> T): T? {
return if (Build.VERSION.SDK_INT >= version) return block() else null
}
}
@RequiresApi(Build.VERSION_CODES.Q)
private fun WifiManager.addNetworkFromBuilder(
builder: WifiNetworkSuggestion.Builder
): Boolean {
val suggestion = builder.build()
val suggestions = listOf(suggestion)
// Remove previous conflicting network suggestion.
removeNetworkSuggestions(suggestions)
val result = addNetworkSuggestions(suggestions)
return result == WifiManager.STATUS_NETWORK_SUGGESTIONS_SUCCESS
}
// Make sure the string ends with a semicolon.
private fun String.trimAndTerminate(): String {
var trimmed = trim()
if (trimmed.isNotEmpty() && trimmed.last() != ';') {
trimmed += ";"
}
return trimmed
}
// Keep possibility of wrongly unescaped \ by explicitly searching
// for special chars.
private val escapedRegex = """\\([\\;,":])""".toRegex()
private val String.unescape: String
get() = this.replace(escapedRegex) { escaped ->
escaped.groupValues[1]
}
private val String.quote: String
get() = if (startsWith("\"") && endsWith("\"")) this else "\"$this\""
private val String.quoteUnlessHex: String
get() = if (isHex()) this else this.quote
// Unfortunately, we can never know whether a string consisting only
// of 0-f is a hex string. So we just make this one assumption that
// a string of 0-f that is exactly 64 characters long, is _probably_
// a hex string for raw PSK. Of course, this may fail for any string
// that just happens to be 64 characters long and contains just 0-f.
private val hexRegex = """^[0-9a-f]+$""".toRegex(
RegexOption.IGNORE_CASE
)
private fun String.isHex() = length == 64 && matches(hexRegex)
private fun WifiManager.enableWifi(): Boolean {
// setWifiEnabled() will always return false for Android Q
// because Q doesn't allow apps to enable/disable Wi-Fi anymore.
@Suppress("DEPRECATION")
return isWifiEnabled || setWifiEnabled(true)
}
// WifiConfiguration is deprecated in Android Q.
@Suppress("DEPRECATION")
private fun WifiManager.removeOldNetwork(
wifiConfig: WifiConfiguration
): Boolean {
try {
configuredNetworks?.firstOrNull {
it.SSID == wifiConfig.SSID &&
it.allowedKeyManagement == wifiConfig.allowedKeyManagement
}?.networkId?.also {
removeNetwork(it)
}
} catch (e: SecurityException) {
// The user didn't allow ACCESS_FINE_LOCATION which is
// required to access configuredNetworks and that's fine.
}
return true
}
// WifiConfiguration is deprecated in Android Q.
@Suppress("DEPRECATION")
private fun WifiManager.enableNewNetwork(
wifiConfig: WifiConfiguration
): Boolean {
val id = addNetwork(wifiConfig)
if (id == -1) {
return false
}
disconnect()
return if (enableNetwork(id, true)) {
reconnect()
true
} else {
false
}
}
@@ -1,45 +1,74 @@
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.hardware.Camera
import android.net.Uri
import android.os.Bundle
import android.os.Vibrator
import android.support.design.widget.FloatingActionButton
import android.support.v7.app.AppCompatActivity
import android.support.v7.widget.Toolbar
import android.view.Menu
import android.view.MenuItem
import android.view.MotionEvent
import android.view.View
import android.widget.SeekBar
import android.widget.Toast
import com.google.zxing.Result
import com.google.zxing.ResultMetadataType
import de.markusfisch.android.binaryeye.R
import de.markusfisch.android.binaryeye.app.db
import de.markusfisch.android.binaryeye.app.hasCameraPermission
import de.markusfisch.android.binaryeye.app.initSystemBars
import de.markusfisch.android.binaryeye.app.prefs
import de.markusfisch.android.binaryeye.rs.Preprocessor
import de.markusfisch.android.binaryeye.zxing.Zxing
import de.markusfisch.android.binaryeye.adapter.prettifyFormatName
import de.markusfisch.android.binaryeye.app.*
import de.markusfisch.android.binaryeye.content.copyToClipboard
import de.markusfisch.android.binaryeye.content.execShareIntent
import de.markusfisch.android.binaryeye.content.openUrl
import de.markusfisch.android.binaryeye.content.toHexString
import de.markusfisch.android.binaryeye.database.toScan
import de.markusfisch.android.binaryeye.graphics.FrameMetrics
import de.markusfisch.android.binaryeye.graphics.mapPosition
import de.markusfisch.android.binaryeye.graphics.setFrameRoi
import de.markusfisch.android.binaryeye.graphics.setFrameToView
import de.markusfisch.android.binaryeye.media.releaseToneGenerators
import de.markusfisch.android.binaryeye.net.sendAsync
import de.markusfisch.android.binaryeye.net.urlEncode
import de.markusfisch.android.binaryeye.view.errorFeedback
import de.markusfisch.android.binaryeye.view.initSystemBars
import de.markusfisch.android.binaryeye.view.scanFeedback
import de.markusfisch.android.binaryeye.view.setPaddingFromWindowInsets
import de.markusfisch.android.binaryeye.widget.DetectorView
import de.markusfisch.android.binaryeye.widget.toast
import de.markusfisch.android.cameraview.widget.CameraView
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import de.markusfisch.android.zxingcpp.ZxingCpp
import de.markusfisch.android.zxingcpp.ZxingCpp.Binarizer
import de.markusfisch.android.zxingcpp.ZxingCpp.DecodeHints
import de.markusfisch.android.zxingcpp.ZxingCpp.Format
import de.markusfisch.android.zxingcpp.ZxingCpp.Result
import java.nio.charset.Charset
import kotlin.math.max
import kotlin.math.min
import kotlin.math.roundToInt
class CameraActivity : AppCompatActivity() {
private val zxing = Zxing()
private val frameRoi = Rect()
private val matrix = Matrix()
private lateinit var vibrator: Vibrator
private lateinit var cameraView: CameraView
private lateinit var detectorView: DetectorView
private lateinit var zoomBar: SeekBar
private lateinit var flashFab: View
private lateinit var flashFab: FloatingActionButton
private var preprocessor: Preprocessor? = null
private var invert = false
private var flash = false
private var formatsToRead = setOf<String>()
private var frameMetrics = FrameMetrics()
private var decoding = true
private var returnResult = false
private var returnUrlTemplate: String? = null
private var finishAfterShowingResult = false
private var frontFacing = false
private var bulkMode = prefs.bulkMode
private var restrictFormat: String? = null
private var ignoreNext: String? = null
private var fallbackBuffer: IntArray? = null
override fun onRequestPermissionsResult(
@@ -48,14 +77,10 @@ class CameraActivity : AppCompatActivity() {
grantResults: IntArray
) {
when (requestCode) {
REQUEST_CAMERA -> if (grantResults.isNotEmpty() &&
PERMISSION_CAMERA -> if (grantResults.isNotEmpty() &&
grantResults[0] != PackageManager.PERMISSION_GRANTED
) {
Toast.makeText(
this,
R.string.no_camera_no_fun,
Toast.LENGTH_SHORT
).show()
toast(R.string.no_camera_no_fun)
finish()
}
}
@@ -78,23 +103,30 @@ class CameraActivity : AppCompatActivity() {
}
}
override fun attachBaseContext(base: Context?) {
base?.applyLocale(prefs.customLocale)
super.attachBaseContext(base)
}
override fun onCreate(state: Bundle?) {
super.onCreate(state)
setContentView(R.layout.activity_camera)
// Necessary to get the right translation after setting a
// custom locale.
setTitle(R.string.scan_code)
initSystemBars(this)
setSupportActionBar(findViewById(R.id.toolbar) as Toolbar)
vibrator = getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
cameraView = findViewById(R.id.camera_view) as CameraView
detectorView = findViewById(R.id.detector_view) as DetectorView
zoomBar = findViewById(R.id.zoom) as SeekBar
flashFab = findViewById(R.id.flash)
flashFab.setOnClickListener { toggleTorchMode() }
flashFab = findViewById(R.id.flash) as FloatingActionButton
initCameraView()
initZoomBar()
restoreZoom()
initDetectorView()
if (intent?.action == Intent.ACTION_SEND &&
intent.type == "text/plain"
@@ -105,22 +137,57 @@ class CameraActivity : AppCompatActivity() {
override fun onDestroy() {
super.onDestroy()
preprocessor?.destroy()
fallbackBuffer = null
saveZoom()
detectorView.saveCropHandlePos()
releaseToneGenerators()
}
override fun onResume() {
super.onResume()
System.gc()
if (hasCameraPermission(this, REQUEST_CAMERA)) {
updateHints()
if (prefs.bulkMode && bulkMode != prefs.bulkMode) {
bulkMode = prefs.bulkMode
invalidateOptionsMenu()
ignoreNext = null
}
setReturnTarget(intent)
if (hasCameraPermission()) {
openCamera()
}
}
private fun updateHints() {
val restriction = restrictFormat
formatsToRead = if (restriction != null) {
title = getString(
R.string.scan_format,
prettifyFormatName(restriction)
)
setOf(restriction)
} else {
setTitle(R.string.scan_code)
prefs.barcodeFormats
}
}
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 openCamera() {
cameraView.openAsync(
CameraView.findCameraId(
@Suppress("DEPRECATION")
if (frontFacing) {
Camera.CameraInfo.CAMERA_FACING_FRONT
} else {
@@ -143,23 +210,39 @@ class CameraActivity : AppCompatActivity() {
super.onRestoreInstanceState(savedState)
zoomBar.max = savedState.getInt(ZOOM_MAX)
zoomBar.progress = savedState.getInt(ZOOM_LEVEL)
frontFacing = savedState.getBoolean(FRONT_FACING)
bulkMode = savedState.getBoolean(BULK_MODE)
restrictFormat = savedState.getString(RESTRICT_FORMAT)
}
override fun onSaveInstanceState(outState: Bundle) {
outState.putInt(ZOOM_MAX, zoomBar.max)
outState.putInt(ZOOM_LEVEL, zoomBar.progress)
outState.putBoolean(FRONT_FACING, frontFacing)
outState.putBoolean(BULK_MODE, bulkMode)
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.
if (detectorView.onTouchEvent(ev)) {
return true
}
return super.dispatchTouchEvent(ev)
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.activity_camera, menu)
menu.findItem(R.id.bulk_mode).isChecked = bulkMode
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.create -> {
startActivity(MainActivity.getEncodeIntent(this))
createBarcode()
true
}
R.id.history -> {
@@ -167,11 +250,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
@@ -182,6 +265,16 @@ class CameraActivity : AppCompatActivity() {
switchCamera()
true
}
R.id.bulk_mode -> {
bulkMode = bulkMode xor true
item.isChecked = bulkMode
ignoreNext = null
true
}
R.id.restrict_format -> {
showRestrictionDialog()
true
}
R.id.preferences -> {
startActivity(MainActivity.getPreferencesIntent(this))
true
@@ -194,20 +287,43 @@ class CameraActivity : AppCompatActivity() {
}
}
private fun createBarcode() {
startActivity(MainActivity.getEncodeIntent(this))
}
private fun switchCamera() {
closeCamera()
frontFacing = frontFacing xor true
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() {
startActivity(
Intent(
Intent.ACTION_VIEW, Uri.parse(
"https://github.com/markusfisch/BinaryEye/blob/master/README.md"
)
)
val intent = Intent(
Intent.ACTION_VIEW,
Uri.parse(getString(R.string.project_url))
)
execShareIntent(intent)
}
private fun handleSendText(intent: Intent) {
@@ -220,12 +336,55 @@ class CameraActivity : AppCompatActivity() {
private fun initCameraView() {
cameraView.setUseOrientationListener(true)
cameraView.setTapToFocus()
@Suppress("ClickableViewAccessibility")
cameraView.setOnTouchListener(object : View.OnTouchListener {
var focus = true
var offset = -1f
var progress = 0
override fun onTouch(v: View?, event: MotionEvent?): Boolean {
event ?: return false
val pos = event.y
when (event.actionMasked) {
MotionEvent.ACTION_DOWN -> {
offset = pos
progress = zoomBar.progress
return true
}
MotionEvent.ACTION_MOVE -> {
if (prefs.zoomBySwiping) {
v ?: return false
val dist = offset - pos
val maxValue = zoomBar.max
val change = maxValue / v.height.toFloat() * 2f * dist
zoomBar.progress = min(
maxValue,
max(progress + change.roundToInt(), 0)
)
return true
}
}
MotionEvent.ACTION_UP -> {
// Stop calling focusTo() as soon as it returns false
// to avoid throwing and catching future exceptions.
if (focus) {
focus = cameraView.focusTo(v, event.x, event.y)
if (focus) {
v?.performClick()
return true
}
}
}
}
return false
}
})
@Suppress("DEPRECATION")
cameraView.setOnCameraListener(object : CameraView.OnCameraListener {
override fun onConfigureParameters(
parameters: Camera.Parameters
) {
if (parameters.isZoomSupported) {
zoomBar.visibility = if (parameters.isZoomSupported) {
val max = parameters.maxZoom
if (zoomBar.max != max) {
zoomBar.max = max
@@ -233,8 +392,9 @@ class CameraActivity : AppCompatActivity() {
saveZoom()
}
parameters.zoom = zoomBar.progress
View.VISIBLE
} else {
zoomBar.visibility = View.GONE
View.GONE
}
val sceneModes = parameters.supportedSceneModes
sceneModes?.let {
@@ -250,33 +410,52 @@ class CameraActivity : AppCompatActivity() {
}
override fun onCameraError() {
Toast.makeText(
this@CameraActivity,
R.string.camera_error,
Toast.LENGTH_SHORT
).show()
this@CameraActivity.toast(R.string.camera_error)
finish()
}
override fun onCameraReady(camera: Camera) {
val frameWidth = cameraView.frameWidth
val frameHeight = cameraView.frameHeight
val frameOrientation = cameraView.frameOrientation
var decoding = true
frameMetrics = FrameMetrics(
cameraView.frameWidth,
cameraView.frameHeight,
cameraView.frameOrientation
)
updateFrameRoiAndMappingMatrix()
ignoreNext = null
decoding = true
// These settings can't change while the camera is open.
val decodeHints = DecodeHints(
tryHarder = prefs.tryHarder,
tryRotate = prefs.autoRotate,
tryInvert = true,
tryDownscale = true
)
var useLocalAverage = false
camera.setPreviewCallback { frameData, _ ->
if (decoding) {
val result = decodeFrame(
useLocalAverage = useLocalAverage xor true
ZxingCpp.readByteArray(
frameData,
frameWidth,
frameHeight,
frameOrientation
)
result?.let {
cameraView.post {
vibrator.vibrate(100)
showResult(this@CameraActivity, result)
frameMetrics.width,
frameRoi.left, frameRoi.top,
frameRoi.width(), frameRoi.height(),
frameMetrics.orientation,
decodeHints.apply {
// By default, ZXing uses LOCAL_AVERAGE, but
// this does not work well with inverted
// barcodes on low-contrast backgrounds.
binarizer = if (useLocalAverage) {
Binarizer.LOCAL_AVERAGE
} else {
Binarizer.GLOBAL_HISTOGRAM
}
formats = formatsToRead.joinToString()
}
)?.let { result ->
if (result.text != ignoreNext) {
postResult(result)
decoding = false
}
decoding = false
}
}
}
@@ -299,25 +478,24 @@ class CameraActivity : AppCompatActivity() {
progress: Int,
fromUser: Boolean
) {
setZoom(progress)
cameraView.camera?.setZoom(progress)
}
override fun onStartTrackingTouch(seekBar: SeekBar) {}
override fun onStopTrackingTouch(seekBar: SeekBar) {}
})
restoreZoom()
}
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
}
@Suppress("DEPRECATION")
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.
}
}
@@ -336,150 +514,185 @@ class CameraActivity : AppCompatActivity() {
)
}
private fun updateFlashFab(available: Boolean) {
flashFab.visibility = if (available) {
View.GONE
private fun initDetectorView() {
detectorView.onRoiChange = {
decoding = false
}
detectorView.onRoiChanged = {
decoding = true
updateFrameRoiAndMappingMatrix()
}
detectorView.setPaddingFromWindowInsets()
detectorView.restoreCropHandlePos()
}
private fun updateFrameRoiAndMappingMatrix() {
val viewRect = cameraView.previewRect
val viewRoi = if (detectorView.roi.width() < 1) {
viewRect
} else {
View.VISIBLE
detectorView.roi
}
frameRoi.setFrameRoi(frameMetrics, viewRect, viewRoi)
matrix.setFrameToView(frameMetrics, viewRect, viewRoi)
}
private fun updateFlashFab(unavailable: Boolean) {
if (unavailable) {
flashFab.setImageResource(R.drawable.ic_action_create)
flashFab.setOnClickListener { createBarcode() }
} else {
flashFab.setImageResource(R.drawable.ic_action_flash)
flashFab.setOnClickListener { toggleTorchMode() }
}
}
@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))
}
}
private fun postResult(result: Result) {
cameraView.post {
detectorView.update(
matrix.mapPosition(
result.position,
detectorView.coordinates
)
)
scanFeedback()
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,
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) {
if (prefs.ignoreConsecutiveDuplicates) {
ignoreNext = result.text
}
if (prefs.showToastInBulkMode) {
toast(result.text)
}
detectorView.postDelayed({
decoding = true
}, prefs.bulkModeDelay.toLong())
}
}
}
private fun decodeFrame(
frameData: ByteArray?,
frameWidth: Int,
frameHeight: Int,
frameOrientation: Int
): Result? {
frameData ?: return null
invert = invert xor true
val pp = preprocessor ?: Preprocessor(
this,
frameWidth,
frameHeight,
frameOrientation
)
pp.process(frameData)
preprocessor = pp
return zxing.decode(
frameData,
pp.outWidth,
pp.outHeight,
invert
)
}
companion object {
private const val REQUEST_CAMERA = 1
private const val PICK_FILE_RESULT_CODE = 1
private const val ZOOM_MAX = "zoom_max"
private const val ZOOM_LEVEL = "zoom_level"
private const val FRONT_FACING = "front_facing"
private const val BULK_MODE = "bulk_mode"
private const val RESTRICT_FORMAT = "restrict_format"
}
}
fun showResult(
activity: Activity,
result: Result,
isResult: Boolean = false
bulkMode: Boolean = false,
) {
val rawBytes = getRawBytes(result)
if (prefs.copyImmediately) {
activity.copyToClipboard(result.text)
}
val scan = result.toScan()
if (prefs.useHistory) {
GlobalScope.launch {
db.insertScan(
System.currentTimeMillis(),
result.text,
rawBytes,
result.barcodeFormat.toString()
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) {
activity.errorFeedback()
}
if (body != null && body.isNotEmpty()) {
activity.toast(body)
} else if (code == null || code > 299) {
activity.toast(R.string.background_request_failed)
}
}
}
if (isResult) {
activity.setResult(Activity.RESULT_OK, getReturnIntent(result))
activity.finish()
} else {
if (!bulkMode) {
activity.startActivity(
MainActivity.getDecodeIntent(
activity,
result.text,
result.barcodeFormat,
rawBytes
)
MainActivity.getDecodeIntent(activity, scan)
)
}
}
fun getRawBytes(result: Result): ByteArray? {
val metadata = result.resultMetadata ?: return null
val segments = metadata[ResultMetadataType.BYTE_SEGMENTS] ?: return null
var bytes = ByteArray(0)
@Suppress("UNCHECKED_CAST")
for (seg in segments as Iterable<ByteArray>) {
bytes += seg
private fun getReturnIntent(result: Result) = Intent().apply {
putExtra("SCAN_RESULT", result.text)
putExtra("SCAN_RESULT_FORMAT", result.format)
putExtra("SCAN_RESULT_ORIENTATION", result.orientation)
putExtra("SCAN_RESULT_ERROR_CORRECTION_LEVEL", result.ecLevel)
if (result.rawBytes.isNotEmpty()) {
putExtra("SCAN_RESULT_BYTES", result.rawBytes)
}
// 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
}
fun getReturnIntent(result: Result): Intent {
val intent = Intent()
intent.putExtra("SCAN_RESULT", result.text)
intent.putExtra(
"SCAN_RESULT_FORMAT",
result.barcodeFormat.toString()
)
if (result.rawBytes?.isNotEmpty() == true) {
intent.putExtra("SCAN_RESULT_BYTES", result.rawBytes)
}
val metadata = result.resultMetadata
metadata?.let {
val orientation = metadata[ResultMetadataType.UPC_EAN_EXTENSION]
orientation?.let {
intent.putExtra(
"SCAN_RESULT_ORIENTATION",
orientation.toString()
)
}
val ecLevel = metadata[ResultMetadataType.ERROR_CORRECTION_LEVEL]
ecLevel?.let {
intent.putExtra(
"SCAN_RESULT_ERROR_CORRECTION_LEVEL",
ecLevel.toString()
)
}
val upcEanExtension = metadata[ResultMetadataType.UPC_EAN_EXTENSION]
upcEanExtension?.let {
intent.putExtra(
"SCAN_RESULT_UPC_EAN_EXTENSION",
upcEanExtension.toString()
)
}
val segments = metadata[ResultMetadataType.BYTE_SEGMENTS]
segments?.let {
var i = 0
@Suppress("UNCHECKED_CAST")
for (seg in segments as Iterable<ByteArray>) {
intent.putExtra("SCAN_RESULT_BYTE_SEGMENTS_$i", seg)
++i
}
}
}
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.format.urlEncode()
)
// And support {CODE} from the old ZXing app, too.
.replace("{CODE}", result.text.urlEncode())
)
@@ -2,21 +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 com.google.zxing.BarcodeFormat
import de.markusfisch.android.binaryeye.R
import de.markusfisch.android.binaryeye.app.initSystemBars
import de.markusfisch.android.binaryeye.app.setFragment
import de.markusfisch.android.binaryeye.app.setSystemAndToolBarTransparency
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) {
@@ -27,40 +45,27 @@ class MainActivity : AppCompatActivity() {
return true
}
override fun attachBaseContext(base: Context?) {
base?.applyLocale(prefs.customLocale)
super.attachBaseContext(base)
}
override fun onCreate(state: Bundle?) {
super.onCreate(state)
setContentView(R.layout.activity_main)
initSystemBars(this)
setSupportActionBar(findViewById(R.id.toolbar) as Toolbar)
initSystemBars(this)
val toolbar = findViewById(R.id.toolbar) as Toolbar
recordToolbarHeight(toolbar)
setSupportActionBar(toolbar)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
supportFragmentManager.addOnBackStackChangedListener {
setSystemAndToolBarTransparency(this@MainActivity)
colorSystemAndToolBars(this@MainActivity)
}
if (state == null) {
setFragment(
supportFragmentManager, when {
intent?.hasExtra(PREFERENCES) == true ->
PreferencesFragment()
intent?.hasExtra(HISTORY) == true ->
HistoryFragment()
intent?.hasExtra(ENCODE) == true ->
EncodeFragment.newInstance(
intent.getStringExtra(ENCODE) ?: ""
)
intent?.hasExtra(DECODED_TEXT) == true ->
DecodeFragment.newInstance(
intent.getStringExtra(DECODED_TEXT) ?: "",
intent.getSerializableExtra(
DECODED_FORMAT
) as BarcodeFormat,
intent.getByteArrayExtra(DECODED_RAW)
)
else -> DecodeFragment()
}
)
supportFragmentManager?.setFragment(getFragmentForIntent(intent))
}
}
@@ -68,9 +73,22 @@ class MainActivity : AppCompatActivity() {
private const val PREFERENCES = "preferences"
private const val HISTORY = "history"
private const val ENCODE = "encode"
private const val DECODED_TEXT = "decoded_text"
private const val DECODED_FORMAT = "decoded_format"
private const val DECODED_RAW = "decoded_raw"
const val DECODED = "decoded"
private fun getFragmentForIntent(intent: Intent?): Fragment {
intent ?: return PreferencesFragment()
return when {
intent.hasExtra(PREFERENCES) -> PreferencesFragment()
intent.hasExtra(HISTORY) -> HistoryFragment()
intent.hasExtra(ENCODE) -> EncodeFragment.newInstance(
intent.getStringExtra(ENCODE)
)
intent.hasExtra(DECODED) -> DecodeFragment.newInstance(
intent.getParcelableExtra(DECODED)!!
)
else -> PreferencesFragment()
}
}
fun getPreferencesIntent(context: Context): Intent {
val intent = Intent(context, MainActivity::class.java)
@@ -105,18 +123,9 @@ class MainActivity : AppCompatActivity() {
return intent
}
fun getDecodeIntent(
context: Context,
text: String,
format: BarcodeFormat,
raw: ByteArray? = null
): Intent {
fun getDecodeIntent(context: Context, scan: Scan): Intent {
val intent = Intent(context, MainActivity::class.java)
intent.putExtra(DECODED_TEXT, text)
intent.putExtra(DECODED_FORMAT, format)
if (raw != null) {
intent.putExtra(DECODED_RAW, raw)
}
intent.putExtra(DECODED, scan)
return intent
}
}
@@ -1,124 +1,200 @@
package de.markusfisch.android.binaryeye.activity
import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.Matrix
import android.graphics.Rect
import android.graphics.RectF
import android.net.Uri
import android.os.Bundle
import android.os.Parcelable
import android.provider.MediaStore
import android.support.v7.app.AppCompatActivity
import android.support.v7.widget.Toolbar
import android.view.Menu
import android.view.MenuItem
import android.widget.Toast
import com.google.zxing.Result
import de.markusfisch.android.binaryeye.R
import de.markusfisch.android.binaryeye.app.initSystemBars
import de.markusfisch.android.binaryeye.app.setSystemAndToolBarTransparency
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.downsizeIfBigger
import de.markusfisch.android.binaryeye.graphics.fixTransparency
import de.markusfisch.android.binaryeye.graphics.loadImageUri
import de.markusfisch.android.binaryeye.graphics.mapPosition
import de.markusfisch.android.binaryeye.media.releaseToneGenerators
import de.markusfisch.android.binaryeye.view.*
import de.markusfisch.android.binaryeye.widget.CropImageView
import de.markusfisch.android.binaryeye.zxing.Zxing
import java.io.IOException
import kotlin.math.min
import de.markusfisch.android.binaryeye.widget.DetectorView
import de.markusfisch.android.binaryeye.widget.toast
import de.markusfisch.android.zxingcpp.ZxingCpp
import de.markusfisch.android.zxingcpp.ZxingCpp.Binarizer
import de.markusfisch.android.zxingcpp.ZxingCpp.DecodeHints
import de.markusfisch.android.zxingcpp.ZxingCpp.Result
import kotlinx.coroutines.*
import kotlin.math.max
import kotlin.math.min
import kotlin.math.roundToInt
class PickActivity : AppCompatActivity() {
private val zxing = Zxing()
private val matrix = Matrix()
private val parentJob = Job()
private val scope = CoroutineScope(Dispatchers.IO + parentJob)
private val decodeHints = DecodeHints(
tryHarder = true,
tryRotate = true,
tryInvert = true,
tryDownscale = true,
formats = prefs.barcodeFormats.joinToString()
)
private lateinit var cropImageView: CropImageView
private lateinit var detectorView: DetectorView
private lateinit var freeRotationItem: MenuItem
private var returnResult = false
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)
initSystemBars(this)
setSupportActionBar(findViewById(R.id.toolbar) as Toolbar)
// Necessary to get the right translation after setting a custom
// locale.
setTitle(R.string.pick_code_to_scan)
initSystemBars(this)
val toolbar = findViewById(R.id.toolbar) as Toolbar
recordToolbarHeight(toolbar)
setSupportActionBar(toolbar)
supportFragmentManager.addOnBackStackChangedListener {
setSystemAndToolBarTransparency(this@PickActivity)
}
returnResult = intent?.action == "com.google.zxing.client.android.SCAN"
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
colorSystemAndToolBars(this@PickActivity)
}
val bitmap = getBitmapFromIntent()?.fixTransparency()
if (bitmap == null) {
Toast.makeText(
applicationContext,
R.string.error_no_content,
Toast.LENGTH_SHORT
).show()
applicationContext.toast(R.string.error_no_content)
finish()
return
}
val scannedRect = Rect()
val scanBounds: () -> Result? = {
var result: Result? = null
crop(
bitmap,
cropImageView.normalizedRectInBounds,
cropImageView.imageRotation
)?.also {
scannedRect.set(0, 0, it.width, it.height)
result = zxing.decodePositiveNegative(it)
}
result
}
cropImageView = findViewById(R.id.image) as CropImageView
cropImageView.restrictTranslation = false
cropImageView.freeRotation = prefs.freeRotation
cropImageView.setImageBitmap(bitmap)
cropImageView.onScan = {
val rect = Rect()
scanBounds()?.also {
rect.left = Int.MAX_VALUE
rect.top = Int.MAX_VALUE
for (rp in it.resultPoints) {
val x = rp.x.toInt()
val y = rp.y.toInt()
rect.left = min(rect.left, x)
rect.right = max(rect.right, x)
rect.top = min(rect.top, y)
rect.bottom = max(rect.bottom, y)
}
// map result points onto view
val bounds = cropImageView.getBoundsRect()
val fx = bounds.width() / scannedRect.width().toFloat()
val fy = bounds.height() / scannedRect.height().toFloat()
rect.set(
(rect.left * fx).toInt(),
(rect.top * fy).toInt(),
(rect.right * fx).toInt(),
(rect.bottom * fx).toInt()
)
rect.offset(bounds.left.toInt(), bounds.top.toInt())
}
rect
scanWithinBounds(bitmap)
}
findViewById(R.id.scan).setOnClickListener {
scanImage(scanBounds())
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 viewRoi = if (detectorView.roi.width() < 1) {
cropImageView.getBoundsRect()
} else {
detectorView.roi
}
val mappedRect = cropImageView.mappedRect
val cropped = bitmap.crop(
getNormalizedRoi(mappedRect, viewRoi),
cropImageView.imageRotation,
cropImageView.pivotX,
cropImageView.pivotY
) ?: return
val croppedInView = Rect(
max(viewRoi.left, mappedRect.left.roundToInt()),
max(viewRoi.top, mappedRect.top.roundToInt()),
min(viewRoi.right, mappedRect.right.roundToInt()),
min(viewRoi.bottom, mappedRect.bottom.roundToInt())
)
matrix.apply {
setScale(
croppedInView.width().toFloat() / cropped.width,
croppedInView.height().toFloat() / cropped.height
)
postTranslate(
croppedInView.left.toFloat(),
croppedInView.top.toFloat()
)
}
scope.launch {
cropped.decode()?.let {
withContext(Dispatchers.Main) {
if (isFinishing) {
return@withContext
}
result = it
scanFeedback()
detectorView.update(
matrix.mapPosition(
it.position,
detectorView.coordinates
)
)
}
}
}
}
// By default, ZXing uses LOCAL_AVERAGE, but this does not work
// well with inverted barcodes on low-contrast backgrounds.
private fun Bitmap.decode() = ZxingCpp.readBitmap(
this,
0, 0,
width, height,
0,
decodeHints.apply {
binarizer = Binarizer.LOCAL_AVERAGE
}
) ?: ZxingCpp.readBitmap(
this,
0, 0,
width, height,
0,
decodeHints.apply {
binarizer = Binarizer.GLOBAL_HISTOGRAM
}
)
override fun onDestroy() {
super.onDestroy()
detectorView.saveCropHandlePos()
parentJob.cancel()
releaseToneGenerators()
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.activity_pick, menu)
freeRotationItem = menu.findItem(R.id.toggle_free).apply {
updateFreeRotationIcon()
}
return true
}
@@ -128,10 +204,26 @@ class PickActivity : AppCompatActivity() {
rotateClockwise()
true
}
R.id.toggle_free -> {
prefs.freeRotation = prefs.freeRotation xor true
cropImageView.freeRotation = prefs.freeRotation
freeRotationItem.updateFreeRotationIcon()
true
}
else -> super.onOptionsItemSelected(item)
}
}
private fun MenuItem.updateFreeRotationIcon() {
setIcon(
if (prefs.freeRotation) {
R.drawable.ic_action_rotation_unlocked
} else {
R.drawable.ic_action_rotation_locked
}
)
}
private fun rotateClockwise() {
cropImageView.imageRotation = cropImageView.imageRotation + 90 % 360
}
@@ -140,33 +232,32 @@ class PickActivity : AppCompatActivity() {
val uri = intent.getParcelableExtra<Parcelable>(
Intent.EXTRA_STREAM
) as? Uri ?: return null
return loadImageUri(uri)
return contentResolver.loadImageUri(uri)
}
private fun loadImageToOpen(intent: Intent): Bitmap? {
val uri = intent.data ?: return null
return loadImageUri(uri)
return contentResolver.loadImageUri(uri)
}
private fun loadImageUri(uri: Uri): Bitmap? = try {
downsizeIfBigger(
MediaStore.Images.Media.getBitmap(contentResolver, uri),
1024
)
} catch (e: IOException) {
null
}
private fun scanImage(result: Result?) {
if (result != null) {
showResult(this, result, returnResult)
private fun showResult() {
val r = result
if (r != null) {
showResult(this, r)
finish()
return
} else {
applicationContext.toast(R.string.no_barcode_found)
}
Toast.makeText(
applicationContext,
R.string.no_barcode_found,
Toast.LENGTH_SHORT
).show()
}
}
private fun getNormalizedRoi(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
)
}
@@ -8,9 +8,9 @@ class SplashActivity : AppCompatActivity() {
override fun onCreate(state: Bundle?) {
super.onCreate(state)
// it's important _not_ to inflate a layout file here
// It's important _not_ to inflate a layout file here
// because that would happen after the app is fully
// initialized what is too late
// initialized what is too late.
startActivity(Intent(applicationContext, CameraActivity::class.java))
finish()
@@ -2,54 +2,132 @@ package de.markusfisch.android.binaryeye.adapter
import android.content.Context
import android.database.Cursor
import android.support.v4.content.ContextCompat
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.CursorAdapter
import android.widget.TextView
import de.markusfisch.android.binaryeye.R
import de.markusfisch.android.binaryeye.data.Database
import de.markusfisch.android.binaryeye.database.Database
import java.text.DateFormat
import java.text.ParseException
import java.text.SimpleDateFormat
import java.util.*
class ScansAdapter(context: Context, cursor: Cursor) :
CursorAdapter(context, cursor, false) {
private val idIndex = cursor.getColumnIndex(Database.SCANS_ID)
private val timeIndex = cursor.getColumnIndex(Database.SCANS_DATETIME)
private val nameIndex = cursor.getColumnIndex(Database.SCANS_NAME)
private val contentIndex = cursor.getColumnIndex(Database.SCANS_CONTENT)
private val formatIndex = cursor.getColumnIndex(Database.SCANS_FORMAT)
private val selections = mutableMapOf<Long, Int>()
private val selectedColor = ContextCompat.getColor(
context, R.color.selected_row
)
fun select(view: View, id: Long, position: Int) {
view.select(
if (selections[id] == null) {
selections[id] = position
true
} else {
selections.remove(id)
false
}
)
}
fun clearSelection() {
selections.clear()
}
fun forSelection(callback: (Long, Int) -> Unit) {
selections.forEach {
callback.invoke(it.key, it.value)
}
}
fun getSelectedIds(): List<Long> = mutableListOf<Long>().apply {
forSelection { id, _ ->
add(id)
}
}
fun getSelectedContent(separator: String): String {
val sb = StringBuilder()
forSelection { _, position ->
getContent(position)?.let {
if (sb.isNotEmpty()) {
sb.append(separator)
}
sb.append(it)
}
}
return sb.toString()
}
fun getName(
position: Int
) = (getItem(position) as Cursor?)?.getString(nameIndex)
fun getContent(
position: Int
) = (getItem(position) as Cursor?)?.getString(contentIndex)
override fun newView(
context: Context,
cursor: Cursor,
parent: ViewGroup
): View {
return LayoutInflater.from(parent.context).inflate(
R.layout.row_scan, parent, false
)
}
): View? = LayoutInflater.from(parent.context).inflate(
R.layout.row_scan, parent, false
)
override fun bindView(
view: View,
context: Context,
cursor: Cursor
) {
val holder = getViewHolder(view)
val content = cursor.getString(contentIndex)
holder.timeView.text = cursor.getString(timeIndex)
holder.contentView.text = if (content.isEmpty()) {
context.getString(R.string.binary_data)
} else {
content
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
}
contentView.setCompoundDrawablesWithIntrinsicBounds(
icon, 0, 0, 0
)
formatView.text = prettifyFormatName(cursor.getString(formatIndex))
}
val selected = selections[cursor.getLong(idIndex)] != null
view.post {
// Needs to be put on the queue to work.
view.select(selected)
}
holder.formatView.text = cursor.getString(formatIndex)
}
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 View.select(selected: Boolean) {
setBackgroundColor(if (selected) selectedColor else 0)
}
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(
@@ -58,3 +136,19 @@ class ScansAdapter(context: Context, cursor: Cursor) :
val formatView: TextView
)
}
fun prettifyFormatName(format: String) = format.replace("_", " ")
private fun formatDateTime(rfc: String): String {
try {
SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.US).parse(rfc)?.let {
return DateFormat.getDateTimeInstance(
DateFormat.LONG,
DateFormat.SHORT
).format(it)
}
} catch (e: ParseException) {
// Ignore and fall through.
}
return rfc
}
@@ -0,0 +1,23 @@
package de.markusfisch.android.binaryeye.app
import android.app.AlertDialog
import android.content.Context
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
suspend inline fun <T : Any> alertDialog(
context: Context,
crossinline build: AlertDialog.Builder.(resume: (T?) -> Unit) -> Unit
): T? = withContext(Dispatchers.Main) {
suspendCoroutine { continuation ->
AlertDialog.Builder(context).apply {
setOnCancelListener {
continuation.resume(null)
}
build(continuation::resume)
show()
}
}
}
@@ -1,21 +1,15 @@
package de.markusfisch.android.binaryeye.app
import android.app.Application
import android.support.v8.renderscript.RenderScript
import de.markusfisch.android.binaryeye.database.Database
import de.markusfisch.android.binaryeye.preference.Preferences
import de.markusfisch.android.binaryeye.repository.DatabaseRepository
val db = DatabaseRepository()
val db = Database()
val prefs = Preferences()
class BinaryEyeApp : Application() {
override fun onCreate() {
super.onCreate()
// required to make RenderScript work for Lineage 16.0
// possibly because of a system/device bug
RenderScript.forceCompat()
db.open(this)
prefs.init(this)
}
@@ -1,62 +0,0 @@
package de.markusfisch.android.binaryeye.app
import android.annotation.SuppressLint
import android.content.Context
import android.support.v4.app.Fragment
import android.support.v4.app.FragmentManager
import android.support.v7.app.AlertDialog
import android.view.View
import de.markusfisch.android.binaryeye.R
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
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)
suspend inline fun View.useVisibility(
whileExecuting: Int = View.VISIBLE,
otherwise: Int = View.GONE,
crossinline block: suspend () -> Unit
) {
if (visibility == whileExecuting) {
return
}
withContext(Dispatchers.Main) {
visibility = whileExecuting
}
try {
block()
} finally {
withContext(Dispatchers.Main) {
visibility = otherwise
}
}
}
suspend inline fun <T : Any> alertDialog(
context: Context,
crossinline build: AlertDialog.Builder.(resume: (T?) -> Unit) -> Unit
): T? = withContext(Dispatchers.Main) {
suspendCoroutine<T?> { continuation ->
AlertDialog.Builder(context).apply {
setOnCancelListener {
continuation.resume(null)
}
build(continuation::resume)
show()
}
}
}
@@ -1,12 +0,0 @@
package de.markusfisch.android.binaryeye.app
import android.net.Uri
import android.os.Build
private val nonPrintable = "[\\x00-\\x08\\x0e-\\x1f]".toRegex()
fun hasNonPrintableCharacters(s: String) = nonPrintable.containsMatchIn(s)
fun parseAndNormalizeUri(input: String): Uri = Uri.parse(input).let {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) it.normalizeScheme() else it
}
@@ -1,57 +0,0 @@
package de.markusfisch.android.binaryeye.app
import android.app.Activity
import android.content.Context
import android.os.Environment
import android.support.annotation.MainThread
import android.widget.EditText
import de.markusfisch.android.binaryeye.R
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.collect
import java.io.File
import java.io.IOException
fun addSuffixIfNotGiven(name: String, suffix: String): String {
val trimmed = name.trim()
return if (suffix.isNotEmpty() && !trimmed.endsWith(suffix)) {
"$trimmed$suffix"
} else {
trimmed
}
}
// Dialogs doesn't have any root layout, need to be inflated with null root
@Suppress("InflateParams")
@MainThread
suspend fun Activity.askForFileName(suffix: String = ""): String? {
val view = layoutInflater.inflate(R.layout.dialog_save_file, null)
val editText = view.findViewById<EditText>(R.id.file_name)
return alertDialog<String>(this as Context) { resume ->
setView(view)
setPositiveButton(android.R.string.ok) { _, _ ->
resume(editText.text.toString())
}
}?.let { name ->
addSuffixIfNotGiven(name, suffix)
}
}
suspend fun Flow<ByteArray>.writeToFile(name: String): Int {
return try {
val file = File(
Environment.getExternalStoragePublicDirectory(
Environment.DIRECTORY_DOWNLOADS
),
name
)
if (file.exists()) {
return R.string.error_file_exists
}
collect { bytes ->
file.appendBytes(bytes)
}
R.string.saved_in_downloads
} catch (e: IOException) {
R.string.error_saving_binary_data
}
}
@@ -1,23 +0,0 @@
package de.markusfisch.android.binaryeye.app
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.emitAll
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.scanReduce
fun Flow<ByteArray>.addDelimiter(delimiter: String) = addDelimiter(
delimiter.toByteArray(),
ByteArray::plus
)
inline fun <T, R> Flow<T>.addDelimiter(
delimiter: R,
crossinline append: R.(T) -> T
): Flow<T> = scanReduce { _, content ->
delimiter.append(content)
}
infix fun <T> T.asFlowWith(other: Flow<T>) = flow<T> {
emit(this@asFlowWith)
emitAll(other)
}
@@ -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,33 +6,46 @@ import android.content.pm.PackageManager
import android.support.v4.app.ActivityCompat
import android.support.v4.content.ContextCompat
fun hasCameraPermission(activity: Activity, requestCode: Int): Boolean {
return hasPermission(activity, Manifest.permission.CAMERA, requestCode)
}
var permissionGrantedCallback: (() -> Any)? = null
fun hasWritePermission(activity: Activity, requestCode: Int = 1): Boolean {
const val PERMISSION_CAMERA = 1
fun Activity.hasCameraPermission() = hasPermission(
Manifest.permission.CAMERA,
PERMISSION_CAMERA
)
const val PERMISSION_WRITE = 2
fun Activity.hasWritePermission(callback: () -> Any): Boolean {
permissionGrantedCallback = callback
return hasPermission(
activity,
Manifest.permission.WRITE_EXTERNAL_STORAGE,
requestCode
PERMISSION_WRITE
)
}
fun hasPermission(
activity: Activity,
const val PERMISSION_LOCATION = 3
fun Activity.hasLocationPermission(callback: () -> Any): Boolean {
permissionGrantedCallback = callback
return hasPermission(
Manifest.permission.ACCESS_FINE_LOCATION,
PERMISSION_LOCATION
)
}
private fun Activity.hasPermission(
permission: String,
requestCode: Int
): Boolean {
return if (ContextCompat.checkSelfPermission(activity, permission) !=
PackageManager.PERMISSION_GRANTED
) {
ActivityCompat.requestPermissions(
activity,
arrayOf(permission),
requestCode
)
false
} else {
true
}
) = if (ContextCompat.checkSelfPermission(
this,
permission
) != PackageManager.PERMISSION_GRANTED
) {
ActivityCompat.requestPermissions(
this,
arrayOf(permission),
requestCode
)
false
} else {
true
}
@@ -1,54 +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 android.widget.Toast
import de.markusfisch.android.binaryeye.BuildConfig
import de.markusfisch.android.binaryeye.R
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 {
Toast.makeText(
context,
R.string.cannot_resolve_action,
Toast.LENGTH_SHORT
).show()
}
}
fun shareFile(context: Context, file: File, type: String) {
shareUri(context, getUriForFile(context, file), 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,83 +0,0 @@
package de.markusfisch.android.binaryeye.app
import android.content.Context
import android.graphics.Rect
import android.graphics.drawable.ColorDrawable
import android.os.Build
import android.support.v4.content.ContextCompat
import android.support.v4.view.ViewCompat
import android.support.v7.app.AppCompatActivity
import android.view.View
import android.widget.AbsListView
import de.markusfisch.android.binaryeye.R
val windowInsets = Rect()
val systemBarScrollListener = 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
view.post {
val scrolled = firstVisibleItem > 0 ||
(totalItemCount > 0 && view.getChildAt(0).top < 0)
val scrollable = if (scrolled) true else totalItemCount > 0 &&
view.getChildAt(view.lastVisiblePosition).bottom > view.height
setSystemAndToolBarTransparency(view.context, scrolled, scrollable)
}
}
override fun onScrollStateChanged(
view: AbsListView,
scrollState: Int
) {
}
}
fun setSystemAndToolBarTransparency(
context: Context,
scrolled: Boolean = false,
scrollable: Boolean = false
) {
val opaqueColor = ContextCompat.getColor(context, R.color.primary)
val transparentColor = 0x00000000
val topColor = if (scrolled) opaqueColor else transparentColor
val bottomColor = if (scrolled || scrollable) opaqueColor else transparentColor
val activity = context as AppCompatActivity
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
val window = activity.window
window.statusBarColor = topColor
window.navigationBarColor = bottomColor
}
activity.supportActionBar?.setBackgroundDrawable(ColorDrawable(topColor))
}
fun initSystemBars(activity: AppCompatActivity) {
activity.findViewById(R.id.main_layout)?.also { view ->
setWindowInsetListener(view)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
activity.window.decorView.systemUiVisibility =
View.SYSTEM_UI_FLAG_LAYOUT_STABLE or
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
}
setSystemAndToolBarTransparency(activity)
}
private fun setWindowInsetListener(view: View) {
ViewCompat.setOnApplyWindowInsetsListener(view) { _, insets ->
if (insets.hasSystemWindowInsets()) {
val left = insets.systemWindowInsetLeft
val top = insets.systemWindowInsetTop
val right = insets.systemWindowInsetRight
val bottom = insets.systemWindowInsetBottom
view.setPadding(left, top, right, bottom)
windowInsets.set(left, top, right, bottom)
}
insets.consumeSystemWindowInsets()
}
}
@@ -0,0 +1,37 @@
package de.markusfisch.android.binaryeye.content
import android.content.ClipData
import android.content.ClipDescription
import android.content.Context
import android.os.Build
import android.os.PersistableBundle
fun Context.copyToClipboard(text: String, isSensitive: Boolean = false) {
val service = getSystemService(Context.CLIPBOARD_SERVICE)
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) {
@Suppress("DEPRECATION")
(service as android.text.ClipboardManager).text = text
} else {
(service as android.content.ClipboardManager).setPrimaryClip(
ClipData.newPlainText("plain text", text).apply {
setSensitive(isSensitive)
}
)
}
}
private fun ClipData.setSensitive(isSensitive: Boolean) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
description.extras = PersistableBundle().apply {
putBoolean(EXTRA_IS_SENSITIVE, isSensitive)
}
}
}
private val EXTRA_IS_SENSITIVE = if (
Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU
) {
ClipDescription.EXTRA_IS_SENSITIVE
} else {
"android.content.extra.IS_SENSITIVE"
}
@@ -0,0 +1,3 @@
package de.markusfisch.android.binaryeye.content
fun ByteArray.toHexString() = joinToString("") { "%02X".format(it) }
@@ -0,0 +1,86 @@
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.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, silent: Boolean = false): Boolean {
val intent = Intent(Intent.ACTION_VIEW, url.parseAndNormalizeUri())
return if (silent) {
startIntent(intent)
} else {
execShareIntent(intent)
}
}
fun String.parseAndNormalizeUri(): Uri = Uri.parse(this).let {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
it.normalizeScheme()
} else {
it
}
}
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
})
}
@@ -1,122 +0,0 @@
package de.markusfisch.android.binaryeye.data
import android.content.ContentValues
import android.content.Context
import android.database.Cursor
import android.database.sqlite.SQLiteDatabase
import android.database.sqlite.SQLiteOpenHelper
import android.text.format.DateFormat
import de.markusfisch.android.binaryeye.app.hasNonPrintableCharacters
class Database {
private lateinit var db: SQLiteDatabase
fun open(context: Context) {
db = OpenHelper(context).writableDatabase
}
fun getScans(): Cursor? = db.rawQuery(
"""SELECT
$SCANS_ID,
$SCANS_DATETIME,
$SCANS_CONTENT,
$SCANS_FORMAT
FROM $SCANS
ORDER BY $SCANS_DATETIME DESC
""", null
)
fun getScan(id: Long): Cursor? = db.rawQuery(
"""SELECT
$SCANS_DATETIME,
$SCANS_CONTENT,
$SCANS_RAW,
$SCANS_FORMAT
FROM $SCANS
WHERE $SCANS_ID = ?
""", arrayOf("$id")
)
fun hasBinaryData(): Cursor? = db.rawQuery(
"""SELECT
1
FROM $SCANS
WHERE $SCANS_RAW IS NOT NULL
LIMIT 1
""", null
)
fun insertScan(
timestamp: Long,
content: String,
raw: ByteArray?,
format: String
): Long {
val cv = ContentValues()
cv.put(
SCANS_DATETIME, DateFormat.format(
"yyyy-MM-dd HH:mm:ss",
timestamp
).toString()
)
if (hasNonPrintableCharacters(content) && raw != null) {
cv.put(SCANS_CONTENT, "")
cv.put(SCANS_RAW, raw)
} else {
cv.put(SCANS_CONTENT, content)
}
cv.put(SCANS_FORMAT, format)
return db.insert(SCANS, null, cv)
}
fun removeScan(id: Long) {
db.delete(SCANS, "$SCANS_ID = ?", arrayOf("$id"))
}
fun removeScans() {
db.delete(SCANS, null, null)
}
private class OpenHelper(context: Context) :
SQLiteOpenHelper(context, "history.db", null, 2) {
override fun onCreate(db: SQLiteDatabase) {
createScans(db)
}
override fun onUpgrade(
db: SQLiteDatabase,
oldVersion: Int,
newVersion: Int
) {
if (oldVersion < 2) {
addRawColumn(db)
}
}
}
companion object {
const val SCANS = "scans"
const val SCANS_ID = "_id"
const val SCANS_DATETIME = "_datetime"
const val SCANS_CONTENT = "content"
const val SCANS_RAW = "raw"
const val SCANS_FORMAT = "format"
private fun createScans(db: SQLiteDatabase) {
db.execSQL("DROP TABLE IF EXISTS $SCANS")
db.execSQL(
"""CREATE TABLE $SCANS (
$SCANS_ID INTEGER PRIMARY KEY AUTOINCREMENT,
$SCANS_DATETIME DATETIME NOT NULL,
$SCANS_CONTENT TEXT NOT NULL,
$SCANS_RAW BLOB,
$SCANS_FORMAT TEXT NOT NULL
)"""
)
}
private fun addRawColumn(db: SQLiteDatabase) {
db.execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_RAW BLOB")
}
}
}
@@ -1,80 +0,0 @@
package de.markusfisch.android.binaryeye.data.csv
import de.markusfisch.android.binaryeye.app.addDelimiter
import de.markusfisch.android.binaryeye.app.asFlowWith
import kotlinx.coroutines.flow.*
fun <T> csvBuilder(config: CSVBuilder<T>.() -> Unit): CSVBuilder<T> {
return CSVBuilder<T>().apply(config)
}
class CSVBuilder<T> {
private val columns = mutableListOf<ColumnBuilder>()
fun column(config: ColumnBuilder.() -> Unit): ColumnBuilder {
return ColumnBuilder().apply(config).also {
columns.add(it)
}
}
inner class ColumnBuilder {
var isBinary = false
var name: String? = null
internal var gettingBy: ((T) -> ByteArray)? = null
fun gettingBy(getter: (T) -> ByteArray) {
gettingBy = getter
}
fun gettingByString(getter: (T) -> String): Unit = gettingBy {
getter(it).toByteArray()
}
}
fun buildWith(inputs: Flow<T>, delimiter: String): Flow<ByteArray> = flow {
val columnsFlow = columns.asFlow()
val header = columnsFlow.buildHeader(delimiter)
val contentLines = inputs.buildContent(columnsFlow, delimiter)
emitAll(header)
emit("\n".toByteArray())
emitAll(contentLines)
}
private fun Flow<ColumnBuilder>.buildHeader(delimiter: String) = map {
it.name?.escaped(delimiter)?.toByteArray()
?: throw IllegalStateException("You need to provide a name for csv column")
}.addDelimiter(delimiter)
private fun Flow<T>.buildContent(
columnsFlow: Flow<ColumnBuilder>,
delimiter: String
): Flow<ByteArray> = map { input ->
input.getLine(columnsFlow, delimiter)
}.addDelimiter("\n".toByteArray()) { other: Flow<ByteArray> ->
// this: ByteArray
this asFlowWith other
}.flattenConcat()
private fun T.getLine(columnsFlow: Flow<ColumnBuilder>, delimiter: String): Flow<ByteArray> =
columnsFlow.map { column ->
column.gettingBy?.run {
var result = invoke(this@getLine)
if (!column.isBinary) {
result = String(result).escaped(delimiter).toByteArray()
}
return@run result
} ?: throw IllegalStateException("You need to provide a getter for the value")
}.addDelimiter(delimiter)
private fun Any.escaped(delimiter: String): String {
val any = this.toString()
return when {
any.run { contains('"') || contains('\n') || contains(delimiter) } ->
"\"${any.replace("\"", "\"\"").replace(delimiter, "\"$delimiter")}\""
else -> any
}
}
}
@@ -0,0 +1,94 @@
package de.markusfisch.android.binaryeye.database
import android.content.Context
import android.database.Cursor
import de.markusfisch.android.binaryeye.content.toHexString
import de.markusfisch.android.binaryeye.io.writeExternalFile
import java.io.ByteArrayOutputStream
import java.io.OutputStream
fun Context.exportCsv(
name: String,
cursor: Cursor,
delimiter: String
) = writeExternalFile(name, "text/csv") { outputStream ->
exportCsv(outputStream, cursor, delimiter)
}
fun Cursor.exportCsv(delimiter: String): String {
val outputStream = ByteArrayOutputStream()
exportCsv(outputStream, this, delimiter)
return outputStream.toString()
}
private fun exportCsv(
outputStream: OutputStream,
cursor: Cursor,
delimiter: String
) {
if (!cursor.moveToFirst()) {
return
}
val columns = arrayOf(
Database.SCANS_DATETIME,
Database.SCANS_FORMAT,
Database.SCANS_CONTENT,
Database.SCANS_ERROR_CORRECTION_LEVEL,
Database.SCANS_VERSION,
Database.SCANS_SEQUENCE_SIZE,
Database.SCANS_SEQUENCE_INDEX,
Database.SCANS_SEQUENCE_ID,
Database.SCANS_GTIN_COUNTRY,
Database.SCANS_GTIN_ADD_ON,
Database.SCANS_GTIN_PRICE,
Database.SCANS_GTIN_ISSUE_NUMBER
)
val indices = columns.map {
cursor.getColumnIndex(it)
}
val contentIndex = cursor.getColumnIndex(Database.SCANS_CONTENT)
val rawIndex = cursor.getColumnIndex(Database.SCANS_RAW)
outputStream.write(
columns.joinToString(
delimiter,
postfix = "\n"
).toByteArray()
)
do {
var deviation: Pair<Int, String>? = null
if (cursor.getString(contentIndex)?.isEmpty() == true) {
deviation = Pair(
contentIndex,
cursor.getBlob(rawIndex).toHexString()
)
}
outputStream.write(
cursor.toCsvRecord(indices, delimiter, deviation)
)
} while (cursor.moveToNext())
}
private fun Cursor.toCsvRecord(
indices: List<Int>,
delimiter: String,
deviation: Pair<Int, String>?
): ByteArray {
val sb = StringBuilder()
indices.forEach {
val value = if (deviation?.first == it) {
deviation.second
} else {
this.getString(it)
}
sb.append(value?.quoteAndEscape() ?: "")
sb.append(delimiter)
}
sb.append("\n")
return sb.toString().toByteArray()
}
private fun String.quoteAndEscape() = "\"${
this
.replace("\n", " ")
.replace("\"", "\"\"")
}\""
@@ -0,0 +1,32 @@
package de.markusfisch.android.binaryeye.database
import android.database.Cursor
// Overwrite Kotlin's ".use" function for Cursor because Cursor cannot
// be cast to Closeable below API level 16. This should be removed when
// the minSDK is increased to at least JELLY_BEAN (16).
inline fun <R> Cursor.use(block: (Cursor) -> R): R = try {
block.invoke(this)
} finally {
close()
}
fun Cursor.getInt(name: String): Int {
val idx = getColumnIndex(name)
return if (idx < 0) 0 else getInt(idx)
}
fun Cursor.getLong(name: String): Long {
val idx = getColumnIndex(name)
return if (idx < 0) 0L else getLong(idx)
}
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)
}
@@ -0,0 +1,314 @@
package de.markusfisch.android.binaryeye.database
import android.content.ContentValues
import android.content.Context
import android.database.Cursor
import android.database.sqlite.SQLiteDatabase
import android.database.sqlite.SQLiteOpenHelper
import de.markusfisch.android.binaryeye.app.prefs
class Database {
private lateinit var db: SQLiteDatabase
fun open(context: Context) {
db = OpenHelper(context).writableDatabase
}
fun getScans(query: String? = null): Cursor? = db.rawQuery(
"""SELECT
$SCANS_ID,
$SCANS_DATETIME,
$SCANS_NAME,
$SCANS_CONTENT,
$SCANS_FORMAT
FROM $SCANS
${getWhereClause(query)}
ORDER BY $SCANS_DATETIME DESC
""".trimMargin(), getWhereArguments(query)
)
fun getScansDetailed(query: String? = null): Cursor? = db.rawQuery(
"""SELECT
$SCANS_ID,
$SCANS_DATETIME,
$SCANS_NAME,
$SCANS_CONTENT,
$SCANS_RAW,
$SCANS_FORMAT,
$SCANS_ERROR_CORRECTION_LEVEL,
$SCANS_VERSION,
$SCANS_SEQUENCE_SIZE,
$SCANS_SEQUENCE_INDEX,
$SCANS_SEQUENCE_ID,
$SCANS_GTIN_COUNTRY,
$SCANS_GTIN_ADD_ON,
$SCANS_GTIN_PRICE,
$SCANS_GTIN_ISSUE_NUMBER
FROM $SCANS
${getWhereClause(query)}
ORDER BY $SCANS_DATETIME DESC
""".trimMargin(), getWhereArguments(query)
)
private fun getWhereClause(
query: String?,
prefix: String = "WHERE"
) = if (query?.isNotEmpty() == true) {
"""$prefix $SCANS_CONTENT LIKE ?
OR $SCANS_NAME LIKE ?"""
} else {
""
}
private fun getWhereArguments(
query: String?
) = if (query?.isNotEmpty() == true) {
val instr = "%$query%"
arrayOf(instr, instr)
} else {
null
}
fun getScan(id: Long): Scan? = db.rawQuery(
"""SELECT
$SCANS_ID,
$SCANS_DATETIME,
$SCANS_NAME,
$SCANS_CONTENT,
$SCANS_RAW,
$SCANS_FORMAT,
$SCANS_ERROR_CORRECTION_LEVEL,
$SCANS_VERSION,
$SCANS_SEQUENCE_SIZE,
$SCANS_SEQUENCE_INDEX,
$SCANS_SEQUENCE_ID,
$SCANS_GTIN_COUNTRY,
$SCANS_GTIN_ADD_ON,
$SCANS_GTIN_PRICE,
$SCANS_GTIN_ISSUE_NUMBER
FROM $SCANS
WHERE $SCANS_ID = ?
""".trimMargin(), arrayOf("$id")
)?.use {
if (it.moveToFirst()) {
Scan(
it.getString(SCANS_CONTENT),
it.getBlob(SCANS_RAW),
it.getString(SCANS_FORMAT),
it.getString(SCANS_ERROR_CORRECTION_LEVEL),
it.getString(SCANS_VERSION),
it.getInt(SCANS_SEQUENCE_SIZE),
it.getInt(SCANS_SEQUENCE_INDEX),
it.getString(SCANS_SEQUENCE_ID),
it.getString(SCANS_GTIN_COUNTRY),
it.getString(SCANS_GTIN_ADD_ON),
it.getString(SCANS_GTIN_PRICE),
it.getString(SCANS_GTIN_ISSUE_NUMBER),
it.getString(SCANS_DATETIME),
it.getLong(SCANS_ID)
)
} else {
null
}
}
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)
}
put(SCANS_VERSION, scan.version)
put(SCANS_SEQUENCE_SIZE, scan.sequenceSize)
put(SCANS_SEQUENCE_INDEX, scan.sequenceIndex)
put(SCANS_SEQUENCE_ID, scan.sequenceId)
scan.country?.let { put(SCANS_GTIN_COUNTRY, it) }
scan.addOn?.let { put(SCANS_GTIN_ADD_ON, it) }
scan.price?.let { put(SCANS_GTIN_PRICE, it) }
scan.issueNumber?.let { put(SCANS_GTIN_ISSUE_NUMBER, 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
}
}
}
)
private fun getIdOfLastScan(
content: String,
raw: ByteArray?,
format: String
): Long = db.rawQuery(
"""SELECT
$SCANS_ID,
$SCANS_CONTENT,
$SCANS_RAW,
$SCANS_FORMAT
FROM $SCANS
ORDER BY $SCANS_ID DESC
LIMIT 1
""".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"))
}
fun removeScans(query: String? = null) {
db.delete(SCANS, getWhereClause(query, ""), getWhereArguments(query))
}
fun renameScan(id: Long, name: String) {
val cv = ContentValues()
cv.put(SCANS_NAME, name)
db.update(SCANS, cv, "$SCANS_ID = ?", arrayOf("$id"))
}
private class OpenHelper(context: Context) :
SQLiteOpenHelper(context, FILE_NAME, null, 6) {
override fun onCreate(db: SQLiteDatabase) {
db.createScans()
}
override fun onUpgrade(
db: SQLiteDatabase,
oldVersion: Int,
newVersion: Int
) {
if (oldVersion < 2) {
db.addRawColumn()
}
if (oldVersion < 3) {
db.addMetaDataColumns()
}
if (oldVersion < 4) {
db.addNameColumn()
}
if (oldVersion < 5) {
db.migrateToZxingCpp()
}
if (oldVersion < 6) {
db.migrateToVersionString()
}
}
}
companion object {
const val FILE_NAME = "history.db"
const val SCANS = "scans"
const val SCANS_ID = "_id"
const val SCANS_DATETIME = "_datetime"
const val SCANS_NAME = "name"
const val SCANS_CONTENT = "content"
const val SCANS_RAW = "raw"
const val SCANS_FORMAT = "format"
const val SCANS_ERROR_CORRECTION_LEVEL = "error_correction_level"
const val SCANS_VERSION_NUMBER = "version_number"
const val SCANS_VERSION = "version"
const val SCANS_ISSUE_NUMBER = "issue_number"
const val SCANS_ORIENTATION = "orientation"
const val SCANS_OTHER_META_DATA = "other_meta_data"
const val SCANS_PDF417_EXTRA_METADATA = "pdf417_extra_metadata"
const val SCANS_POSSIBLE_COUNTRY = "possible_country"
const val SCANS_SUGGESTED_PRICE = "suggested_price"
const val SCANS_UPC_EAN_EXTENSION = "upc_ean_extension"
const val SCANS_SEQUENCE_SIZE = "sequence_size"
const val SCANS_SEQUENCE_INDEX = "sequence_index"
const val SCANS_SEQUENCE_ID = "sequence_id"
const val SCANS_GTIN_COUNTRY = "gtin_country"
const val SCANS_GTIN_ADD_ON = "gtin_add_on"
const val SCANS_GTIN_PRICE = "gtin_price"
const val SCANS_GTIN_ISSUE_NUMBER = "gtin_issue_number"
private fun SQLiteDatabase.createScans() {
execSQL("DROP TABLE IF EXISTS $SCANS".trimMargin())
execSQL(
"""CREATE TABLE $SCANS (
$SCANS_ID INTEGER PRIMARY KEY AUTOINCREMENT,
$SCANS_DATETIME DATETIME NOT NULL,
$SCANS_NAME TEXT,
$SCANS_CONTENT TEXT NOT NULL,
$SCANS_RAW BLOB,
$SCANS_FORMAT TEXT NOT NULL,
$SCANS_ERROR_CORRECTION_LEVEL TEXT,
$SCANS_VERSION TEXT,
$SCANS_SEQUENCE_SIZE INTEGER,
$SCANS_SEQUENCE_INDEX INTEGER,
$SCANS_SEQUENCE_ID TEXT,
$SCANS_GTIN_COUNTRY TEXT,
$SCANS_GTIN_ADD_ON TEXT,
$SCANS_GTIN_PRICE TEXT,
$SCANS_GTIN_ISSUE_NUMBER TEXT
)""".trimMargin()
)
}
private fun SQLiteDatabase.addRawColumn() {
execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_RAW BLOB".trimMargin())
}
private fun SQLiteDatabase.addMetaDataColumns() {
execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_ERROR_CORRECTION_LEVEL TEXT".trimMargin())
execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_ISSUE_NUMBER INT".trimMargin())
execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_ORIENTATION INT".trimMargin())
execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_OTHER_META_DATA TEXT".trimMargin())
execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_PDF417_EXTRA_METADATA TEXT".trimMargin())
execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_POSSIBLE_COUNTRY TEXT".trimMargin())
execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_SUGGESTED_PRICE TEXT".trimMargin())
execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_UPC_EAN_EXTENSION TEXT".trimMargin())
}
private fun SQLiteDatabase.addNameColumn() {
execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_NAME TEXT".trimMargin())
}
private fun SQLiteDatabase.migrateToZxingCpp() {
execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_VERSION_NUMBER INTEGER".trimMargin())
execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_SEQUENCE_SIZE INTEGER".trimMargin())
execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_SEQUENCE_INDEX INTEGER".trimMargin())
execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_SEQUENCE_ID TEXT".trimMargin())
execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_GTIN_COUNTRY TEXT".trimMargin())
execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_GTIN_ADD_ON TEXT".trimMargin())
execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_GTIN_PRICE TEXT".trimMargin())
execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_GTIN_ISSUE_NUMBER TEXT".trimMargin())
execSQL("UPDATE $SCANS SET $SCANS_SEQUENCE_SIZE = -1")
execSQL("UPDATE $SCANS SET $SCANS_SEQUENCE_INDEX = -1")
execSQL("UPDATE $SCANS SET $SCANS_GTIN_COUNTRY = $SCANS_POSSIBLE_COUNTRY")
execSQL("UPDATE $SCANS SET $SCANS_GTIN_ADD_ON = $SCANS_UPC_EAN_EXTENSION")
execSQL("UPDATE $SCANS SET $SCANS_GTIN_PRICE = $SCANS_SUGGESTED_PRICE")
execSQL("UPDATE $SCANS SET $SCANS_GTIN_ISSUE_NUMBER = $SCANS_ISSUE_NUMBER")
}
private fun SQLiteDatabase.migrateToVersionString() {
execSQL("ALTER TABLE $SCANS ADD $SCANS_VERSION TEXT")
execSQL("UPDATE $SCANS SET $SCANS_VERSION = $SCANS_VERSION_NUMBER")
}
}
}
@@ -0,0 +1,23 @@
package de.markusfisch.android.binaryeye.database
import android.app.Activity
import android.os.Environment
import de.markusfisch.android.binaryeye.io.writeExternalFile
import java.io.File
import java.io.FileInputStream
fun Activity.exportDatabase(fileName: String): Boolean {
val dbFile = File(
Environment.getDataDirectory(),
"//data//${packageName}//databases//${Database.FILE_NAME}"
)
if (!dbFile.exists()) {
return false
}
return writeExternalFile(
fileName,
"application/vnd.sqlite3"
) {
FileInputStream(dbFile).copyTo(it)
}
}
@@ -0,0 +1,75 @@
package de.markusfisch.android.binaryeye.database
import android.content.Context
import android.database.Cursor
import de.markusfisch.android.binaryeye.content.toHexString
import de.markusfisch.android.binaryeye.io.writeExternalFile
import org.json.JSONArray
import org.json.JSONException
import org.json.JSONObject
fun Context.exportJson(
name: String,
cursor: Cursor
) = writeExternalFile(name, "application/json") { outputStream ->
cursor.exportJson()?.let {
outputStream.write(it.toByteArray())
}
}
fun Cursor.exportJson(): String? {
if (!moveToFirst()) {
return null
}
val columns = arrayOf(
Database.SCANS_DATETIME,
Database.SCANS_FORMAT,
Database.SCANS_CONTENT,
Database.SCANS_ERROR_CORRECTION_LEVEL,
Database.SCANS_VERSION,
Database.SCANS_SEQUENCE_SIZE,
Database.SCANS_SEQUENCE_INDEX,
Database.SCANS_SEQUENCE_ID,
Database.SCANS_GTIN_COUNTRY,
Database.SCANS_GTIN_ADD_ON,
Database.SCANS_GTIN_PRICE,
Database.SCANS_GTIN_ISSUE_NUMBER
)
val indices = columns.map {
Pair(getColumnIndex(it), it)
}
val contentIndex = getColumnIndex(Database.SCANS_CONTENT)
val rawIndex = getColumnIndex(Database.SCANS_RAW)
val root = JSONArray()
do {
var deviation: Pair<Int, String>? = null
if (getString(contentIndex)?.isEmpty() == true) {
deviation = Pair(
contentIndex,
getBlob(rawIndex).toHexString()
)
}
root.put(toJsonObject(indices, deviation))
} while (moveToNext())
return root.toString()
}
private fun Cursor.toJsonObject(
indices: List<Pair<Int, String>>,
deviation: Pair<Int, String>?
): JSONObject {
val obj = JSONObject()
return try {
indices.forEach {
val value = if (deviation?.first == it.first) {
deviation.second
} else {
this.getString(it.first)
}
obj.put(it.second, value ?: "")
}
obj
} catch (e: JSONException) {
obj
}
}
@@ -0,0 +1,221 @@
package de.markusfisch.android.binaryeye.database
import android.graphics.Bitmap
import android.os.Build
import android.os.Parcel
import android.os.Parcelable
import android.text.format.DateFormat
import de.markusfisch.android.zxingcpp.ZxingCpp
import de.markusfisch.android.zxingcpp.ZxingCpp.ContentType
import de.markusfisch.android.zxingcpp.ZxingCpp.Format
import de.markusfisch.android.zxingcpp.ZxingCpp.Result
data class Scan(
val content: String,
val raw: ByteArray?,
val format: String,
val errorCorrectionLevel: String? = null,
val version: String? = null,
val sequenceSize: Int = -1,
val sequenceIndex: Int = -1,
val sequenceId: String = "",
val country: String? = null,
val addOn: String? = null,
val price: String? = null,
val issueNumber: String? = null,
val dateTime: String = getDateTime(),
var id: Long = 0L
) : Parcelable {
// Needs to be overwritten manually, as ByteArray is an array and
// this isn't handled well by Kotlin.
override fun equals(other: Any?): Boolean {
if (this === other) {
return true
}
if (javaClass != other?.javaClass) {
return false
}
other as Scan
return id == other.id &&
dateTime == other.dateTime &&
content == other.content &&
((raw == null && other.raw == null) ||
(raw != null && other.raw != null && raw.contentEquals(other.raw))) &&
format == other.format &&
errorCorrectionLevel == other.errorCorrectionLevel &&
version == other.version &&
sequenceSize == other.sequenceSize &&
sequenceIndex == other.sequenceIndex &&
sequenceId == other.sequenceId &&
country == other.country &&
addOn == other.addOn &&
price == other.price &&
issueNumber == other.issueNumber
}
// Needs to be overwritten manually, as ByteArray is an array and
// this isn't handled well by Kotlin.
override fun hashCode(): Int {
var result = id.hashCode()
result = 31 * result + dateTime.hashCode()
result = 31 * result + content.hashCode()
result = 31 * result + (raw?.contentHashCode() ?: 0)
result = 31 * result + format.hashCode()
result = 31 * result + (errorCorrectionLevel?.hashCode() ?: 0)
result = 31 * result + version.hashCode()
result = 31 * result + sequenceSize
result = 31 * result + sequenceIndex
result = 31 * result + sequenceId.hashCode()
result = 31 * result + (country?.hashCode() ?: 0)
result = 31 * result + (addOn?.hashCode() ?: 0)
result = 31 * result + (price?.hashCode() ?: 0)
result = 31 * result + (issueNumber?.hashCode() ?: 0)
return result
}
private constructor(parcel: Parcel) : this(
content = parcel.readString() ?: "",
raw = parcel.readSizedByteArray(),
format = parcel.readString() ?: "",
errorCorrectionLevel = parcel.readString(),
version = parcel.readString(),
sequenceSize = parcel.readInt(),
sequenceIndex = parcel.readInt(),
sequenceId = parcel.readString() ?: "",
country = parcel.readString(),
addOn = parcel.readString(),
price = parcel.readString(),
issueNumber = parcel.readString(),
dateTime = parcel.readString() ?: "",
id = parcel.readLong()
)
override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.apply {
writeString(content)
writeSizedByteArray(raw)
writeString(format)
writeString(errorCorrectionLevel)
writeString(version)
writeInt(sequenceSize)
writeInt(sequenceIndex)
writeString(sequenceId)
writeString(country)
writeString(addOn)
writeString(price)
writeString(issueNumber)
writeString(dateTime)
writeLong(id)
}
}
override fun describeContents() = 0
companion object {
@JvmField
val CREATOR = object : Parcelable.Creator<Scan> {
override fun createFromParcel(parcel: Parcel) = Scan(parcel)
override fun newArray(size: Int) = arrayOfNulls<Scan>(size)
}
}
}
fun Result.toScan(): Scan {
val content: String
val raw: ByteArray?
when (contentType) {
ContentType.GS1,
ContentType.ISO15434,
ContentType.TEXT -> {
content = text
raw = null
}
else -> {
content = ""
raw = rawBytes
}
}
return Scan(
content,
raw,
format,
ecLevel,
version,
sequenceSize,
sequenceIndex,
sequenceId,
gtin?.country,
gtin?.addOn,
gtin?.price,
gtin?.issueNumber
)
}
data class Recreation(
val format: Format,
val ecLevel: Int,
val size: Int,
val margin: Int
) {
fun encode(content: String): Bitmap? = try {
ZxingCpp.encodeAsBitmap(
content, format, size, size, margin, ecLevel
)
} catch (_: RuntimeException) {
null
}
}
fun Scan.toRecreation(
size: Int = 128,
margin: Int = -1
) = if (content.isEmpty()) {
null
} else {
Recreation(
Format.valueOf(format),
when (errorCorrectionLevel) {
"L" -> 0
"M" -> 1
"Q" -> 2
"H" -> 3
else -> -1
},
size,
margin
)
}
private fun getDateTime(
time: Long = System.currentTimeMillis()
) = DateFormat.format(
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2) {
// Up to API level 17, 'k' was interpreted as 'H', which wasn't
// implemented until API level 18.
"yyyy-MM-dd kk:mm:ss"
} else {
"yyyy-MM-dd HH:mm:ss"
},
time
).toString()
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
}
}
@@ -2,29 +2,46 @@ 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.os.Environment
import android.support.v4.app.Fragment
import android.view.*
import android.widget.EditText
import android.widget.ImageView
import android.widget.Toast
import com.google.zxing.BarcodeFormat
import de.markusfisch.android.binaryeye.R
import de.markusfisch.android.binaryeye.app.addSuffixIfNotGiven
import de.markusfisch.android.binaryeye.app.hasWritePermission
import de.markusfisch.android.binaryeye.app.shareFile
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.content.copyToClipboard
import de.markusfisch.android.binaryeye.content.shareFile
import de.markusfisch.android.binaryeye.content.shareText
import de.markusfisch.android.binaryeye.graphics.COLOR_BLACK
import de.markusfisch.android.binaryeye.graphics.COLOR_WHITE
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.zxingcpp.ZxingCpp
import de.markusfisch.android.zxingcpp.ZxingCpp.Format
import kotlinx.coroutines.*
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import java.io.OutputStream
import java.util.*
import kotlin.math.min
class BarcodeFragment : Fragment() {
private var barcode: Bitmap? = null
private enum class FileType {
PNG, SVG, TXT
}
private val parentJob = Job()
private val scope = CoroutineScope(Dispatchers.IO + parentJob)
private lateinit var barcode: Barcode
override fun onCreate(state: Bundle?) {
super.onCreate(state)
@@ -36,7 +53,8 @@ class BarcodeFragment : Fragment() {
container: ViewGroup?,
state: Bundle?
): View? {
activity?.setTitle(R.string.view_barcode)
val ac = activity ?: return null
ac.setTitle(R.string.view_barcode)
val view = inflater.inflate(
R.layout.fragment_barcode,
@@ -44,64 +62,146 @@ class BarcodeFragment : Fragment() {
false
)
val args = arguments ?: return view
val content = args.getString(CONTENT) ?: return view
val format = args.getSerializable(FORMAT) as BarcodeFormat? ?: return view
val size = args.getInt(SIZE)
val bitmap: Bitmap
try {
barcode = Zxing.encodeAsBitmap(content, format, size, size)
barcode = arguments?.toBarcode() ?: throw IllegalArgumentException(
"Illegal arguments"
)
// Catch exceptions from encoding.
bitmap = barcode.bitmap()
} catch (e: Exception) {
var message = e.message
if (message == null || message.isEmpty()) {
message = getString(R.string.error_encoding_barcode)
}
Toast.makeText(activity, message, Toast.LENGTH_SHORT).show()
message?.let {
ac.toast(message)
}
fragmentManager.popBackStack()
return null
}
view.findViewById<ImageView>(R.id.barcode).setImageBitmap(barcode)
val imageView = view.findViewById<ConfinedScalingImageView>(
R.id.barcode
)
imageView.setImageBitmap(bitmap)
imageView.post {
// Make sure to invoke this after ScalingImageView.onLayout().
imageView.minWidth = min(
imageView.minWidth / 2f,
barcode.size.toFloat()
)
}
view.findViewById<View>(R.id.share).setOnClickListener {
val bitmap = barcode
bitmap?.let {
share(bitmap)
it.context?.apply {
pickFileType(R.string.share_as) { fileType ->
shareAs(fileType)
}
}
}
(view.findViewById(R.id.inset_layout) as View).setPaddingFromWindowInsets()
imageView.doOnApplyWindowInsets { v, insets ->
(v as ConfinedScalingImageView).insets.set(insets)
}
return view
}
private fun Bundle.toBarcode() = Barcode(
getString(CONTENT) ?: throw IllegalArgumentException(
"content cannot be null"
),
Format.valueOf(
getString(FORMAT) ?: throw IllegalArgumentException(
"format cannot be null"
)
),
getInt(SIZE),
getInt(EC_LEVEL),
Colors.values()[getInt(COLORS)]
)
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.save -> {
askForFileNameAndSave()
R.id.copy_to_clipboard -> {
context.apply {
copyToClipboard(barcode.text())
toast(R.string.copied_to_clipboard)
}
true
}
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() {
private fun askForFileNameAndSave(fileType: FileType) {
val ac = activity ?: return
// 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)
val editText = view.findViewById<EditText>(R.id.file_name)
editText.setText(
encodeFileName("${barcode.format}_${barcode.content}")
)
AlertDialog.Builder(ac)
.setView(view)
.setPositiveButton(android.R.string.ok) { _, _ ->
val bitmap = barcode
bitmap?.let {
saveAsFile(
bitmap,
addSuffixIfNotGiven(
editText.text.toString(),
".png"
)
)
val fileName = editText.text.toString()
when (fileType) {
FileType.PNG -> saveAs(
addSuffixIfNotGiven(fileName, ".png"),
MIME_PNG
) {
barcode.bitmap().saveAsPng(it)
}
FileType.SVG -> saveAs(
addSuffixIfNotGiven(fileName, ".svg"),
MIME_SVG
) { outputStream ->
outputStream.write(barcode.svg().toByteArray())
}
FileType.TXT -> saveAs(
addSuffixIfNotGiven(fileName, ".txt"),
MIME_TXT
) { outputStream ->
outputStream.write(barcode.text().toByteArray())
}
}
}
.setNegativeButton(android.R.string.cancel) { _, _ ->
@@ -109,54 +209,47 @@ class BarcodeFragment : Fragment() {
.show()
}
private fun saveAsFile(bitmap: Bitmap, path: String) {
private fun saveAs(
fileName: String,
mimeType: String,
write: (outputStream: OutputStream) -> Unit
) {
val ac = activity ?: return
if (!hasWritePermission(ac)) {
return
}
GlobalScope.launch {
val success = saveBitmap(
bitmap,
File(
Environment.getExternalStoragePublicDirectory(
Environment.DIRECTORY_DOWNLOADS
),
path
)
)
GlobalScope.launch(Main) {
Toast.makeText(
ac,
if (success) {
R.string.saved_in_downloads
} else {
R.string.error_saving_binary_data
},
Toast.LENGTH_LONG
).show()
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 -> share(barcode.bitmap())
FileType.SVG -> shareText(barcode.svg(), MIME_SVG)
FileType.TXT -> shareText(barcode.text())
}
}
private fun Context.share(bitmap: Bitmap) {
scope.launch(Dispatchers.IO) {
val file = File(
context.externalCacheDir,
externalCacheDir,
"shared_barcode.png"
)
val success = saveBitmap(bitmap, file)
GlobalScope.launch(Main) {
val success = try {
FileOutputStream(file).use {
bitmap.saveAsPng(it)
}
true
} catch (e: IOException) {
false
}
withContext(Dispatchers.Main) {
if (success) {
shareFile(context, file, "image/png")
shareFile(file, "image/png")
} else {
val ac = activity
ac?.let {
Toast.makeText(
ac,
R.string.error_saving_binary_data,
Toast.LENGTH_LONG
).show()
}
toast(R.string.error_saving_file)
}
}
}
@@ -166,16 +259,25 @@ class BarcodeFragment : Fragment() {
private const val CONTENT = "content"
private const val FORMAT = "format"
private const val SIZE = "size"
private const val EC_LEVEL = "ec_level"
private const val COLORS = "colors"
private const val MIME_PNG = "image/png"
private const val MIME_SVG = "image/svg+xmg"
private const val MIME_TXT = "text/plain"
fun newInstance(
content: String,
format: BarcodeFormat,
size: Int
format: Format,
size: Int,
ecLevel: Int = -1,
colors: Int = 0
): Fragment {
val args = Bundle()
args.putString(CONTENT, content)
args.putSerializable(FORMAT, format)
args.putString(FORMAT, format.name)
args.putInt(SIZE, size)
args.putInt(EC_LEVEL, ecLevel)
args.putInt(COLORS, colors)
val fragment = BarcodeFragment()
fragment.arguments = args
return fragment
@@ -183,13 +285,68 @@ class BarcodeFragment : Fragment() {
}
}
private fun saveBitmap(bitmap: Bitmap, file: File): Boolean {
return try {
val fos = FileOutputStream(file)
bitmap.compress(Bitmap.CompressFormat.PNG, 90, fos)
fos.close()
true
} catch (e: IOException) {
false
private data class Barcode(
val content: String,
val format: Format,
val size: Int,
val ecLevel: Int,
val colors: Colors
) {
private var _bitmap: Bitmap? = null
fun bitmap(): Bitmap {
val b = _bitmap ?: ZxingCpp.encodeAsBitmap(
content, format, size, size, -1, ecLevel,
setColor = colors.foregroundColor(),
unsetColor = colors.backgroundColor()
)
_bitmap = b
return b
}
private var _svg: String? = null
fun svg(): String {
val s = _svg ?: ZxingCpp.encodeAsSvg(
content, format, -1, ecLevel
)
_svg = s
return s
}
private var _text: String? = null
fun text(): String {
val t = _text ?: ZxingCpp.encodeAsText(
content, format, -1, ecLevel,
inverted = colors == Colors.BLACK_ON_WHITE
)
_text = t
return t
}
}
private enum class Colors {
BLACK_ON_WHITE,
WHITE_ON_BLACK,
BLACK_ON_TRANSPARENT,
WHITE_ON_TRANSPARENT;
fun foregroundColor(): Int = when (this) {
BLACK_ON_WHITE,
BLACK_ON_TRANSPARENT -> COLOR_BLACK
WHITE_ON_BLACK,
WHITE_ON_TRANSPARENT -> COLOR_WHITE
}
fun backgroundColor(): Int = when (this) {
BLACK_ON_WHITE -> COLOR_WHITE
WHITE_ON_BLACK -> COLOR_BLACK
BLACK_ON_TRANSPARENT,
WHITE_ON_TRANSPARENT -> 0
}
}
private fun Bitmap.saveAsPng(outputStream: OutputStream, quality: Int = 90) =
compress(Bitmap.CompressFormat.PNG, quality, outputStream)
private val fileNameCharacters = "[^A-Za-z0-9]".toRegex()
private fun encodeFileName(name: String): String =
fileNameCharacters.replace(name, "_").take(16).trim('_').lowercase(Locale.getDefault())
@@ -1,41 +1,61 @@
package de.markusfisch.android.binaryeye.fragment
import android.content.Context
import android.os.Build
import android.os.Bundle
import android.support.design.widget.FloatingActionButton
import android.support.v4.app.Fragment
import android.text.ClipboardManager
import android.text.Editable
import android.text.Html
import android.text.TextWatcher
import android.text.method.LinkMovementMethod
import android.view.*
import android.widget.EditText
import android.widget.TextView
import android.widget.Toast
import com.google.zxing.BarcodeFormat
import android.widget.*
import de.markusfisch.android.binaryeye.R
import de.markusfisch.android.binaryeye.actions.ActionRegistry
import de.markusfisch.android.binaryeye.actions.IAction
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 kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.launch
import de.markusfisch.android.binaryeye.content.copyToClipboard
import de.markusfisch.android.binaryeye.content.shareText
import de.markusfisch.android.binaryeye.content.toHexString
import de.markusfisch.android.binaryeye.database.Recreation
import de.markusfisch.android.binaryeye.database.Scan
import de.markusfisch.android.binaryeye.database.toRecreation
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.*
import kotlin.math.roundToInt
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: BarcodeFormat
private lateinit var stampView: TextView
private lateinit var recreationView: ImageView
private lateinit var fab: FloatingActionButton
private var action = ActionRegistry.DEFAULT_ACTION
private var isBinary = false
private val content: String
get() = contentView.text.toString()
private lateinit var format: String
private val parentJob = Job()
private val scope: CoroutineScope = CoroutineScope(Dispatchers.Main + parentJob)
private val dp: Float
get() = resources.displayMetrics.density
private val content: String
get() = contentView.text.toString()
private var closeAutomatically = false
private var action = ActionRegistry.DEFAULT_ACTION
private var isBinary = false
private var originalBytes: ByteArray = ByteArray(0)
private var id = 0L
private var recreation: Recreation? = null
override fun onCreate(state: Bundle?) {
super.onCreate(state)
@@ -55,19 +75,70 @@ class DecodeFragment : Fragment() {
false
)
val inputContent = arguments?.getString(CONTENT) ?: ""
isBinary = hasNonPrintableCharacters(inputContent) or inputContent.isEmpty()
val raw = arguments?.getByteArray(RAW) ?: inputContent.toByteArray()
format = arguments?.getSerializable(FORMAT) as BarcodeFormat? ?: BarcodeFormat.QR_CODE
closeAutomatically = prefs.closeAutomatically &&
activity?.intent?.hasExtra(MainActivity.DECODED) == true
val scan = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
arguments?.getParcelable(SCAN, Scan::class.java)
} else {
@Suppress("DEPRECATION")
arguments?.getParcelable(SCAN) as Scan?
} ?: throw IllegalArgumentException("DecodeFragment needs a Scan")
val originalContent = scan.content
isBinary = scan.raw != null
originalBytes = scan.raw ?: originalContent.toByteArray()
format = scan.format
id = scan.id
contentView = view.findViewById(R.id.content)
formatView = view.findViewById(R.id.format)
dataView = view.findViewById(R.id.data)
metaView = view.findViewById(R.id.meta)
hexView = view.findViewById(R.id.hex)
stampView = view.findViewById(R.id.stamp)
recreationView = view.findViewById(R.id.recreation)
fab = view.findViewById(R.id.open)
if (!isBinary) {
contentView.setText(inputContent)
initContentAndFab(originalContent)
if (prefs.showMetaData) {
metaView.fillMetaView(scan)
} else {
metaView.visibility = View.GONE
}
if (prefs.showRecreation) {
recreation = scan.toRecreation(
(200f * dp).roundToInt()
)
}
updateViewsAndFab(originalContent, originalBytes)
(view.findViewById(R.id.inset_layout) as View).setPaddingFromWindowInsets()
(view.findViewById(R.id.scroll_view) as View).setPaddingFromWindowInsets()
return view
}
override fun onDestroy() {
super.onDestroy()
parentJob.cancel()
}
private fun initContentAndFab(originalContent: String) {
if (isBinary) {
contentView.setText(String(originalBytes).foldNonAlNum())
contentView.isEnabled = false
fab.setImageResource(R.drawable.ic_action_save)
fab.setOnClickListener {
askForFileNameAndSave(originalBytes)
}
} else {
contentView.setText(originalContent)
contentView.addTextChangedListener(object : TextWatcher {
override fun afterTextChanged(s: Editable?) {
updateViewsAndAction(content.toByteArray())
updateViewsAndFab(s?.toString() ?: "")
}
override fun beforeTextChanged(
@@ -75,67 +146,155 @@ class DecodeFragment : Fragment() {
start: Int,
count: Int,
after: Int
) {
}
) = Unit
override fun onTextChanged(
s: CharSequence?,
start: Int,
before: Int,
count: Int
) {
}
) = Unit
})
fab.setOnClickListener {
executeAction(content.toByteArray())
executeAction(content)
}
if (prefs.openImmediately) {
executeAction(content.toByteArray())
}
} else {
contentView.setText(R.string.binary_data)
contentView.isEnabled = false
fab.setImageResource(R.drawable.ic_action_save)
fab.setOnClickListener {
askForFileNameAndSave(raw)
executeAction(content)
}
}
formatView = view.findViewById(R.id.format)
hexView = view.findViewById(R.id.hex)
updateViewsAndAction(raw)
return view
}
private fun updateViewsAndAction(bytes: ByteArray) {
private fun updateViewsAndFab(text: String, bytes: ByteArray? = null) {
val b = bytes ?: text.toByteArray()
resolveActionAndUpdateFab(b)
updateViews(text, b)
}
private fun resolveActionAndUpdateFab(bytes: ByteArray) {
val prevAction = action
if (!prevAction.canExecuteOn(bytes)) {
action = ActionRegistry.getAction(bytes)
}
if (prevAction !== action) {
updateFab(action)
}
}
private fun updateFab(action: IAction) {
fab.setImageResource(action.iconResId)
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
fab.setOnLongClickListener { v ->
v.context.toast(action.titleResId)
true
}
} else {
fab.tooltipText = getString(action.titleResId)
}
}
private fun updateViews(text: String, bytes: ByteArray) {
formatView.text = resources.getQuantityString(
R.plurals.barcode_info,
bytes.size,
format.toString(),
prettifyFormatName(format),
bytes.size
)
hexView.text = hexDump(bytes, 33)
if (prevAction !== action) {
fab.setImageResource(action.iconResId)
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
fab.setOnLongClickListener { v ->
Toast.makeText(
v.context,
action.titleResId,
Toast.LENGTH_SHORT
).show()
true
}
} else {
fab.tooltipText = getString(action.titleResId)
hexView.showIf(prefs.showHexDump) { v ->
v.text = hexDump(bytes)
}
recreationView.showIf(prefs.showRecreation) { v ->
val r = recreation ?: return@showIf
v.setImageBitmap(r.encode(text))
v.setOnClickListener {
fragmentManager?.addFragment(
BarcodeFragment.newInstance(
text,
r.format,
r.size,
r.ecLevel
)
)
}
}
dataView.fillDataView(text)
stampView.setTrackingLink(bytes, format)
}
private fun TableLayout.fillDataView(text: String) {
val items = LinkedHashMap<Int, String?>()
if (action is WifiAction) {
WifiConnector.parseMap(text)?.let { wifiData ->
items.putAll(
linkedMapOf(
R.string.entry_type to getString(R.string.wifi_network),
R.string.wifi_ssid to wifiData["S"],
R.string.wifi_password to wifiData["P"],
R.string.wifi_type to wifiData["T"],
R.string.wifi_hidden to wifiData["H"],
R.string.wifi_eap to wifiData["E"],
R.string.wifi_identity to wifiData["I"],
R.string.wifi_anonymous_identity to wifiData["A"],
R.string.wifi_phase2 to wifiData["PH2"]
)
)
}
}
fill(items)
}
private fun TableLayout.fillMetaView(scan: Scan) {
val items = linkedMapOf(
R.string.error_correction_level to scan.errorCorrectionLevel,
R.string.sequence_size to scan.sequenceSize.positiveToString(),
R.string.sequence_index to scan.sequenceIndex.positiveToString(),
R.string.sequence_id to scan.sequenceId,
R.string.gtin_country to scan.country,
R.string.gtin_add_on to scan.addOn,
R.string.gtin_price to scan.price,
R.string.gtin_issue_number to scan.issueNumber,
)
if (!scan.version.isNullOrBlank()) {
val versionString = if (scan.format == "QR_CODE") {
getString(
R.string.qr_version_and_modules,
scan.version,
(scan.version.toIntOrNull() ?: 0) * 4 + 17
)
} else {
scan.version
}
items.putAll(
linkedMapOf(
R.string.barcode_version_number to versionString
)
)
}
fill(items)
}
private fun TableLayout.fill(
items: LinkedHashMap<Int, String?>
) {
removeAllViews()
visibility = if (items.isEmpty()) View.GONE else {
val ctx = context
val spaceBetween = (16f * dp).roundToInt()
items.forEach { item ->
val text = item.value
if (!text.isNullOrBlank()) {
val tr = TableRow(ctx)
val keyView = TextView(ctx)
keyView.setText(item.key)
val valueView = TextView(ctx)
valueView.setPadding(spaceBetween, 0, 0, 0)
valueView.text = text
tr.addView(keyView)
tr.addView(valueView)
addView(tr)
}
}
View.VISIBLE
}
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
@@ -144,84 +303,134 @@ class DecodeFragment : Fragment() {
menu.findItem(R.id.copy_to_clipboard).isVisible = false
menu.findItem(R.id.create).isVisible = false
}
if (id > 0L) {
menu.findItem(R.id.remove).isVisible = true
}
if (action is WifiAction) {
menu.findItem(R.id.copy_password).isVisible = true
}
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.copy_password -> {
copyPasswordToClipboard()
maybeBackOrFinish()
true
}
R.id.copy_to_clipboard -> {
copyToClipboard(content)
copyToClipboard(textOrHex())
maybeBackOrFinish()
true
}
R.id.share -> {
context?.also { shareText(it, content) }
context?.apply {
shareText(textOrHex())
maybeBackOrFinish()
}
true
}
R.id.create -> {
addFragment(
fragmentManager,
fragmentManager?.addFragment(
EncodeFragment.newInstance(content, format)
)
true
}
R.id.remove -> {
db.removeScan(id)
backOrFinish()
true
}
else -> super.onOptionsItemSelected(item)
}
}
private fun copyToClipboard(text: String) {
activity ?: return
val cm = activity.getSystemService(
Context.CLIPBOARD_SERVICE
) as ClipboardManager
cm.text = text
Toast.makeText(
activity,
R.string.put_into_clipboard,
Toast.LENGTH_SHORT
).show()
private fun textOrHex() = if (isBinary) {
originalBytes.toHexString()
} else {
content
}
private fun executeAction(content: ByteArray) {
if (activity != null && content.isNotEmpty()) scope.launch {
action.execute(activity, content)
private fun copyPasswordToClipboard() {
val ac = action
if (ac is WifiAction) {
ac.password?.let { password ->
activity?.apply {
copyToClipboard(password, isSensitive = true)
toast(R.string.copied_password_to_clipboard)
}
}
}
}
private fun copyToClipboard(text: String, isSensitive: Boolean = false) {
activity?.apply {
copyToClipboard(text, isSensitive)
toast(R.string.copied_to_clipboard)
}
}
private fun maybeBackOrFinish() {
if (closeAutomatically) {
backOrFinish()
}
}
private fun backOrFinish() {
val fm = fragmentManager
if (fm != null && fm.backStackEntryCount > 0) {
fm.popBackStack()
} else {
activity?.finish()
}
}
private fun executeAction(content: String) {
val ac = activity ?: return
if (content.isNotEmpty()) {
if (action is WifiAction &&
Build.VERSION.SDK_INT < Build.VERSION_CODES.Q &&
!ac.hasLocationPermission { executeAction(content) }
) {
return
}
scope.launch {
action.execute(ac, content.toByteArray())
maybeBackOrFinish()
}
}
}
private fun askForFileNameAndSave(raw: ByteArray) {
val ac = activity ?: return
if (!hasWritePermission(ac)) return
// 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 = flowOf(raw).writeToFile(name)
Toast.makeText(
ac,
message,
Toast.LENGTH_SHORT
).show()
withContext(Dispatchers.IO) {
val message = ac.writeExternalFile(
name,
"application/octet-stream"
) {
it.write(raw)
}.toSaveResult()
withContext(Dispatchers.Main) {
ac.toast(message)
}
}
}
}
override fun onDestroyView() {
parentJob.cancel()
super.onDestroyView()
}
companion object {
private const val CONTENT = "content"
private const val FORMAT = "format"
private const val RAW = "raw"
private const val SCAN = "scan"
fun newInstance(
content: String,
format: BarcodeFormat,
raw: ByteArray? = null
): Fragment {
fun newInstance(scan: Scan): Fragment {
val args = Bundle()
args.putString(CONTENT, content)
args.putSerializable(FORMAT, format)
if (raw != null) {
args.putByteArray(RAW, raw)
}
args.putParcelable(SCAN, scan)
val fragment = DecodeFragment()
fragment.arguments = args
return fragment
@@ -229,7 +438,24 @@ class DecodeFragment : Fragment() {
}
}
private fun hexDump(bytes: ByteArray, charsPerLine: Int): String {
private inline fun <T : View> T.showIf(
visible: Boolean,
block: (T) -> Unit
) {
visibility = if (visible) {
block.invoke(this)
View.VISIBLE
} else {
View.GONE
}
}
private val nonAlNum = "[^a-zA-Z0-9]".toRegex()
private val multipleDots = "[…]+".toRegex()
private fun String.foldNonAlNum() = replace(nonAlNum, "")
.replace(multipleDots, "")
private fun hexDump(bytes: ByteArray, charsPerLine: Int = 33): String {
if (charsPerLine < 4 || bytes.isEmpty()) {
return ""
}
@@ -242,10 +468,8 @@ private fun hexDump(bytes: ByteArray, charsPerLine: Int): 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
var atLineEnd = posInLine == 0
@@ -269,3 +493,92 @@ private fun hexDump(bytes: ByteArray, charsPerLine: Int): String {
}
return dump.toString()
}
private fun Int.positiveToString() = if (this > -1) this.toString() else ""
private fun TextView.setTrackingLink(bytes: ByteArray, format: String) {
val trackingLink = generateDpTrackingLink(bytes, format)
if (trackingLink != null) {
text = trackingLink.fromHtml()
isClickable = true
movementMethod = LinkMovementMethod.getInstance()
} else {
visibility = View.GONE
}
}
private fun generateDpTrackingLink(bytes: ByteArray, format: String): String? {
// Check for Deutsche Post Matrixcode stamp.
var isStamp = false
var rawData = bytes
if (format == "DATA_MATRIX" &&
bytes.toString(Charsets.ISO_8859_1).startsWith("DEA5")
) {
if (bytes.size == 47) {
isStamp = true
} else if (bytes.size > 47) {
// Transform back to original data.
rawData = bytes.toString(Charsets.UTF_8).toByteArray(
Charsets.ISO_8859_1
)
if (rawData.size == 47) {
isStamp = true
}
}
}
if (!isStamp) {
return null
}
val hex = StringBuilder()
hex.append(String.format("%02X", rawData[9]))
hex.append(String.format("%02X", rawData[10]))
hex.append(String.format("%02X", rawData[11]))
hex.append(String.format("%02X", rawData[12]))
hex.append(String.format("%02X", rawData[13]))
hex.append(String.format("%X", (rawData[4].toInt() and 0x0f).toByte()))
hex.append(String.format("%02X", rawData[5]))
hex.append(String.format("%02X", rawData[6]))
hex.append(String.format("%02X", rawData[7]))
hex.append(String.format("%02X", rawData[8]))
val hexString = hex.toString()
val trackingNumber = hexString + String.format(
"%X",
crc4(hexString.toByteArray(Charsets.ISO_8859_1))
)
return "<a href=\"https://www.deutschepost.de/de/s/sendungsverfolgung/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 String.fromHtml() = if (
Build.VERSION.SDK_INT >= Build.VERSION_CODES.N
) {
Html.fromHtml(this, Html.FROM_HTML_MODE_LEGACY)
} else {
@Suppress("DEPRECATION")
Html.fromHtml(this)
}
@@ -6,28 +6,38 @@ import android.support.v4.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.InputMethodManager
import android.widget.*
import com.google.zxing.BarcodeFormat
import de.markusfisch.android.binaryeye.R
import de.markusfisch.android.binaryeye.adapter.prettifyFormatName
import de.markusfisch.android.binaryeye.app.addFragment
import de.markusfisch.android.binaryeye.app.prefs
import de.markusfisch.android.binaryeye.view.hideSoftKeyboard
import de.markusfisch.android.binaryeye.view.setPaddingFromWindowInsets
import de.markusfisch.android.binaryeye.widget.toast
import de.markusfisch.android.zxingcpp.ZxingCpp.Format
class EncodeFragment : Fragment() {
private lateinit var formatView: Spinner
private lateinit var ecLabel: TextView
private lateinit var ecSpinner: Spinner
private lateinit var colorsLabel: TextView
private lateinit var colorsSpinner: Spinner
private lateinit var sizeView: TextView
private lateinit var sizeBarView: SeekBar
private lateinit var contentView: EditText
private val writers = arrayListOf(
BarcodeFormat.AZTEC,
BarcodeFormat.CODABAR,
BarcodeFormat.CODE_39,
BarcodeFormat.CODE_128,
BarcodeFormat.DATA_MATRIX,
BarcodeFormat.EAN_8,
BarcodeFormat.EAN_13,
BarcodeFormat.ITF,
BarcodeFormat.PDF_417,
BarcodeFormat.QR_CODE,
BarcodeFormat.UPC_A
Format.AZTEC,
Format.CODABAR,
Format.CODE_39,
Format.CODE_128,
Format.DATA_MATRIX,
Format.EAN_8,
Format.EAN_13,
Format.ITF,
Format.PDF_417,
Format.QR_CODE,
Format.UPC_A
)
override fun onCreateView(
@@ -45,50 +55,101 @@ class EncodeFragment : Fragment() {
)
formatView = view.findViewById(R.id.format)
formatView.adapter = ArrayAdapter<String>(
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 ecVisibility = if (
writers[position] == Format.QR_CODE
) View.VISIBLE else View.GONE
ecLabel.visibility = ecVisibility
ecSpinner.visibility = ecVisibility
val colorsVisibility = if (
writers[position].canBeInverted()
) View.VISIBLE else View.GONE
colorsLabel.visibility = colorsVisibility
colorsSpinner.visibility = colorsVisibility
}
override fun onNothingSelected(parentView: AdapterView<*>?) {}
}
ecLabel = view.findViewById(R.id.error_correction_label)
ecSpinner = view.findViewById(R.id.error_correction_level)
colorsLabel = view.findViewById(R.id.colors_label)
colorsSpinner = view.findViewById(R.id.colors)
sizeView = view.findViewById(R.id.size_display)
sizeBarView = view.findViewById(R.id.size_bar)
initSizeBar()
val contentView = view.findViewById<EditText>(R.id.content)
contentView = view.findViewById(R.id.content)
val args = arguments
args?.let {
contentView.setText(args.getString(CONTENT))
formatView.setSelection(
writers.indexOf(
args.getSerializable(FORMAT) as BarcodeFormat?
)
)
args?.getString(CONTENT)?.let {
contentView.setText(it)
}
view.findViewById<View>(R.id.encode).setOnClickListener { v ->
val format = writers[formatView.selectedItemPosition]
val size = getSize(sizeBarView.progress)
val content = contentView.text.toString()
if (content.isEmpty()) {
Toast.makeText(
v.context,
R.string.error_no_content,
Toast.LENGTH_SHORT
).show()
} else {
hideSoftKeyboard(contentView)
addFragment(
fragmentManager,
BarcodeFragment.newInstance(content, format, size)
)
val barcodeFormat = args?.getString(FORMAT)
if (barcodeFormat != null) {
formatView.setSelection(
writers.indexOf(barcodeFormat.toFormat())
)
} else if (state == null) {
formatView.post {
formatView.setSelection(prefs.indexOfLastSelectedFormat)
}
}
view.findViewById<View>(R.id.encode).setOnClickListener {
it.context.encode()
}
(view.findViewById(R.id.inset_layout) as View).setPaddingFromWindowInsets()
(view.findViewById(R.id.scroll_view) as View).setPaddingFromWindowInsets()
return view
}
override fun onPause() {
super.onPause()
prefs.indexOfLastSelectedFormat = formatView.selectedItemPosition
}
private fun Context.encode() {
val content = contentView.text.toString()
if (content.isEmpty()) {
toast(R.string.error_no_content)
return
}
hideSoftKeyboard(contentView)
val writer = writers[formatView.selectedItemPosition]
fragmentManager?.addFragment(
BarcodeFragment.newInstance(
content,
writer,
getSize(sizeBarView.progress),
ecSpinner.selectedItemPosition,
if (writer.canBeInverted()) {
colorsSpinner.selectedItemPosition
} else 0
)
)
}
private fun initSizeBar() {
updateSize(sizeBarView.progress)
sizeBarView.setOnSeekBarChangeListener(
@@ -114,29 +175,33 @@ class EncodeFragment : Fragment() {
private fun getSize(power: Int) = 128 * (power + 1)
private fun hideSoftKeyboard(view: View) {
val im = activity?.getSystemService(
Context.INPUT_METHOD_SERVICE
) as InputMethodManager?
im?.let {
im.hideSoftInputFromWindow(view.windowToken, 0)
}
}
companion object {
private const val CONTENT = "content"
private const val FORMAT = "format"
fun newInstance(
content: String,
format: BarcodeFormat = BarcodeFormat.AZTEC
content: String? = null,
format: String? = null
): Fragment {
val args = Bundle()
args.putString(CONTENT, content)
args.putSerializable(FORMAT, format)
content?.let { args.putString(CONTENT, content) }
format?.let { args.putString(FORMAT, it) }
val fragment = EncodeFragment()
fragment.arguments = args
return fragment
}
}
}
private fun Format.canBeInverted() = when (this) {
Format.AZTEC,
Format.DATA_MATRIX,
Format.QR_CODE -> true
else -> false
}
private fun String.toFormat(default: Format = Format.QR_CODE): Format = try {
Format.valueOf(this)
} catch (_: IllegalArgumentException) {
default
}
@@ -1,32 +1,119 @@
package de.markusfisch.android.binaryeye.fragment
import android.annotation.SuppressLint
import android.app.AlertDialog
import android.app.SearchManager
import android.content.Context
import android.database.Cursor
import android.os.Build
import android.os.Bundle
import android.support.annotation.WorkerThread
import android.os.Parcelable
import android.support.v4.app.ActivityCompat
import android.support.v4.app.Fragment
import android.support.v4.content.ContextCompat
import android.support.v7.app.AppCompatActivity
import android.support.v7.view.ActionMode
import android.support.v7.widget.SearchView
import android.support.v7.widget.SwitchCompat
import android.view.*
import android.widget.EditText
import android.widget.ListView
import android.widget.Toast
import com.google.zxing.BarcodeFormat
import de.markusfisch.android.binaryeye.R
import de.markusfisch.android.binaryeye.adapter.ScansAdapter
import de.markusfisch.android.binaryeye.app.*
import de.markusfisch.android.binaryeye.data.csv.csvBuilder
import de.markusfisch.android.binaryeye.repository.DatabaseRepository
import de.markusfisch.android.binaryeye.content.copyToClipboard
import de.markusfisch.android.binaryeye.content.shareText
import de.markusfisch.android.binaryeye.database.*
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.*
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.mapNotNull
class HistoryFragment : Fragment() {
private lateinit var useHistorySwitch: SwitchCompat
private lateinit var listView: ListView
private lateinit var fab: View
private lateinit var progressView: View
private val parentJob = Job()
private val scope = CoroutineScope(Dispatchers.IO + parentJob)
private val actionModeCallback = object : ActionMode.Callback {
override fun onCreateActionMode(
mode: ActionMode,
menu: Menu
): Boolean {
mode.menuInflater.inflate(
R.menu.fragment_history_edit,
menu
)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
lockStatusBarColor()
val ac = activity ?: return false
ac.window.statusBarColor = ContextCompat.getColor(
ac,
R.color.accent_dark
)
}
return true
}
override fun onPrepareActionMode(
mode: ActionMode,
menu: Menu
): Boolean {
return false
}
override fun onActionItemClicked(
mode: ActionMode,
item: MenuItem
): Boolean {
val ac = activity ?: return false
return when (item.itemId) {
R.id.copy_scan -> {
scansAdapter?.getSelectedContent("\n")?.let {
ac.copyToClipboard(it)
ac.toast(R.string.copied_to_clipboard)
}
closeActionMode()
true
}
R.id.edit_scan -> {
scansAdapter?.forSelection { id, position ->
ac.askForName(
id,
scansAdapter?.getName(position),
scansAdapter?.getContent(position)
)
}
closeActionMode()
true
}
R.id.remove_scan -> {
scansAdapter?.getSelectedIds()?.let {
if (it.isNotEmpty()) {
ac.askToRemoveScans(it)
}
}
closeActionMode()
true
}
else -> false
}
}
override fun onDestroyActionMode(mode: ActionMode) {
closeActionMode()
}
}
private var scansAdapter: ScansAdapter? = null
private var listViewState: Parcelable? = null
private var actionMode: ActionMode? = null
private var filter: String? = null
private var clearListMenuItem: MenuItem? = null
private var exportHistoryMenuItem: MenuItem? = null
override fun onCreate(state: Bundle?) {
super.onCreate(state)
@@ -37,8 +124,9 @@ class HistoryFragment : Fragment() {
inflater: LayoutInflater,
container: ViewGroup?,
state: Bundle?
): View {
activity?.setTitle(R.string.history)
): View? {
val ac = activity ?: return null
ac.setTitle(R.string.history)
val view = inflater.inflate(
R.layout.fragment_history,
@@ -46,55 +134,95 @@ class HistoryFragment : Fragment() {
false
)
val useHistorySwitch = view.findViewById(
useHistorySwitch = view.findViewById(
R.id.use_history
) as SwitchCompat
initHistorySwitch(useHistorySwitch)
listView = view.findViewById(R.id.scans)
listView.emptyView = useHistorySwitch
listView.setOnItemClickListener { _, _, _, id ->
showScan(id)
}
listView.setOnItemLongClickListener { _, v, _, id ->
askToRemoveScan(v.context, id)
listView.setOnItemLongClickListener { _, v, position, id ->
scansAdapter?.select(v, id, position)
if (actionMode == null && ac is AppCompatActivity) {
actionMode = ac.delegate.startSupportActionMode(
actionModeCallback
)
}
true
}
listView.setOnScrollListener(systemBarScrollListener)
listView.setOnScrollListener(systemBarListViewScrollListener)
fab = view.findViewById(R.id.share)
fab.setOnClickListener { v ->
pickListSeparatorAndShare(v.context)
v.context.pickListSeparatorAndShare()
}
progressView = view.findViewById(R.id.progress_view)
(view.findViewById(R.id.inset_layout) as View).setPaddingFromWindowInsets()
listView.setPaddingFromWindowInsets()
update()
return view
}
override fun onDestroy() {
super.onDestroy()
(listView.adapter as ScansAdapter?)?.changeCursor(null)
scansAdapter?.changeCursor(null)
parentJob.cancel()
}
override fun onResume() {
super.onResume()
update(context)
override fun onPause() {
super.onPause()
listViewState = listView.onSaveInstanceState()
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.fragment_history, menu)
initSearchView(menu.findItem(R.id.search))
menu.setGroupVisible(R.id.scans_available, scansAdapter?.count != 0)
clearListMenuItem = menu.findItem(R.id.clear)
exportHistoryMenuItem = menu.findItem(R.id.export_history)
}
private fun initSearchView(item: MenuItem?) {
item ?: return
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) {
item.isVisible = false
} else {
val ac = activity ?: return
val searchView = item.actionView as SearchView
val searchManager = ac.getSystemService(
Context.SEARCH_SERVICE
) as SearchManager
searchView.setSearchableInfo(
searchManager.getSearchableInfo(ac.componentName)
)
searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String): Boolean {
update(query)
return false
}
override fun onQueryTextChange(query: String): Boolean {
update(query)
return false
}
})
}
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.clear -> {
askToRemoveAllScans(context)
context.askToRemoveScans()
true
}
R.id.export_history -> {
askToExportToFile(context)
askToExportToFile()
true
}
else -> super.onOptionsItemSelected(item)
@@ -110,141 +238,180 @@ class HistoryFragment : Fragment() {
}
}
private fun update(context: Context) {
private fun updateAndClearFilter() {
filter = null
update()
}
private fun update(query: String? = null) {
query?.let { filter = it }
scope.launch {
val cursor = db.getScansCursor()
val cursor = db.getScans(filter)
withContext(Dispatchers.Main) {
fab.visibility = if (cursor != null && cursor.count > 0) {
val ac = activity ?: return@withContext
val hasScans = cursor != null && cursor.count > 0
if (filter == null) {
if (!hasScans) {
listView.emptyView = useHistorySwitch
}
ActivityCompat.invalidateOptionsMenu(ac)
}
enableMenuItems(hasScans)
fab.visibility = if (hasScans) {
View.VISIBLE
} else {
View.GONE
}
cursor?.let {
if (listView.adapter == null) {
listView.adapter = ScansAdapter(context, cursor)
} else {
val adapter = listView.adapter as ScansAdapter
adapter.changeCursor(cursor)
adapter.notifyDataSetChanged()
cursor?.let { cursor ->
// Close previous cursor.
scansAdapter?.also { it.changeCursor(null) }
scansAdapter = ScansAdapter(ac, cursor)
listView.adapter = scansAdapter
listViewState?.also {
listView.onRestoreInstanceState(it)
}
}
}
}
}
private fun enableMenuItems(enabled: Boolean) {
clearListMenuItem?.isEnabled = enabled
exportHistoryMenuItem?.isEnabled = enabled
}
private fun closeActionMode() {
unlockStatusBarColor()
scansAdapter?.clearSelection()
actionMode?.finish()
actionMode = null
scansAdapter?.notifyDataSetChanged()
}
private fun showScan(id: Long) = db.getScan(id)?.also { scan ->
closeActionMode()
try {
addFragment(
fragmentManager,
DecodeFragment.newInstance(
scan.content,
BarcodeFormat.valueOf(scan.format),
scan.raw
)
)
fragmentManager?.addFragment(DecodeFragment.newInstance(scan))
} catch (e: IllegalArgumentException) {
// shouldn't ever happen
// Ignore, can never happen.
}
}
private fun askToRemoveScan(context: Context, id: Long) {
AlertDialog.Builder(context)
.setMessage(R.string.really_remove_scan)
.setPositiveButton(android.R.string.ok) { _, _ ->
db.removeScan(id)
update(context)
}
.setNegativeButton(android.R.string.cancel) { _, _ ->
}
.show()
}
private fun askToRemoveAllScans(context: Context) {
AlertDialog.Builder(context)
.setMessage(R.string.really_remove_all_scans)
.setPositiveButton(android.R.string.ok) { _, _ ->
db.removeScans()
update(context)
}
.setNegativeButton(android.R.string.cancel) { _, _ ->
}
.show()
}
private fun askToExportToFile(context: Context) = scope.launch {
progressView.useVisibility {
if (!hasWritePermission(activity)) {
return@useVisibility
}
val (getBinaries, scans) = getScans {
alertDialog(context) { resume ->
setTitle(R.string.csv_allow_binary)
setMessage(R.string.csv_allow_binary_message)
setPositiveButton(R.string.no) { _, _ ->
resume(false)
}
setNegativeButton(android.R.string.yes) { _, _ ->
resume(true)
}
setNeutralButton(android.R.string.cancel) { _, _ ->
resume(null)
}
// Dialogs don't have a parent layout.
@SuppressLint("InflateParams")
private fun Context.askForName(id: Long, text: String?, content: String?) {
val view = LayoutInflater.from(this).inflate(
R.layout.dialog_enter_name, null
)
val nameView = view.findViewById<EditText>(R.id.name)
nameView.setText(text)
AlertDialog.Builder(this)
.setTitle(
if (content.isNullOrEmpty()) {
getString(R.string.binary_data)
} else {
content
}
} ?: return@useVisibility
val delimiters = context.resources.getStringArray(
R.array.csv_delimiters_values
)
val delimiter = alertDialog<String>(context) { resume ->
setTitle(R.string.csv_delimiter)
setItems(R.array.csv_delimiters_names) { _, which ->
resume(delimiters[which])
.setView(view)
.setPositiveButton(android.R.string.ok) { _, _ ->
val name = nameView.text.toString()
db.renameScan(id, name)
update()
}
.setNegativeButton(android.R.string.cancel) { _, _ -> }
.show()
}
private fun Context.askToRemoveScans(ids: List<Long>) {
AlertDialog.Builder(this)
.setMessage(
if (ids.size > 1) {
R.string.really_remove_selected_scans
} else {
R.string.really_remove_scan
}
)
.setPositiveButton(android.R.string.ok) { _, _ ->
ids.forEach { db.removeScan(it) }
if (scansAdapter?.count == 1) {
updateAndClearFilter()
} else {
update()
}
}
.setNegativeButton(android.R.string.cancel) { _, _ ->
}
.show()
}
private fun Context.askToRemoveScans() {
AlertDialog.Builder(this)
.setMessage(
if (filter == null) {
R.string.really_remove_all_scans
} else {
R.string.really_remove_selected_scans
}
)
.setPositiveButton(android.R.string.ok) { _, _ ->
db.removeScans(filter)
updateAndClearFilter()
}
.setNegativeButton(android.R.string.cancel) { _, _ ->
}
.show()
}
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
}
val options = ac.resources.getStringArray(
R.array.export_options_values
)
val delimiter = alertDialog<String>(ac) { resume ->
setTitle(R.string.export_as)
setItems(R.array.export_options_names) { _, which ->
resume(options[which])
}
} ?: return@useVisibility
val name = withContext(Dispatchers.Main) {
ac.askForFileName(
when (delimiter) {
"db" -> ".db"
"json" -> ".json"
else -> ".csv"
}
)
} ?: return@useVisibility
val message = when (delimiter) {
"db" -> 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)
}
} ?: return@useVisibility
val name = withContext(Dispatchers.Main) {
activity.askForFileName(suffix = "csv")
} ?: return@useVisibility
val csv = scans.toCSV(delimiter, getBinaries)
val toastMessage = csv.writeToFile(name)
withContext(Dispatchers.Main) {
Toast.makeText(
context,
toastMessage,
Toast.LENGTH_SHORT
).show()
}
}
}
private fun Flow<DatabaseRepository.Scan>.toCSV(
delimiter: String,
allowBinary: Boolean
): Flow<ByteArray> {
return csvBuilder<DatabaseRepository.Scan> {
column {
name = "DATE"
gettingByString { it.timestamp }
}
column {
name = "TYPE"
gettingByString { it.format }
}
column {
name = "CONTENT"
gettingByString { it.content }
}
if (allowBinary) column {
isBinary = true
name = "BINARY_CONTENT"
gettingBy { it.raw ?: ByteArray(0) }
}
}.buildWith(this, delimiter)
}
private fun pickListSeparatorAndShare(context: Context) {
val separators = context.resources.getStringArray(
private fun Context.pickListSeparatorAndShare() {
val separators = resources.getStringArray(
R.array.list_separators_values
)
AlertDialog.Builder(context)
AlertDialog.Builder(this)
.setTitle(R.string.pick_list_separator)
.setItems(R.array.list_separators_names) { _, which ->
shareScans(separators[which])
@@ -252,33 +419,37 @@ class HistoryFragment : Fragment() {
.show()
}
private fun shareScans(separator: String) {
scope.launch {
progressView.useVisibility {
val sb = StringBuilder()
getScans { false }?.second?.collect {
sb.append(it.content)
sb.append(separator)
}
val text = sb.toString()
if (text.isNotEmpty()) withContext(Dispatchers.Main) {
shareText(context, text)
private fun shareScans(format: String) = scope.launch {
progressView.useVisibility {
var text: String? = null
db.getScansDetailed(filter)?.use { cursor ->
val details = format.split(":")
text = when (details[0]) {
"text" -> cursor.exportText(details[1])
"csv" -> cursor.exportCsv(details[1])
else -> cursor.exportJson()
}
}
}
}
@WorkerThread
private suspend inline fun getScans(
crossinline binaryData: suspend () -> Boolean?
): Pair<Boolean, Flow<DatabaseRepository.Scan>>? {
val getBinaries: Boolean = db.hasBinaryData() && binaryData() ?: return null
return getBinaries to db.getScans().mapNotNull {
when {
it.content.isNotEmpty() -> db.getScan(it.id)
getBinaries -> db.getScan(it.id)
else -> null
text?.let {
withContext(Dispatchers.Main) {
context.shareText(it)
}
}
}
}
}
private fun Cursor.exportText(separator: String): String {
val sb = StringBuilder()
val contentIndex = getColumnIndex(Database.SCANS_CONTENT)
if (contentIndex > -1 && moveToFirst()) {
do {
val content = getString(contentIndex)
if (content?.isNotEmpty() == true) {
sb.append(content)
sb.append(separator)
}
} while (moveToNext())
}
return sb.toString()
}
@@ -0,0 +1,92 @@
package de.markusfisch.android.binaryeye.fragment
import android.content.Context
import android.net.wifi.WifiManager
import android.net.wifi.WifiNetworkSuggestion
import android.os.Build
import android.os.Bundle
import android.support.annotation.RequiresApi
import android.support.v4.app.Fragment
import android.view.*
import android.widget.ArrayAdapter
import android.widget.ListView
import de.markusfisch.android.binaryeye.R
import de.markusfisch.android.binaryeye.view.setPaddingFromWindowInsets
import de.markusfisch.android.binaryeye.view.systemBarListViewScrollListener
import de.markusfisch.android.binaryeye.widget.toast
class NetworkSuggestionsFragment : Fragment() {
@RequiresApi(Build.VERSION_CODES.R)
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
state: Bundle?
): View? {
val ac = activity ?: return null
ac.setTitle(R.string.network_suggestions)
val view = inflater.inflate(
R.layout.fragment_network_suggestions,
container,
false
)
val wm = ac.applicationContext.getSystemService(
Context.WIFI_SERVICE
) as WifiManager
val suggestionArrayAdapter = ArrayAdapter(
ac,
android.R.layout.simple_list_item_checked,
wm.networkSuggestions.map {
Suggestion(
it.ssid ?: it.toString(),
it
)
}
)
val listView = view.findViewById<ListView>(R.id.suggestions)
listView.emptyView = view.findViewById(R.id.no_suggestions)
listView.adapter = suggestionArrayAdapter
listView.setOnScrollListener(systemBarListViewScrollListener)
(view.findViewById(R.id.inset_layout) as View).setPaddingFromWindowInsets()
listView.setPaddingFromWindowInsets()
view.findViewById<View>(R.id.remove).setOnClickListener {
val removeList = ArrayList<WifiNetworkSuggestion>()
val removeFromAdapter = ArrayList<Suggestion>()
val checked = listView.checkedItemPositions
val size = checked.size()
for (i in 0 until size) {
if (checked.valueAt(i)) {
val pos = checked.keyAt(i)
listView.setItemChecked(pos, false)
val suggestion = listView.getItemAtPosition(
pos
) as Suggestion
removeList.add(suggestion.suggestion)
removeFromAdapter.add(suggestion)
}
}
if (removeList.isNotEmpty()) {
removeFromAdapter.forEach {
suggestionArrayAdapter.remove(it)
}
wm.removeNetworkSuggestions(removeList)
} else {
ac.toast(
R.string.clear_network_suggestions_nothing_to_remove
)
}
}
return view
}
}
private data class Suggestion(
val label: String,
val suggestion: WifiNetworkSuggestion
) {
override fun toString() = label
}
@@ -1,53 +1,177 @@
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.activity.SplashActivity
import de.markusfisch.android.binaryeye.app.addFragment
import de.markusfisch.android.binaryeye.app.prefs
import de.markusfisch.android.binaryeye.media.beepConfirm
import de.markusfisch.android.binaryeye.preference.UrlPreference
import de.markusfisch.android.binaryeye.view.setPaddingFromWindowInsets
import de.markusfisch.android.binaryeye.view.systemBarRecyclerViewScrollListener
import de.markusfisch.android.binaryeye.widget.toast
class PreferencesFragment : Fragment() {
private lateinit var openImmediatelySwitch: SwitchCompat
private lateinit var useHistorySwitch: SwitchCompat
private lateinit var openWithUrlInput: EditText
class PreferencesFragment : PreferenceFragmentCompat() {
private val changeListener = object : OnSharedPreferenceChangeListener {
override fun onSharedPreferenceChanged(
sharedPreferences: SharedPreferences,
key: String
) {
val preference = findPreference(key) ?: return
prefs.update()
when (preference.key) {
"custom_locale" -> activity?.restartApp()
"beep_tone_name" -> {
beepConfirm()
setSummary(preference)
}
else -> setSummary(preference)
}
}
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
state: Bundle?
): View? {
activity?.setTitle(R.string.preferences)
override fun onCreatePreferences(state: Bundle?, rootKey: String?) {
addPreferencesFromResource(R.xml.preferences)
wireClearNetworkPreferences()
}
val view = inflater.inflate(
R.layout.fragment_preferences,
container,
false
private fun wireClearNetworkPreferences() {
findPreference("clear_network_suggestions").apply {
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.Q) {
// From R+ we can query past network suggestions and
// make them editable.
setOnPreferenceClickListener {
fragmentManager.addFragment(NetworkSuggestionsFragment())
true
}
} else if (Build.VERSION.SDK_INT == Build.VERSION_CODES.Q) {
// On Q, we can only clear *all* suggestions.
// Note that previous versions of this app allowed
// adding network suggestions on Q as well, so we
// need to keep this option.
setOnPreferenceClickListener {
context.askToClearNetworkSuggestions()
true
}
} else {
// There are no network suggestions below Q.
isVisible = false
}
}
}
@RequiresApi(Build.VERSION_CODES.Q)
private fun Context.askToClearNetworkSuggestions() {
AlertDialog.Builder(this)
.setMessage(R.string.really_remove_all_networks)
.setPositiveButton(android.R.string.ok) { _, _ ->
clearNetworkSuggestions()
}
.setNegativeButton(android.R.string.cancel) { _, _ -> }
.show()
}
@RequiresApi(Build.VERSION_CODES.Q)
private fun Context.clearNetworkSuggestions() {
toast(
if (removeAllNetworkSuggestions() ==
WifiManager.STATUS_NETWORK_SUGGESTIONS_SUCCESS
) {
R.string.clear_network_suggestions_success
} else {
R.string.clear_network_suggestions_nothing_to_remove
}
)
}
openImmediatelySwitch = view.findViewById<SwitchCompat>(R.id.open_immediately)
if (prefs.openImmediately) {
openImmediatelySwitch.toggle()
}
useHistorySwitch = view.findViewById<SwitchCompat>(R.id.use_history)
if (prefs.useHistory) {
useHistorySwitch.toggle()
}
openWithUrlInput = view.findViewById<EditText>(R.id.open_with_url)
openWithUrlInput.setText(prefs.openWithUrl)
return view
override fun onResume() {
super.onResume()
activity?.setTitle(R.string.preferences)
listView.setPaddingFromWindowInsets()
listView.removeOnScrollListener(systemBarRecyclerViewScrollListener)
listView.addOnScrollListener(systemBarRecyclerViewScrollListener)
preferenceScreen.sharedPreferences
.registerOnSharedPreferenceChangeListener(changeListener)
setSummaries(preferenceScreen)
}
override fun onPause() {
super.onPause()
prefs.openImmediately = openImmediatelySwitch.isChecked
prefs.useHistory = useHistorySwitch.isChecked
prefs.openWithUrl = openWithUrlInput.text.toString()
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)
}
@RequiresApi(Build.VERSION_CODES.Q)
private fun Context.removeAllNetworkSuggestions(): Int =
(applicationContext.getSystemService(
Context.WIFI_SERVICE
) as WifiManager).removeNetworkSuggestions(listOf())
@@ -0,0 +1,88 @@
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"
).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
}
@@ -1,40 +1,71 @@
package de.markusfisch.android.binaryeye.graphics
import android.content.ContentResolver
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.Matrix
import android.graphics.RectF
import android.net.Uri
import java.io.IOException
import kotlin.math.max
import kotlin.math.min
import kotlin.math.roundToInt
fun downsizeIfBigger(bitmap: Bitmap, maxSize: Int): Bitmap = if (
max(bitmap.width, bitmap.height) > maxSize
) {
val srcWidth = bitmap.width
val srcHeight = bitmap.height
var newWidth = maxSize
var newHeight = maxSize
if (srcWidth > srcHeight) {
newHeight = (newWidth.toFloat() / srcWidth.toFloat() *
srcHeight.toFloat()).roundToInt()
} else {
newWidth = (newHeight.toFloat() / srcHeight.toFloat() *
srcWidth.toFloat()).roundToInt()
fun ContentResolver.loadImageUri(uri: Uri): Bitmap? = try {
val options = BitmapFactory.Options()
openInputStream(uri)?.use {
options.inJustDecodeBounds = true
BitmapFactory.decodeStream(it, null, options)
options.inSampleSize = calculateInSampleSize(
options.outWidth,
options.outHeight
)
options.inJustDecodeBounds = false
}
Bitmap.createScaledBitmap(bitmap, newWidth, newHeight, true)
} else {
bitmap
openInputStream(uri)?.use {
BitmapFactory.decodeStream(it, null, options)
}
} catch (e: IOException) {
null
}
fun crop(bitmap: Bitmap, rect: RectF, rotation: Float) = try {
val erected = erect(bitmap, rotation)
private fun calculateInSampleSize(
width: Int,
height: Int,
reqWidth: Int = 1024,
reqHeight: Int = 1024
): Int {
var inSampleSize = 1
if (height > reqHeight || width > reqWidth) {
val halfHeight = height / 2
val halfWidth = width / 2
while (
halfHeight / inSampleSize >= reqHeight ||
halfWidth / inSampleSize >= reqWidth
) {
inSampleSize *= 2
}
}
return inSampleSize
}
fun 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
@@ -42,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
}
@@ -0,0 +1,4 @@
package de.markusfisch.android.binaryeye.graphics
const val COLOR_BLACK = 0xff000000.toInt()
const val COLOR_WHITE = 0xffffffff.toInt()
@@ -0,0 +1,15 @@
package de.markusfisch.android.binaryeye.graphics
import android.content.Context
import android.graphics.DashPathEffect
import android.graphics.Paint
import android.support.v4.content.ContextCompat
import de.markusfisch.android.binaryeye.R
fun Context.getDashedBorderPaint() = Paint(Paint.ANTI_ALIAS_FLAG).apply {
val dp = resources.displayMetrics.density
color = ContextCompat.getColor(applicationContext, R.color.crop_bound)
style = Paint.Style.STROKE
strokeWidth = dp * 2f
pathEffect = DashPathEffect(floatArrayOf(10f * dp, 10f * dp), 0f)
}
@@ -0,0 +1,36 @@
package de.markusfisch.android.binaryeye.graphics
import android.content.res.Resources
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.Drawable
import android.os.Build
fun Resources.getBitmapFromDrawable(
resId: Int
): Bitmap = getBitmapFromDrawable(getDrawableCompat(resId))
fun Resources.getDrawableCompat(
resId: Int
): Drawable = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
getDrawable(resId, null)
} else {
@Suppress("DEPRECATION")
getDrawable(resId)
}
private fun getBitmapFromDrawable(drawable: Drawable): Bitmap {
if (drawable is BitmapDrawable) {
return drawable.bitmap
}
val bitmap = Bitmap.createBitmap(
drawable.intrinsicWidth,
drawable.intrinsicHeight,
Bitmap.Config.ARGB_8888
)
val canvas = Canvas(bitmap)
drawable.setBounds(0, 0, canvas.width, canvas.height)
drawable.draw(canvas)
return bitmap
}
@@ -0,0 +1,87 @@
package de.markusfisch.android.binaryeye.graphics
import android.graphics.Matrix
import android.graphics.Rect
import android.graphics.RectF
import de.markusfisch.android.zxingcpp.ZxingCpp.Position
import kotlin.math.roundToInt
data class FrameMetrics(
var width: Int = 0,
var height: Int = 0,
var orientation: Int = 0
)
fun Rect.setFrameRoi(
frameMetrics: FrameMetrics,
viewRect: Rect,
viewRoi: Rect
) {
Matrix().apply {
// Map ROI from view coordinates to frame coordinates.
setTranslate(-viewRect.left.toFloat(), -viewRect.top.toFloat())
postScale(1f / viewRect.width(), 1f / viewRect.height())
postRotate(-frameMetrics.orientation.toFloat(), .5f, .5f)
postScale(frameMetrics.width.toFloat(), frameMetrics.height.toFloat())
val frameRoiF = RectF()
val viewRoiF = RectF(
viewRoi.left.toFloat(),
viewRoi.top.toFloat(),
viewRoi.right.toFloat(),
viewRoi.bottom.toFloat()
)
mapRect(frameRoiF, viewRoiF)
set(
frameRoiF.left.roundToInt(),
frameRoiF.top.roundToInt(),
frameRoiF.right.roundToInt(),
frameRoiF.bottom.roundToInt()
)
}
}
fun Matrix.setFrameToView(
frameMetrics: FrameMetrics,
viewRect: Rect,
viewRoi: Rect? = null
) {
// Configure this matrix to map points in frame coordinates to
// view coordinates.
val uprightWidth: Int
val uprightHeight: Int
when (frameMetrics.orientation) {
90, 270 -> {
uprightWidth = frameMetrics.height
uprightHeight = frameMetrics.width
}
else -> {
uprightWidth = frameMetrics.width
uprightHeight = frameMetrics.height
}
}
setScale(
viewRect.width().toFloat() / uprightWidth,
viewRect.height().toFloat() / uprightHeight
)
viewRoi?.let {
postTranslate(viewRoi.left.toFloat(), viewRoi.top.toFloat())
}
}
fun Matrix.mapPosition(
position: Position,
coords: FloatArray
): Int {
var i = 0
setOf(
position.topLeft,
position.topRight,
position.bottomRight,
position.bottomLeft
).forEach {
coords[i++] = it.x.toFloat()
coords[i++] = it.y.toFloat()
}
mapPoints(coords)
return i
}
@@ -0,0 +1,57 @@
package de.markusfisch.android.binaryeye.graphics
import android.graphics.Bitmap
import android.graphics.Canvas
fun Bitmap.fixTransparency(): Bitmap {
val result = analyzeTransparency()
return if (result.hasTransparentPixels) {
val copy = Bitmap.createBitmap(
width,
height,
config
)
Canvas(copy).apply {
drawColor(result.backgroundColor)
drawBitmap(this@fixTransparency, 0f, 0f, null)
}
copy
} else {
this
}
}
private fun Bitmap.analyzeTransparency(): Result {
val pixels = IntArray(width * height)
getPixels(pixels, 0, width, 0, 0, width, height)
var transparent = 0
var visible = 0
var bright = 0
for (pixel in pixels) {
val alpha = (pixel shr 24) and 0xff
if (alpha < 0xff) {
++transparent
}
if (alpha > 0) {
++visible
if (
((pixel shr 16) and 0xff) +
((pixel shr 8) and 0xff) +
(pixel and 0xff) > 128
) ++bright
}
}
return Result(
transparent > 0,
if (bright > 0 && visible / bright < 2) {
COLOR_BLACK
} else {
COLOR_WHITE
}
)
}
private data class Result(
val hasTransparentPixels: Boolean,
val backgroundColor: Int
)
@@ -0,0 +1,85 @@
package de.markusfisch.android.binaryeye.io
import android.app.Activity
import android.content.ContentValues
import android.content.Context
import android.os.Build
import android.os.Environment
import android.provider.MediaStore
import android.support.annotation.MainThread
import android.widget.EditText
import de.markusfisch.android.binaryeye.R
import de.markusfisch.android.binaryeye.app.alertDialog
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import java.io.OutputStream
fun addSuffixIfNotGiven(name: String, suffix: String): String {
val trimmed = name.trim()
return if (suffix.isNotEmpty() && !trimmed.endsWith(suffix)) {
"$trimmed$suffix"
} else {
trimmed
}
}
// Dialogs doesn't have any root layout, need to be inflated with null root
@Suppress("InflateParams")
@MainThread
suspend fun Activity.askForFileName(suffix: String = ""): String? {
val view = layoutInflater.inflate(R.layout.dialog_save_file, null)
val editText = view.findViewById<EditText>(R.id.file_name)
return alertDialog<String>(this as Context) { resume ->
setView(view)
setPositiveButton(android.R.string.ok) { _, _ ->
resume(editText.text.toString())
}
}?.let { name ->
addSuffixIfNotGiven(name, suffix)
}
}
fun Boolean.toSaveResult() = if (this) {
R.string.saved_in_downloads
} else {
R.string.error_saving_file
}
fun Context.writeExternalFile(
fileName: String,
mimeType: String,
write: (outputStream: OutputStream) -> Unit
): Boolean = try {
openExternalOutputStream(fileName, mimeType).use { write(it) }
true
} catch (e: IOException) {
false
}
private fun Context.openExternalOutputStream(
fileName: String,
mimeType: String
): OutputStream = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
@Suppress("DEPRECATION")
val file = File(
Environment.getExternalStoragePublicDirectory(
Environment.DIRECTORY_DOWNLOADS
),
fileName
)
if (file.exists()) {
throw IOException()
}
FileOutputStream(file)
} else {
val resolver = contentResolver
val uri = resolver.insert(
MediaStore.Downloads.EXTERNAL_CONTENT_URI,
ContentValues().apply {
put(MediaStore.Downloads.DISPLAY_NAME, fileName)
put(MediaStore.Downloads.MIME_TYPE, mimeType)
}
) ?: throw IOException()
resolver.openOutputStream(uri) ?: throw IOException()
}
@@ -0,0 +1,33 @@
package de.markusfisch.android.binaryeye.media
import android.media.AudioManager
import android.media.ToneGenerator
import de.markusfisch.android.binaryeye.app.prefs
private var confirmToneGenerator: ToneGenerator? = null
private var errorToneGenerator: ToneGenerator? = null
fun beepConfirm() {
val tg = confirmToneGenerator ?: ToneGenerator(
AudioManager.STREAM_NOTIFICATION,
ToneGenerator.MAX_VOLUME
)
confirmToneGenerator = tg
tg.startTone(prefs.beepTone())
}
fun beepError() {
val tg = errorToneGenerator ?: ToneGenerator(
AudioManager.STREAM_ALARM,
ToneGenerator.MAX_VOLUME
)
errorToneGenerator = tg
tg.startTone(ToneGenerator.TONE_SUP_ERROR)
}
fun releaseToneGenerators() {
confirmToneGenerator?.release()
confirmToneGenerator = null
errorToneGenerator?.release()
errorToneGenerator = null
}
@@ -0,0 +1,124 @@
package de.markusfisch.android.binaryeye.net
import de.markusfisch.android.binaryeye.content.toHexString
import de.markusfisch.android.binaryeye.database.Scan
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.json.JSONObject
import java.io.BufferedReader
import java.io.IOException
import java.io.InputStream
import java.io.InputStreamReader
import java.net.HttpURLConnection
import java.net.ProtocolException
import java.net.URL
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,
"version" to version,
"sequenceSize" to sequenceSize.toString(),
"sequenceIndex" to sequenceIndex.toString(),
"sequenceId" to sequenceId,
"country" to country,
"addOn" to addOn,
"price" to price,
"issueNumber" to issueNumber,
"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,10 @@
package de.markusfisch.android.binaryeye.net
import java.io.UnsupportedEncodingException
import java.net.URLEncoder
fun String.urlEncode(): String = try {
URLEncoder.encode(this, "UTF-8")
} catch (e: UnsupportedEncodingException) {
this
}
@@ -0,0 +1,66 @@
package de.markusfisch.android.binaryeye.os
import android.content.Context
import android.os.Build
import android.os.VibrationEffect
import android.os.Vibrator
private const val DURATION_IN_MS = 250L
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
)
// On SDK 31 and better, we should ask the `VIBRATOR_MANAGER_SERVICE`
// for the default vibrator. This was implemented here before:
/*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
}*/
// But for reasons beyond my interests, using `VIBRATOR_MANAGER_SERVICE`
// leads to a very cryptic `java.lang.VerifyError: r0/b` on Android 4.
// This is probably a Proguard/R8 issue, but I don't want to waste more
// time just to get rid of the deprecation warning. So let's use the old
// `VIBRATOR_SERVICE` and wait for Google to fix their stuff.
fun Context.getVibrator(): Vibrator = getSystemService(
Context.VIBRATOR_SERVICE
) as Vibrator
fun Vibrator.vibrate() {
if (beforeO) {
@Suppress("DEPRECATION")
vibrate(DURATION_IN_MS)
} else {
vibrate(
VibrationEffect.createOneShot(
DURATION_IN_MS,
VibrationEffect.DEFAULT_AMPLITUDE
)
)
}
}
fun Vibrator.error() {
if (beforeO) {
@Suppress("DEPRECATION")
vibrate(errorPatternTimings, -1)
} else {
vibrate(
VibrationEffect.createWaveform(
errorPatternTimings,
errorPatternAmplitudes,
-1
)
)
}
}
@@ -2,24 +2,182 @@ package de.markusfisch.android.binaryeye.preference
import android.content.Context
import android.content.SharedPreferences
import android.media.ToneGenerator
import android.os.Build
import android.preference.PreferenceManager
import android.support.annotation.RequiresApi
import de.markusfisch.android.zxingcpp.ZxingCpp.Format
class Preferences {
lateinit var preferences: SharedPreferences
var openImmediately = false
var barcodeFormats = setOf(
Format.AZTEC.name,
Format.CODABAR.name,
Format.CODE_39.name,
Format.CODE_93.name,
Format.CODE_128.name,
Format.DATA_BAR.name,
Format.DATA_BAR_EXPANDED.name,
Format.DATA_MATRIX.name,
Format.EAN_8.name,
Format.EAN_13.name,
Format.ITF.name,
Format.MAXICODE.name,
Format.PDF_417.name,
Format.QR_CODE.name,
Format.MICRO_QR_CODE.name,
Format.UPC_A.name,
Format.UPC_E.name,
)
@RequiresApi(Build.VERSION_CODES.HONEYCOMB)
set(value) {
setBoolean(OPEN_IMMEDIATELY, 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 = -2
set(value) {
apply(CROP_HANDLE_Y, value)
field = value
}
var cropHandleOrientation = 0
set(value) {
apply(CROP_HANDLE_ORIENTATION, value)
field = value
}
var showCropHandle = true
set(value) {
apply(SHOW_CROP_HANDLE, value)
field = value
}
var zoomBySwiping = true
set(value) {
apply(ZOOM_BY_SWIPING, value)
field = value
}
var autoRotate = true
set(value) {
apply(AUTO_ROTATE, value)
field = value
}
var tryHarder = false
set(value) {
apply(TRY_HARDER, value)
field = value
}
var bulkMode = false
set(value) {
apply(BULK_MODE, value)
field = value
}
var bulkModeDelay = "500"
set(value) {
apply(BULK_MODE_DELAY, value)
field = value
}
var showToastInBulkMode = true
set(value) {
apply(SHOW_TOAST_IN_BULK_MODE, value)
field = value
}
var vibrate = true
set(value) {
apply(VIBRATE, value)
field = value
}
var beep = false
set(value) {
apply(BEEP, value)
field = value
}
var beepToneName = "tone_prop_beep"
set(value) {
apply(BEEP_TONE_NAME, value)
field = value
}
var useHistory = false
set(value) {
setBoolean(USE_HISTORY, value)
apply(USE_HISTORY, value)
field = value
}
var ignoreConsecutiveDuplicates = true
set(value) {
apply(IGNORE_CONSECUTIVE_DUPLICATES, value)
field = value
}
var copyImmediately = false
set(value) {
apply(COPY_IMMEDIATELY, value)
field = value
}
var openImmediately = false
set(value) {
apply(OPEN_IMMEDIATELY, value)
field = value
}
var showMetaData = true
set(value) {
apply(SHOW_META_DATA, value)
field = value
}
var showHexDump = true
set(value) {
apply(SHOW_HEX_DUMP, value)
field = value
}
var showRecreation = true
set(value) {
apply(SHOW_RECREATION, 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) {
setString(OPEN_WITH_URL, 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 freeRotation = true
set(value) {
apply(FREE_ROTATION, value)
field = value
}
@@ -29,26 +187,146 @@ class Preferences {
}
fun update() {
openImmediately = preferences.getBoolean(OPEN_IMMEDIATELY, openImmediately)
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
)
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)
beep = preferences.getBoolean(BEEP, beep)
preferences.getString(BEEP_TONE_NAME, beepToneName)?.also {
beepToneName = it
}
useHistory = preferences.getBoolean(USE_HISTORY, useHistory)
preferences.getString(OPEN_WITH_URL, openWithUrl)?.also { openWithUrl = it }
ignoreConsecutiveDuplicates = preferences.getBoolean(
IGNORE_CONSECUTIVE_DUPLICATES,
ignoreConsecutiveDuplicates
)
copyImmediately = preferences.getBoolean(
COPY_IMMEDIATELY,
copyImmediately
)
openImmediately = preferences.getBoolean(
OPEN_IMMEDIATELY,
openImmediately
)
showMetaData = preferences.getBoolean(SHOW_META_DATA, showMetaData)
showHexDump = preferences.getBoolean(SHOW_HEX_DUMP, showHexDump)
showRecreation = preferences.getBoolean(SHOW_RECREATION, showRecreation)
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
)
freeRotation = preferences.getBoolean(FREE_ROTATION, freeRotation)
}
private fun setBoolean(label: String, value: Boolean) {
val editor = preferences.edit()
editor.putBoolean(label, value)
editor.apply()
fun beepTone() = when (beepToneName) {
"tone_cdma_confirm" -> ToneGenerator.TONE_CDMA_CONFIRM
"tone_sup_radio_ack" -> ToneGenerator.TONE_SUP_RADIO_ACK
"tone_prop_ack" -> ToneGenerator.TONE_PROP_ACK
"tone_prop_beep" -> ToneGenerator.TONE_PROP_BEEP
"tone_prop_beep2" -> ToneGenerator.TONE_PROP_BEEP2
else -> ToneGenerator.TONE_PROP_BEEP
}
private fun setString(label: String, value: String) {
val editor = preferences.edit()
editor.putString(label, value)
editor.apply()
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 apply(label: String, value: Boolean) {
put(label, value).apply()
}
private fun apply(label: String, value: String) {
preferences.edit().putString(label, value).apply()
}
private fun apply(label: String, value: Int) {
preferences.edit().putInt(label, value).apply()
}
@RequiresApi(Build.VERSION_CODES.HONEYCOMB)
private fun apply(label: String, value: Set<String>) {
preferences.edit().putStringSet(label, value).apply()
}
companion object {
const val OPEN_IMMEDIATELY = "open_immediately"
const val USE_HISTORY = "use_history"
const val OPEN_WITH_URL = "open_with_url"
private const val BARCODE_FORMATS = "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 BEEP = "beep"
private const val BEEP_TONE_NAME = "beep_tone_name"
private const val USE_HISTORY = "use_history"
private const val IGNORE_CONSECUTIVE_DUPLICATES = "ignore_consecutive_duplicates"
private const val OPEN_IMMEDIATELY = "open_immediately"
private const val COPY_IMMEDIATELY = "copy_immediately"
private const val SHOW_META_DATA = "show_meta_data"
private const val SHOW_HEX_DUMP = "show_hex_dump"
private const val SHOW_RECREATION = "show_recreation"
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 FREE_ROTATION = "free_rotation"
}
}
@@ -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
}
)
}
}
@@ -1,128 +0,0 @@
package de.markusfisch.android.binaryeye.repository
import android.content.Context
import android.database.Cursor
import de.markusfisch.android.binaryeye.data.Database
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
class DatabaseRepository {
private val db = Database()
fun open(applicationContext: Context) {
db.open(applicationContext)
}
fun getScan(id: Long): Scan? = getScan(id) { _, cursor ->
val timeIndex = cursor.getColumnIndex(Database.SCANS_DATETIME)
val contentIndex = cursor.getColumnIndex(Database.SCANS_CONTENT)
val rawIndex = cursor.getColumnIndex(Database.SCANS_RAW)
val formatIndex = cursor.getColumnIndex(Database.SCANS_FORMAT)
Scan(
id = id,
timestamp = cursor.getString(timeIndex),
content = cursor.getString(contentIndex),
raw = cursor.getBlob(rawIndex),
format = cursor.getString(formatIndex)
)
}
fun <T> getScan(id: Long, map: (id: Long, Cursor) -> T): T? =
db.getScan(id)?.use { map(id, it.apply { moveToFirst() }) }
fun getScans(): Flow<SimpleScan> = getScans { cursor ->
val idIndex = cursor.getColumnIndex(Database.SCANS_ID)
val timeIndex = cursor.getColumnIndex(Database.SCANS_DATETIME)
val contentIndex = cursor.getColumnIndex(Database.SCANS_CONTENT)
val formatIndex = cursor.getColumnIndex(Database.SCANS_FORMAT)
SimpleScan(
id = cursor.getLong(idIndex),
timestamp = cursor.getString(timeIndex),
content = cursor.getString(contentIndex),
format = cursor.getString(formatIndex)
)
}
fun <T> getScans(map: suspend (Cursor) -> T): Flow<T> = flow {
db.getScans()?.use { results ->
results.asIterable.forEach {
emit(it)
}
}
}.map(map).flowOn(Dispatchers.IO)
fun getScansCursor(): Cursor? = db.getScans()
fun hasBinaryData() = db.hasBinaryData()?.use { it.count > 0 } ?: false
fun insertScan(
timestamp: Long,
content: String,
raw: ByteArray?,
format: String
): Long = db.insertScan(timestamp, content, raw, format)
fun removeScan(id: Long) = db.removeScan(id)
fun removeScans() = db.removeScans()
data class SimpleScan(
val id: Long,
val timestamp: String,
val content: String,
val format: String
)
data class Scan(
val id: Long,
val timestamp: String,
val content: String,
val raw: ByteArray?,
val format: String
) {
// Needed to be overwritten manually, as ByteArray is an array and this isn't handled well by Kotlin
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as Scan
if (id != other.id) return false
if (timestamp != other.timestamp) return false
if (content != other.content) return false
if (raw != null) {
if (other.raw == null) return false
if (!raw.contentEquals(other.raw)) return false
} else if (other.raw != null) return false
if (format != other.format) return false
return true
}
// Needs to be overwritten manually, as ByteArray is an array and this
// isn't handled well by Kotlin
override fun hashCode(): Int {
var result = id.hashCode()
result = 31 * result + timestamp.hashCode()
result = 31 * result + content.hashCode()
result = 31 * result + (raw?.contentHashCode() ?: 0)
result = 31 * result + format.hashCode()
return result
}
}
}
private val Cursor.asIterable: Iterable<Cursor>
get() = object : Iterable<Cursor> {
override fun iterator(): Iterator<Cursor> = object : Iterator<Cursor> {
override fun hasNext(): Boolean = position + 1 < count
override fun next(): Cursor = if (moveToNext()) {
this@asIterable
} else {
throw IllegalStateException("You can't access more items, then there are")
}
}
}
@@ -1,129 +0,0 @@
package de.markusfisch.android.binaryeye.rs
import android.content.Context
import android.support.v8.renderscript.*
import de.markusfisch.android.binaryeye.renderscript.ScriptC_rotator
import kotlin.math.roundToInt
class Preprocessor(
context: Context,
width: Int,
height: Int,
private val orientation: Int
) {
var outWidth = 0
var outHeight = 0
private val rs = RenderScript.create(context)
private val resizeScript = ScriptIntrinsicResize.create(rs)
private val rotatorScript = ScriptC_rotator(rs)
private var yuvType: Type? = null
private var yuvAlloc: Allocation? = null
private var resizedType: Type? = null
private var resizedAlloc: Allocation? = null
private var rotatedType: Type? = null
private var rotatedAlloc: Allocation? = null
init {
yuvType = Type.createXY(
rs,
Element.U8(rs),
width,
height // use only grayscale part
)
yuvAlloc = Allocation.createTyped(
rs,
yuvType,
Allocation.USAGE_SCRIPT
)
val f = .75f
var w = (width * f).roundToInt()
var h = (height * f).roundToInt()
resizedType = Type.createXY(
rs,
Element.U8(rs),
w,
h
)
resizedAlloc = Allocation.createTyped(
rs,
resizedType,
Allocation.USAGE_SCRIPT
)
if (orientation == 90 || orientation == 270) {
val tmp = w
w = h
h = tmp
}
outWidth = w
outHeight = h
if (orientation != 0) {
rotatedType = Type.createXY(
rs,
Element.U8(rs),
w,
h
)
rotatedAlloc = Allocation.createTyped(
rs,
rotatedType,
Allocation.USAGE_SCRIPT
)
}
}
fun destroy() {
yuvType?.destroy()
yuvType = null
yuvAlloc?.destroy()
yuvAlloc = null
resizedType?.destroy()
resizedType = null
resizedAlloc?.destroy()
resizedAlloc = null
rotatedType?.destroy()
rotatedType = null
rotatedAlloc?.destroy()
rotatedAlloc = null
resizeScript.destroy()
rotatorScript.destroy()
rs.destroy()
}
fun process(frame: ByteArray) {
yuvAlloc?.copyFrom(frame)
resizeScript.setInput(yuvAlloc)
resizeScript.forEach_bicubic(resizedAlloc)
val t = resizedType
if (t == null || orientation == 0) {
resizedAlloc?.copyTo(frame)
} else {
rotatorScript._inImage = resizedAlloc
rotatorScript._inWidth = t.x
rotatorScript._inHeight = t.y
when (orientation) {
90 -> rotatorScript.forEach_rotate90(
rotatedAlloc,
rotatedAlloc
)
180 -> rotatorScript.forEach_rotate180(
rotatedAlloc,
rotatedAlloc
)
270 -> rotatorScript.forEach_rotate270(
rotatedAlloc,
rotatedAlloc
)
}
rotatedAlloc?.copyTo(frame)
}
}
}
@@ -0,0 +1,17 @@
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() {
override fun onClick() {
super.onClick()
val intent = Intent(applicationContext, CameraActivity::class.java)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
startActivityAndCollapse(intent)
}
}
@@ -0,0 +1,37 @@
package de.markusfisch.android.binaryeye.view
import android.content.Context
import android.media.AudioManager
import de.markusfisch.android.binaryeye.app.prefs
import de.markusfisch.android.binaryeye.media.beepConfirm
import de.markusfisch.android.binaryeye.media.beepError
import de.markusfisch.android.binaryeye.os.error
import de.markusfisch.android.binaryeye.os.getVibrator
import de.markusfisch.android.binaryeye.os.vibrate
fun Context.scanFeedback() {
if (prefs.vibrate) {
getVibrator().vibrate()
}
if (prefs.beep && !isSilent()) {
beepConfirm()
}
}
fun Context.errorFeedback() {
if (prefs.vibrate) {
getVibrator().error()
}
if (!isSilent()) {
beepError()
}
}
private fun Context.isSilent(): Boolean {
val am = getSystemService(Context.AUDIO_SERVICE) as AudioManager
return when (am.ringerMode) {
AudioManager.RINGER_MODE_SILENT,
AudioManager.RINGER_MODE_VIBRATE -> true
else -> false
}
}
@@ -0,0 +1,13 @@
package de.markusfisch.android.binaryeye.view
import android.content.Context
import android.view.View
import android.view.inputmethod.InputMethodManager
fun Context.hideSoftKeyboard(view: View) {
(getSystemService(
Context.INPUT_METHOD_SERVICE
) as InputMethodManager).hideSoftInputFromWindow(
view.windowToken, 0
)
}
@@ -0,0 +1,131 @@
package de.markusfisch.android.binaryeye.view
import android.content.Context
import android.content.ContextWrapper
import android.graphics.drawable.ColorDrawable
import android.os.Build
import android.support.v4.content.ContextCompat
import android.support.v7.app.AppCompatActivity
import android.support.v7.widget.LinearLayoutManager
import android.support.v7.widget.RecyclerView
import android.view.View
import android.view.WindowManager
import android.widget.AbsListView
import de.markusfisch.android.binaryeye.R
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.
view.post {
val scrolled = firstVisibleItem > 0 ||
(totalItemCount > 0 && firstChildScrolled(view))
val scrollable = scrolled || totalItemCount > 0 && lastChildOutOfView(view)
colorSystemAndToolBars(view.context, scrolled, scrollable)
}
}
override fun onScrollStateChanged(view: AbsListView, scrollState: Int) {
}
}
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) {
}
}
private fun firstChildScrolled(listView: AbsListView): Boolean {
val child = listView.getChildAt(0)
return child != null && child.top < listView.paddingTop
}
private fun lastChildOutOfView(listView: AbsListView): Boolean {
val child = listView.getChildAt(listView.lastVisiblePosition)
return child != null && child.bottom >= listView.height
}
fun initSystemBars(activity: AppCompatActivity) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
// Keeps the soft keyboard from repositioning the layout.
val window = activity.window
window.setSoftInputMode(
WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE
)
window.decorView.systemUiVisibility =
View.SYSTEM_UI_FLAG_LAYOUT_STABLE or
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
}
colorSystemAndToolBars(activity)
}
private var statusBarColorLocked = false
fun lockStatusBarColor() {
statusBarColorLocked = true
}
fun unlockStatusBarColor() {
statusBarColorLocked = false
}
private var translucentPrimaryColor = 0
private val actionBarBackground = ColorDrawable()
fun colorSystemAndToolBars(
context: Context,
scrolled: Boolean = false,
scrollable: Boolean = false
) {
if (translucentPrimaryColor == 0) {
translucentPrimaryColor = ContextCompat.getColor(
context,
R.color.primary_translucent
)
}
val topColor = if (scrolled) translucentPrimaryColor else 0
val activity = getAppCompatActivity(context) ?: return
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
val window = activity.window
if (!statusBarColorLocked) {
window.statusBarColor = topColor
}
window.navigationBarColor = if (scrolled || scrollable) {
translucentPrimaryColor
} else {
0
}
}
activity.supportActionBar?.setBackgroundDrawable(
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
// Avoid allocation on Honeycomb and better.
actionBarBackground.color = topColor
actionBarBackground
} else {
// ColorDrawable.setColor() doesn't exist pre Honeycomb.
ColorDrawable(topColor)
}
)
}
private fun getAppCompatActivity(context: Context): AppCompatActivity? {
var ctx = context
while (ctx is ContextWrapper) {
if (ctx is AppCompatActivity) {
return ctx
}
ctx = ctx.baseContext ?: break
}
return null
}
@@ -0,0 +1,35 @@
package de.markusfisch.android.binaryeye.view
import android.graphics.Rect
import android.view.View
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
suspend inline fun View.useVisibility(
whileExecuting: Int = View.VISIBLE,
otherwise: Int = View.GONE,
crossinline block: suspend () -> Unit
) {
if (visibility == whileExecuting) {
return
}
withContext(Dispatchers.Main) {
visibility = whileExecuting
}
try {
block()
} finally {
withContext(Dispatchers.Main) {
visibility = otherwise
}
}
}
fun View.setPadding(rect: Rect) {
this.setPadding(
rect.left,
rect.top,
rect.right,
rect.bottom
)
}
@@ -0,0 +1,61 @@
package de.markusfisch.android.binaryeye.view
import android.graphics.Rect
import android.os.Build
import android.support.annotation.RequiresApi
import android.support.v4.view.ViewCompat
import android.support.v4.view.WindowInsetsCompat
import android.support.v7.widget.Toolbar
import android.view.View
private var toolbarHeight = 0
fun recordToolbarHeight(toolbar: Toolbar) {
toolbarHeight = toolbar.layoutParams.height
}
fun View.setPaddingFromWindowInsets() {
doOnApplyWindowInsets { v, insets -> v.setPadding(insets) }
}
// A slight variation of the idea from Google's Chris Banes to avoid
// adding the toolbar height for every view that needs to be inset.
// For the original post see here:
// https://medium.com/androiddevelopers/windowinsets-listeners-to-layouts-8f9ccc8fa4d1
fun View.doOnApplyWindowInsets(f: (View, Rect) -> Unit) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
f(this, insetsWithToolbar())
} else {
ViewCompat.setOnApplyWindowInsetsListener(this) { v, insets ->
f(v, insetsWithToolbar(insets))
insets
}
// It's important to explicitly request the insets (again) in
// case the view was created in Fragment.onCreateView() because
// setOnApplyWindowInsetsListener() won't fire when the view
// isn't attached.
requestApplyInsetsWhenAttached()
}
}
private fun insetsWithToolbar(insets: WindowInsetsCompat? = null) = Rect(
insets?.systemWindowInsetLeft ?: 0,
(insets?.systemWindowInsetTop ?: 0) + toolbarHeight,
insets?.systemWindowInsetRight ?: 0,
insets?.systemWindowInsetBottom ?: 0
)
@RequiresApi(Build.VERSION_CODES.KITKAT)
private fun View.requestApplyInsetsWhenAttached() {
if (isAttachedToWindow) {
ViewCompat.requestApplyInsets(this)
} else {
addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener {
override fun onViewAttachedToWindow(v: View) {
v.removeOnAttachStateChangeListener(this)
ViewCompat.requestApplyInsets(v)
}
override fun onViewDetachedFromWindow(v: View) = Unit
})
}
}
@@ -2,11 +2,14 @@ package de.markusfisch.android.binaryeye.widget
import android.content.Context
import android.graphics.Canvas
import android.graphics.Rect
import android.util.AttributeSet
import de.markusfisch.android.binaryeye.app.setSystemAndToolBarTransparency
import de.markusfisch.android.binaryeye.view.colorSystemAndToolBars
import de.markusfisch.android.scalingimageview.widget.ScalingImageView
open class ConfinedScalingImageView : ScalingImageView {
val insets = Rect()
constructor(context: Context, attrs: AttributeSet, defStyle: Int) :
super(context, attrs, defStyle)
@@ -15,6 +18,24 @@ open class ConfinedScalingImageView : ScalingImageView {
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
setSystemAndToolBarTransparency(context, !bounds.contains(mappedRect))
colorSystemAndToolBars(context, !bounds.contains(mappedRect))
}
override fun layoutImage(
changed: Boolean,
left: Int,
top: Int,
right: Int,
bottom: Int
) {
if (changed) {
setBounds(
(left + insets.left).toFloat(),
(top + insets.top).toFloat(),
(right - insets.right).toFloat(),
(bottom - insets.bottom).toFloat()
)
}
centerRemap()
}
}
@@ -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.setSystemAndToolBarTransparency
import de.markusfisch.android.binaryeye.view.colorSystemAndToolBars
class ConfinedScrollView : ScrollView {
private var scrollable = false
@@ -23,14 +23,14 @@ 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
setSystemAndToolBarTransparency(
colorSystemAndToolBars(
context,
false,
scrollY > 0,
scrollable
)
}
@@ -40,6 +40,6 @@ class ConfinedScrollView : ScrollView {
override fun onScrollChanged(x: Int, y: Int, oldx: Int, oldy: Int) {
super.onScrollChanged(x, y, oldx, oldy)
setSystemAndToolBarTransparency(context, y > 0, scrollable)
colorSystemAndToolBars(context, y > 0, scrollable)
}
}
@@ -1,97 +1,32 @@
package de.markusfisch.android.binaryeye.widget
import android.content.Context
import android.graphics.*
import android.support.v4.content.ContextCompat
import android.graphics.Canvas
import android.graphics.Rect
import android.graphics.RectF
import android.util.AttributeSet
import de.markusfisch.android.binaryeye.R
import de.markusfisch.android.binaryeye.app.windowInsets
import kotlin.math.roundToInt
class CropImageView(context: Context, attr: AttributeSet) :
ConfinedScalingImageView(context, attr) {
var onScan: (() -> Rect)? = null
var onScan: (() -> Unit)? = null
private val boundsPaint = Paint(Paint.ANTI_ALIAS_FLAG)
private val candidatePaint = Paint(Paint.ANTI_ALIAS_FLAG)
private val lastMappedRect = RectF()
private val dp = context.resources.displayMetrics.density
private val padding: Int = (dp * 24f).roundToInt()
private val onScanRunnable = Runnable {
onScan?.invoke()?.also {
candidateRect = it
invalidate()
}
}
private val onScanRunnable = Runnable { onScan?.invoke() }
private var candidateRect: Rect? = null
init {
boundsPaint.color = ContextCompat.getColor(context, R.color.crop_bound)
boundsPaint.style = Paint.Style.STROKE
boundsPaint.strokeWidth = dp * 2f
boundsPaint.pathEffect = DashPathEffect(floatArrayOf(dp * 10f, dp * 10f), 0f)
candidatePaint.color = ContextCompat.getColor(context, R.color.candidate)
candidatePaint.style = Paint.Style.FILL_AND_STROKE
candidatePaint.strokeWidth = dp * 3f
scaleType = ScaleType.CENTER_CROP
}
override fun onLayout(
changed: Boolean,
left: Int,
top: Int,
right: Int,
bottom: Int
) {
super.onLayout(changed, left, top, right, bottom)
setBoundsWithPadding(
left + windowInsets.left,
top + windowInsets.top,
right - windowInsets.right,
bottom - windowInsets.bottom
)
}
fun getBoundsRect(): RectF {
return bounds
}
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)
}
fun getBoundsRect() = Rect(
bounds.left.roundToInt(),
bounds.top.roundToInt(),
bounds.right.roundToInt(),
bounds.bottom.roundToInt()
)
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
canvas.drawRect(bounds, boundsPaint)
val rc = candidateRect
if (rc != null && rc.width() > 0) {
canvas.drawRect(rc, candidatePaint)
candidateRect = null
}
val mr = mappedRect ?: return
if (mr != lastMappedRect) {
removeCallbacks(onScanRunnable)
postDelayed(onScanRunnable, 500)
postDelayed(onScanRunnable, 300)
lastMappedRect.set(mr)
}
}
@@ -0,0 +1,420 @@
package de.markusfisch.android.binaryeye.widget
import android.annotation.SuppressLint
import android.content.Context
import android.graphics.*
import android.os.Build
import android.os.Parcel
import android.os.Parcelable
import android.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.getBitmapFromDrawable
import de.markusfisch.android.binaryeye.graphics.getDashedBorderPaint
import kotlin.math.abs
import kotlin.math.max
import kotlin.math.min
import kotlin.math.roundToInt
class DetectorView : View {
val coordinates = FloatArray(32)
val roi = Rect()
var onRoiChange: (() -> Unit)? = null
var onRoiChanged: (() -> Unit)? = null
private val currentOrientation = resources.configuration.orientation
private val invalidateRunnable: Runnable = Runnable {
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
)
private val handleXRadius = handleBitmap.width / 2
private val handleYRadius = handleBitmap.height / 2
private val handlePos = Point(-1, -1)
private val handleHome = Point()
private val center = Point()
private val touchDown = Point()
private val distToFull: Int
private val minMoveThresholdSq: Int
private val cornerRadius: Int
private val fabHeight: Int
private val padding: Int
private var coordinatesLast = 0
private var handleGrabbed = false
private var handleActive = false
private var minY = 0
private var maxY = 0
private var minDist = 0
private var shadeColor = 0
init {
val dp = context.resources.displayMetrics.density
dotRadius = 8f * dp
distToFull = (24f * dp).roundToInt()
val minMoveThreshold = (8f * dp).roundToInt()
minMoveThresholdSq = minMoveThreshold * minMoveThreshold
cornerRadius = (8f * dp).roundToInt()
fabHeight = (92f * dp).roundToInt()
padding = (20f * dp).roundToInt()
isSaveEnabled = true
}
constructor(context: Context, attrs: AttributeSet) :
super(context, attrs)
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) :
super(context, attrs, defStyleAttr)
fun saveCropHandlePos() {
val pos = getCropHandlePos()
prefs.cropHandleX = pos.x
prefs.cropHandleY = pos.y
prefs.cropHandleOrientation = currentOrientation
}
private fun getCropHandlePos() = if (handleActive) {
handlePos
} else {
Point(-1, -1)
}
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)
}
override fun onSaveInstanceState(): Parcelable? {
if (!handleActive) {
return super.onSaveInstanceState()
}
return SavedState(super.onSaveInstanceState()).apply {
savedHandlePos.set(getCropHandlePos())
savedOrientation = currentOrientation
}
}
override fun onRestoreInstanceState(state: Parcelable) {
super.onRestoreInstanceState(
if (state is SavedState) {
setCropHandlePos(
state.savedHandlePos.x,
state.savedHandlePos.y,
state.savedOrientation
)
state.superState
} else {
state
}
)
}
@SuppressLint("ClickableViewAccessibility")
override fun onTouchEvent(event: MotionEvent?): Boolean {
event ?: return super.onTouchEvent(event)
val x = event.x.roundToInt()
val y = event.y.roundToInt()
return when (event.actionMasked) {
MotionEvent.ACTION_DOWN -> {
if (prefs.showCropHandle) {
touchDown.set(x, y)
handleGrabbed = abs(x - handlePos.x) < handleXRadius &&
abs(y - handlePos.y) < handleYRadius
if (handleGrabbed) {
onRoiChange?.invoke()
}
handleGrabbed
} else {
false
}
}
MotionEvent.ACTION_MOVE -> {
if (handleGrabbed) {
handlePos.set(x, y)
handleActive = handleActive ||
distSq(handlePos, touchDown) > minMoveThresholdSq
if (handleActive) {
updateClipRect()
invalidate()
}
true
} else {
false
}
}
MotionEvent.ACTION_CANCEL -> {
if (handleGrabbed) {
snap(x, y)
handleGrabbed = false
}
false
}
MotionEvent.ACTION_UP -> {
if (handleGrabbed) {
if (!handleActive) {
setHandleToDefaultRoi()
} else {
snap(x, y)
}
if (handleActive) {
updateClipRect()
invalidate()
}
onRoiChanged?.invoke()
handleGrabbed = false
}
false
}
else -> super.onTouchEvent(event)
}
}
private fun setHandleToDefaultRoi() {
val mn = min(center.x, center.y) * .8f
handlePos.set(
(center.x + mn).roundToInt(),
(center.y + mn).roundToInt()
)
handleActive = true
}
private fun snap(x: Int, y: Int) {
val cx = clampX(x)
val cy = clampY(y)
val dx = abs(cx - center.x)
val dy = abs(cy - center.y)
// Check if handle is close to the vertical or horizontal center line.
if (dx < distToFull ||
dy < distToFull ||
// Check if handle is close to a screen corner.
((abs(cy - minY) < distToFull || abs(maxY - cy) < distToFull) &&
abs(dx - center.x) < distToFull)
) {
reset()
invalidate()
}
}
private fun reset() {
handlePos.set(handleHome)
handleActive = false
roi.set(0, 0, 0, 0)
}
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
super.onLayout(changed, left, top, right, bottom)
val width = right - left
val height = bottom - top
center.set(
left + (width / 2),
top + (height / 2)
)
minY = padding * 2
maxY = height - minY
handleHome.set(
width - handleXRadius - paddingRight - padding,
height - handleYRadius - paddingBottom - fabHeight
)
if (handlePos.x == -2) {
setHandleToDefaultRoi()
}
if (handleActive) {
updateClipRect()
} else {
reset()
}
}
override fun onDraw(canvas: Canvas) {
if (handleActive) {
canvas.drawClip()
}
canvas.drawDots()
if (prefs.showCropHandle) {
canvas.drawHandle()
}
}
private fun Canvas.drawDots() {
var i = 0
while (i < coordinatesLast) {
drawCircle(
coordinates[i++],
coordinates[i++],
dotRadius,
dotPaint
)
}
}
private fun Canvas.drawClip() {
if (minDist < 1) {
return
}
// canvas.clipRect() doesn't work reliably below KITKAT.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
val radius = min(minDist / 2, cornerRadius).toFloat()
save()
clipOutPathCompat(
calculateRoundedRectPath(
roi.left.toFloat(),
roi.top.toFloat(),
roi.right.toFloat(),
roi.bottom.toFloat(),
radius,
radius
)
)
drawColor(shadeColor)
restore()
} else {
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)
val dy = abs(handlePos.y - center.y)
minDist = min(dx, dy)
shadeColor = (min(
1f,
minDist.toFloat() / distToFull.toFloat()
) * 128f).toInt() shl 24
roi.set(
center.x - dx,
center.y - dy,
center.x + dx,
center.y + dy
)
}
private fun clampHandlePos() {
handlePos.x = clampX(handlePos.x)
handlePos.y = clampY(handlePos.y)
}
private fun clampX(x: Int) = min(center.x * 2, max(0, x))
private fun clampY(y: Int) = min(maxY, max(minY, y))
internal class SavedState : BaseSavedState {
val savedHandlePos = Point()
var savedOrientation = 0
constructor(superState: Parcelable?) : super(superState)
private constructor(parcel: Parcel) : super(parcel) {
savedHandlePos.set(
parcel.readInt(),
parcel.readInt()
)
savedOrientation = parcel.readInt()
}
override fun writeToParcel(out: Parcel, flags: Int) {
super.writeToParcel(out, flags)
out.writeInt(savedHandlePos.x)
out.writeInt(savedHandlePos.y)
out.writeInt(savedOrientation)
}
companion object {
@JvmField
val CREATOR = object : Parcelable.Creator<SavedState> {
override fun createFromParcel(source: Parcel) = SavedState(source)
override fun newArray(size: Int): Array<SavedState?> = arrayOfNulls(size)
}
}
}
}
private fun distSq(a: Point, b: Point): Int {
val dx = a.x - b.x
val dy = a.y - b.y
return dx * dx + dy * dy
}
private fun Point.set(point: Point) {
x = point.x
y = point.y
}
private fun Canvas.clipOutPathCompat(path: Path) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
clipOutPath(path)
} else {
@Suppress("DEPRECATION")
clipPath(path, Region.Op.DIFFERENCE)
}
}
private fun calculateRoundedRectPath(
left: Float,
top: Float,
right: Float,
bottom: Float,
rx: Float,
ry: Float
): Path {
val width = right - left
val height = bottom - top
val widthMinusCorners = width - 2 * rx
val heightMinusCorners = height - 2 * ry
return Path().apply {
moveTo(right, top + ry)
rQuadTo(0f, -ry, -rx, -ry)
rLineTo(-widthMinusCorners, 0f)
rQuadTo(-rx, 0f, -rx, ry)
rLineTo(0f, heightMinusCorners)
rQuadTo(0f, ry, rx, ry)
rLineTo(widthMinusCorners, 0f)
rQuadTo(rx, 0f, rx, -ry)
rLineTo(0f, -heightMinusCorners)
close()
}
}
@@ -0,0 +1,22 @@
package de.markusfisch.android.binaryeye.widget
import android.content.Context
import android.widget.Toast
fun Context.toast(message: Int) = Toast.makeText(
applicationContext,
message,
Toast.LENGTH_LONG
).show()
fun Context.toast(message: String) = Toast.makeText(
applicationContext,
message.ellipsize(128),
Toast.LENGTH_LONG
).show()
private fun String.ellipsize(max: Int) = if (length < max) {
this
} else {
"${take(max)}"
}
@@ -1,155 +0,0 @@
package de.markusfisch.android.binaryeye.zxing
import android.graphics.Bitmap
import com.google.zxing.*
import com.google.zxing.common.HybridBinarizer
import java.util.*
class Zxing {
private val multiFormatReader: 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
)
)
)
val hints = EnumMap<DecodeHintType, Any>(DecodeHintType::class.java)
hints[DecodeHintType.POSSIBLE_FORMATS] = decodeFormats
multiFormatReader.setHints(hints)
}
fun decode(
yuvData: ByteArray,
width: Int,
height: Int,
invert: Boolean = false
): Result? {
val source = PlanarYUVLuminanceSource(
yuvData,
width,
height,
0,
0,
width,
height,
false
)
return decodeLuminanceSource(source, invert)
}
fun decodePositiveNegative(bitmap: Bitmap): Result? {
val result = decode(bitmap, false)
if (result != null) {
return result
}
return decode(bitmap, true)
}
fun decode(bitmap: Bitmap, invert: Boolean = false): Result? {
val pixels = IntArray(bitmap.width * bitmap.height)
return decode(pixels, bitmap, invert)
}
fun decode(
pixels: IntArray,
bitmap: Bitmap,
invert: Boolean = false
): Result? {
val width = bitmap.width
val height = bitmap.height
if (bitmap.config != Bitmap.Config.ARGB_8888) {
bitmap.copy(Bitmap.Config.ARGB_8888, true)
} else {
bitmap
}.getPixels(pixels, 0, width, 0, 0, width, height)
val source = RGBLuminanceSource(width, height, pixels)
return decodeLuminanceSource(source, invert)
}
private fun decodeLuminanceSource(
source: LuminanceSource,
invert: Boolean
): Result? {
return decodeLuminanceSource(
if (invert) {
source.invert()
} else {
source
}
)
}
private fun decodeLuminanceSource(source: LuminanceSource): Result? {
val bitmap = BinaryBitmap(HybridBinarizer(source))
return try {
multiFormatReader.decodeWithState(bitmap)
} 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
}
}
}

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