Compare commits

...
338 Commits
Author SHA1 Message Date
Markus Fisch 4971f58f44 Advance version number to 1.60.3 2023-05-19 18:55:49 +02:00
m_s_andMarkus Fisch 0bf24080a8 Update Brazilian Portuguese translation 2023-05-19 18:49:51 +02:00
Markus Fisch 04a21f71ef Update Kotlin version 2023-05-11 20:37:39 +02:00
Unknownman820andGitHub e22a215863 Update Chinese translation 2023-05-04 12:10:51 +02:00
Markus Fisch 881829e7b2 Fix saving custom language
This preference needs to be committed so it is written to disk
immediately.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

This makes it easier to use a generated barcode in place of a small
printed barcode. There are scanners out there that expect a barcode
to be of a certain (small) size.
2022-11-08 20:28:41 +01:00
Markus Fisch c7c1e74240 Update to latest ZXing C++ wrapper
To fix handling of tab characters.
2022-11-08 20:27:23 +01:00
Markus Fisch c793e1399b Fix view/frame transformation matrix
The view offset needs to be translated first - not last, where the
matrix is no longer in view space.
2022-11-08 20:27:23 +01:00
zmniandGitHub c550a6ff51 Update indonesian translation 2022-11-08 20:27:00 +01:00
solokotandGitHub 05b0f719e0 Updated Russian translation 2022-11-07 18:22:34 +01:00
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
248 changed files with 6195 additions and 4696 deletions
+180
View File
@@ -1,5 +1,185 @@
# Change Log
## 1.60.3
* Fix saving custom language
* Update Brazilian Portuguese translation
* Update Chinese translation
* Update Czech localisation
* Update Russian translation
* Update Italian language
## 1.60.2
* Expand escape sequences in encoding input
* Update Taiwan translation
## 1.60.1
* Support sending scans via bluetooth
* Fix resolving error correction level for recreation
* Improve asset image compression
* Update ZXing C++
* Update Italian translation
* Update Russian translation
## 1.59.0
* Add setting error correction for AZTEC/PDF417
* Fix setting error correction level for QR Codes
* Make error beeps follow beep settings too
* Don't close the app when cameras is unavailable
## 1.58.2
* Fix parsing WiFi network suggestions
## 1.58.1
* Update zh-rCn translation
* Fix monochrome launcher icon
## 1.58.0
* Improve detection of inverted barcodes
* Optionally allow duplicates in bulk scan
* Add Danish translation
* Update Italian translation
* Update Japanese translation
* Update Russian translation
* Fix charset decoding for PDF 417
* Fix monochrome icon
## 1.57.0
* Add optional audio feedback for scanning
* Allow removing individual WiFi network suggestions
* Recognize incomplete and malformed WiFi barcodes
* Fix using shared text for encoding
* Add Korean language
* Update Chinese translation
* Update Português/Brasil translation
* Update Russian translation
## 1.56.3
* Fix parsing URLs in barcodes
* Update French, Portuguese, Turkish and Taiwan translation
## 1.56.2
* Fix generating 1D barcode text output
## 1.56.1
* Allow pinching a barcode to its generated size
* Fix view/frame transformation matrix
* Fix handling of tab characters
* Update Indonesian translation
* Update Russian translation
## 1.56.0
* Add an action item to lock/unlock free rotation
* Support setting fore and background colors when creating barcodes
* 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
+1 -1
View File
@@ -17,4 +17,4 @@ Then write a [good commit message][commit_messages] to keep the history
meaningful and useful. One feature, one commit.
[android_best_practices]: https://developer.android.com/distribute/best-practices/develop/
[commit_messages]: https://juffalow.com/other/write-good-git-commit-message
[commit_messages]: https://git-scm.com/book/en/v2/Distributed-Git-Contributing-to-a-Project#_commit_guidelines
-3
View File
@@ -20,9 +20,6 @@ infer: clean
test:
./gradlew test
cat:
./gradlew test cAT
testview:
adb shell am start -W -a android.intent.action.VIEW -d 'binaryeye://scan'
+32 -10
View File
@@ -7,33 +7,40 @@ hesitate to contact me.
## Permissions
### [ACCESS_FINE_LOCATION](https://developer.android.com/reference/android/Manifest.permission#ACCESS_FINE_LOCATION)
### [ACCESS_FINE_LOCATION][ACCESS_FINE_LOCATION]
Required to add a WiFi network from a barcode (usually a QR Code).
Before Android Q, this feature requires using
[WifiManager.getConfiguredNetworks][configuredNetworks]
[WifiManager.getConfiguredNetworks][getConfiguredNetworks]
which requires `ACCESS_FINE_LOCATION`.
On Android Q an better, this permission is not requested nor required.
### [CAMERA](https://developer.android.com/reference/android/Manifest.permission#CAMERA)
### [BLUETOOTH][BLUETOOTH], [BLUETOOTH_ADMIN][BLUETOOTH_ADMIN] and [BLUETOOTH_CONNECT][BLUETOOTH_CONNECT]
Required to optionally forward scans to a Bluetooth device.
[BLUETOOTH][BLUETOOTH] and [BLUETOOTH_ADMIN][BLUETOOTH_ADMIN] are required
before Android S. [BLUETOOTH_CONNECT][BLUETOOTH_CONNECT] from Android S on.
### [CAMERA][CAMERA]
Required to read a barcode from the camera image.
### [INTERNET](https://developer.android.com/reference/android/Manifest.permission#INTERNET)
### [INTERNET][INTERNET]
Required to send a scanned code to a custom URL you can specify in settings.
### [VIBRATE](https://developer.android.com/reference/android/Manifest.permission#VIBRATE)
### [VIBRATE][VIBRATE]
Required to vibrate on detection.
### [WRITE_EXTERNAL_STORAGE](https://developer.android.com/reference/android/Manifest.permission#WRITE_EXTERNAL_STORAGE)
### [WRITE_EXTERNAL_STORAGE][WRITE_EXTERNAL_STORAGE]
Required to save generated barcodes as files before Android Q.
### [CHANGE_WIFI_STATE](https://developer.android.com/reference/android/Manifest.permission#CHANGE_WIFI_STATE)
### [CHANGE_WIFI_STATE][CHANGE_WIFI_STATE]
Required to add a WiFi network from a barcode (usually a QR Code).
@@ -41,13 +48,28 @@ On Android Q and better, this feature requires using
[WifiManager.addNetworkSuggestions][addNetworkSuggestions]
which requires `CHANGE_WIFI_STATE`.
### [ACCESS_WIFI_STATE](https://developer.android.com/reference/android/Manifest.permission#ACCESS_FINE_LOCATION)
### [ACCESS_WIFI_STATE][ACCESS_FINE_LOCATION]
Required to add a WiFi network from a barcode (usually a QR Code).
Before Android Q, this feature requires using
[WifiManager.getConfiguredNetworks][configuredNetworks]
[WifiManager.getConfiguredNetworks][getConfiguredNetworks]
which requires `ACCESS_WIFI_STATE`.
[configuredNetworks]: https://developer.android.com/reference/android/net/wifi/WifiManager#getConfiguredNetworks()
From Android R, `ACCESS_WIFI_STATE` is required for
[WifiManager.getNetworkSuggestions][getNetworkSuggestions]
which is used to remove individual network suggestions.
[ACCESS_FINE_LOCATION]: https://developer.android.com/reference/android/Manifest.permission#ACCESS_FINE_LOCATION
[BLUETOOTH]: https://developer.android.com/reference/android/Manifest.permission#BLUETOOTH
[BLUETOOTH_ADMIN]: https://developer.android.com/reference/android/Manifest.permission#BLUETOOTH_ADMIN
[BLUETOOTH_CONNECT]: https://developer.android.com/reference/android/Manifest.permission#BLUETOOTH_CONNECT
[CAMERA]: https://developer.android.com/reference/android/Manifest.permission#CAMERA
[INTERNET]: https://developer.android.com/reference/android/Manifest.permission#INTERNET
[VIBRATE]: https://developer.android.com/reference/android/Manifest.permission#VIBRATE
[WRITE_EXTERNAL_STORAGE]: https://developer.android.com/reference/android/Manifest.permission#WRITE_EXTERNAL_STORAGE
[CHANGE_WIFI_STATE]: https://developer.android.com/reference/android/Manifest.permission#CHANGE_WIFI_STATE
[ACCESS_WIFI_STATE]: https://developer.android.com/reference/android/Manifest.permission#ACCESS_FINE_LOCATION
[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()
+48 -53
View File
@@ -7,8 +7,8 @@ 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][zxing] ("Zebra Crossing") barcode scanning
library.
Binary Eye uses the [ZXing-C++][zxing_cpp] ("Zebra Crossing") barcode
scanning library.
## Screenshots
@@ -39,10 +39,10 @@ ZXing can read the following barcode formats:
* [EAN 8][ean_8]
* [EAN 13][ean_13]
* [ITF][itf]
* [MAXICODE][maxicode] (only when unrotated and unskewed, see [77][77],
because of which Binary Eye *cannot* read this barcode)
* [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]
@@ -84,65 +84,58 @@ Supported symbols are:
* `RESULT` - scanned content
* `RESULT_BYTES` - raw result as a hex string
* `FORMAT` - barcode format
* `META` - the meta data, if available
### SCAN Intent
You can also use Binary Eye from other apps by using the
`com.google.zxing.client.android.SCAN` Intent with `startActivityForResult()`
like this:
`com.google.zxing.client.android.SCAN` Intent with
[startActivityForResult()][start_activity] like this:
startActivityForResult(
Intent("com.google.zxing.client.android.SCAN"),
SOME_NUMBER
)
```kotlin
startActivityForResult(
Intent("com.google.zxing.client.android.SCAN"),
SOME_NUMBER
)
```
And process the result in `onActivityResult()` of your `Activity`:
And process the result in [onActivityResult()][on_activity_result] of your
`Activity`:
override fun onActivityResult(
requestCode: Int,
resultCode: Int,
data: Intent?
) {
when (requestCode) {
SOME_NUMBER -> if (resultCode == RESULT_OK) {
val result = data.getStringExtra("SCAN_RESULT")
}
```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")
}
}
## RenderScript
This app uses [RenderScript][rs] to resize and rotate the camera image.
Unfortunately, RenderScript has some nasty gotchas.
### RenderScript.forceCompat()
It's necessary to call `RenderScript.forceCompat()` on some devices/roms.
`RenderScript.forceCompat()` needs to be run before any other RenderScript
function and unfortunately there is no way to know if invoking `forceCompat()`
is necessary or not.
If `RenderScript.forceCompat()` is necessary, a `RSRuntimeException` will
be thrown and the only option is to restart the app, this time with calling
`forceCompat()` first.
Calling `RenderScript.forceCompat()` means the processing is done in
software so you probably don't want to enable it by default.
### 2D barcodes
If you want to fork this and are only interested in reading 2D barcodes
(like QR or Aztec), you may want to remove the custom rotation kernel
altogether as ZXing can read 2D barcodes in any orientation.
This will make your app a bit simpler and saves you from compiling a
custom RenderScript kernel for each architecture you want to support.
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
@@ -156,9 +149,11 @@ custom RenderScript kernel for each architecture you want to support.
[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
[77]: https://github.com/markusfisch/BinaryEye/issues/77
[rs]: https://developer.android.com/guide/topics/renderscript/compute
[start_activity]: https://developer.android.com/reference/android/app/Activity#startActivityForResult(android.content.Intent,%20int)
[on_activity_result]: https://developer.android.com/reference/android/app/Activity#onActivityResult(int,%20int,%20android.content.Intent)
[intent_result]: https://developer.android.com/training/basics/intents/result
+9 -33
View File
@@ -2,27 +2,18 @@ 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 87
versionName '1.46.0'
// It's recommended to set this value to the lowest API level
// able to provide all the functionality.
renderscriptTargetApi 24
renderscriptSupportModeEnabled true
renderscriptSupportModeBlasEnabled true
testInstrumentationRunner 'android.support.test.runner.AndroidJUnitRunner'
versionCode 120
versionName '1.60.3'
}
signingConfigs {
config {
release {
keyAlias System.getenv('ANDROID_KEY_ALIAS')
keyPassword System.getenv('ANDROID_KEY_PASSWORD')
storePassword System.getenv('ANDROID_STORE_PASSWORD')
@@ -46,7 +37,7 @@ android {
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
minifyEnabled true
shrinkResources true
signingConfig signingConfigs.config
signingConfig signingConfigs.release
}
}
@@ -61,33 +52,18 @@ android {
enableSplit = false
}
}
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'
}
}
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"
implementation "com.android.support:preference-v7:$support_version"
implementation "com.android.support:preference-v14:$support_version"
// Do NOT upgrade ZXing because newer versions won't work on API < 24
// https://github.com/zxing/zxing/issues/1170
implementation 'com.google.zxing:core:3.3.3'
implementation 'com.github.markusfisch:CameraView:1.9.1'
implementation 'com.github.markusfisch:ScalingImageView:1.3.0'
implementation 'com.github.markusfisch:ScalingImageView:1.4.1'
implementation 'com.github.markusfisch:zxing-cpp:v2.0.0.1'
}
+10
View File
@@ -9,4 +9,14 @@
<issue id="UnusedResources">
<ignore path="*/res/rs/debug/raw/bc*" />
</issue>
<!-- Android 12 requires apps that request ACCESS_FINE_LOCATION
to also request ACCESS_COARSE_LOCATION. But this app doesn't
request nor requires ACCESS_FINE_LOCATION above SDK 28. So
this is a false positive. -->
<issue id="CoarseFineLocation" severity="ignore"/>
<!-- Androidx SplashScreen compat library would require minSdk 21,
make the app unnecessarily larger and Android 12 automatically
replaces our custom splash screen anyway, which is exactly what
we want. So there is no reason to complain. -->
<issue id="CustomSplashScreen" severity="ignore"/>
</lint>
+2 -1
View File
@@ -3,4 +3,5 @@
-dontwarn android.support.v8.**
-keep class android.support.v7.widget.SearchView { *; }
-keep class android.support.v8.renderscript.** { *; }
-keep class de.markusfisch.android.zxingcpp.** { *; }
Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

@@ -1,108 +0,0 @@
package de.markusfisch.android.binaryeye
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.support.test.InstrumentationRegistry
import android.support.test.runner.AndroidJUnit4
import android.support.v8.renderscript.RenderScript
import de.markusfisch.android.binaryeye.rs.Preprocessor
import de.markusfisch.android.binaryeye.zxing.Zxing
import org.junit.Assert.fail;
import org.junit.Test
import org.junit.runner.RunWith
import java.util.regex.Pattern
@RunWith(AndroidJUnit4::class)
class PreprocessorTest {
@Test
fun processSamples() {
val zxing = Zxing()
val assets = InstrumentationRegistry.getInstrumentation()
.context.assets
val pattern = Pattern.compile(
"[0-9]+-([0-9]+)deg.jpg"
)
val samples = assets.list("samples")
checkNotNull(samples) { "no samples found" }
for (sample in samples) {
val m = pattern.matcher(sample)
if (!m.find() || m.groupCount() < 1) {
fail("invalid sample: $sample")
}
val bitmap = BitmapFactory.decodeStream(
assets.open("samples/$sample")
)
val frameData = getNV21(bitmap)
val frameWidth = bitmap.width
val frameHeight = bitmap.height
val frameOrientation = m.group(1) ?: return
val rs = RenderScript.create(
InstrumentationRegistry.getTargetContext(),
)
val preprocessor = Preprocessor(
rs,
frameWidth,
frameHeight,
null
)
val outWidth: Int
val outHeight: Int
val orientation = frameOrientation.toInt()
if (orientation == 90 || orientation == 270) {
preprocessor.resizeAndRotate(frameData)
outWidth = preprocessor.outHeight
outHeight = preprocessor.outWidth
} else {
preprocessor.resizeOnly(frameData)
outWidth = preprocessor.outWidth
outHeight = preprocessor.outHeight
}
val result = zxing.decode(frameData, outWidth, outHeight)
preprocessor.destroy()
rs.destroy()
checkNotNull(result) { "no barcode found in $sample" }
}
}
}
fun getNV21(bitmap: Bitmap): ByteArray {
val width = bitmap.width
val height = bitmap.height
val argb = IntArray(width * height)
bitmap.getPixels(argb, 0, width, 0, 0, width, height)
return encodeYUV420SP(argb, width, height)
}
private fun encodeYUV420SP(argb: IntArray, width: Int, height: Int): ByteArray {
val yuv = ByteArray(ceilIfUneven(width) * ceilIfUneven(height) * 3 / 2)
var yIndex = 0
var uvIndex = width * height
var r: Int
var g: Int
var b: Int
var Y: Int
var u: Int
var v: Int
var index = 0
for (y in 0 until height) {
for (x in 0 until width) {
val pixel = argb[index]
r = pixel and 0xff0000 shr 16
g = pixel and 0xff00 shr 8
b = pixel and 0xff
Y = (66 * r + 129 * g + 25 * b + 128 shr 8) + 16
u = (-38 * r - 74 * g + 112 * b + 128 shr 8) + 128
v = (112 * r - 94 * g - 18 * b + 128 shr 8) + 128
yuv[yIndex++] = (if (Y < 0) 0 else if (Y > 255) 255 else Y).toByte()
if (y % 2 == 0 && index % 2 == 0) {
yuv[uvIndex++] = (if (v < 0) 0 else if (v > 255) 255 else v).toByte()
yuv[uvIndex++] = (if (u < 0) 0 else if (u > 255) 255 else u).toByte()
}
++index
}
}
return yuv
}
private fun ceilIfUneven(n: Int) = if (n and 1 == 1) n + 1 else n
@@ -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.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.5 KiB

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 8.2 KiB

+13 -16
View File
@@ -1,20 +1,23 @@
<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.BLUETOOTH"
android:maxSdkVersion="30"/>
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"
android:maxSdkVersion="30"/>
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT"/>
<uses-permission android:name="android.permission.CAMERA"/>
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.VIBRATE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="28"/>
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE"/>
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"
android:maxSdkVersion="28"/>
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
<uses-feature android:name="android.hardware.camera"/>
<uses-feature android:name="android.hardware.camera.autofocus"/>
<uses-sdk tools:overrideLibrary="android.support.v14.preference"/>
@@ -34,6 +37,7 @@
android:usesCleartextTraffic="true"
android:allowBackup="true"
android:fullBackupContent="@xml/backup_rules"
android:dataExtractionRules="@xml/extraction_rules"
android:supportsRtl="true"
android:resizeableActivity="true"
android:label="@string/app_name"
@@ -41,8 +45,8 @@
android:theme="@style/AppTheme">
<activity
android:name=".activity.SplashActivity"
android:label="@string/app_name"
android:theme="@style/SplashTheme">
android:theme="@style/SplashTheme"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
@@ -53,7 +57,8 @@
</activity>
<activity
android:name=".activity.CameraActivity"
android:label="@string/scan_code">
android:label="@string/scan_code"
android:exported="true">
<intent-filter android:label="@string/compose_barcode">
<action android:name="android.intent.action.SEND"/>
<category android:name="android.intent.category.DEFAULT"/>
@@ -63,15 +68,6 @@
<action android:name="com.google.zxing.client.android.SCAN"/>
<category android:name="android.intent.category.DEFAULT"/>
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>
<data android:scheme="http"
android:host="markusfisch.de"
android:pathPrefix="/BinaryEye"/>
<data android:scheme="https" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
@@ -82,6 +78,7 @@
<activity
android:name=".activity.PickActivity"
android:label="@string/pick_code_to_scan"
android:exported="true"
tools:ignore="AppLinkUrlError">
<intent-filter android:label="@string/decode_barcode">
<action android:name="android.intent.action.SEND"/>
@@ -96,12 +93,12 @@
</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"/>
@@ -1,6 +1,7 @@
package de.markusfisch.android.binaryeye.actions
import de.markusfisch.android.binaryeye.actions.mail.MailAction
import de.markusfisch.android.binaryeye.actions.mail.MatMsgAction
import de.markusfisch.android.binaryeye.actions.otpauth.OtpauthAction
import de.markusfisch.android.binaryeye.actions.search.OpenOrSearchAction
import de.markusfisch.android.binaryeye.actions.sms.SmsAction
@@ -15,13 +16,16 @@ object ActionRegistry {
private val REGISTRY: Set<IAction> = setOf(
MailAction,
MatMsgAction,
OtpauthAction,
SmsAction,
TelAction,
VCardAction,
VEventAction,
WebAction,
WifiAction
WifiAction,
// Try WebAction last because recognizing colloquial URLs is
// very aggressive.
WebAction
)
fun getAction(data: ByteArray): IAction = REGISTRY.find {
@@ -2,8 +2,8 @@ package de.markusfisch.android.binaryeye.actions
import android.content.Context
import android.content.Intent
import de.markusfisch.android.binaryeye.app.parseAndNormalizeUri
import de.markusfisch.android.binaryeye.content.execShareIntent
import de.markusfisch.android.binaryeye.content.openUrl
import de.markusfisch.android.binaryeye.widget.toast
interface IAction {
@@ -18,31 +18,38 @@ abstract class IntentAction : IAction {
abstract val errorMsg: Int
final override suspend fun execute(context: Context, data: ByteArray) {
val intent = createIntent(context, data) ?: return context.toast(
errorMsg
)
context.execShareIntent(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))
context.execShareIntent(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,14 +1,12 @@
package de.markusfisch.android.binaryeye.actions.search
import android.content.Context
import android.content.Intent
import de.markusfisch.android.binaryeye.R
import de.markusfisch.android.binaryeye.actions.IAction
import de.markusfisch.android.binaryeye.app.alertDialog
import de.markusfisch.android.binaryeye.app.parseAndNormalizeUri
import de.markusfisch.android.binaryeye.app.prefs
import de.markusfisch.android.binaryeye.content.startIntent
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
@@ -20,10 +18,13 @@ object OpenOrSearchAction : IAction {
view(context, String(data), true)
}
private suspend fun view(context: Context, s: String, search: Boolean) {
val intent = Intent(Intent.ACTION_VIEW, parseAndNormalizeUri(s))
if (!context.startIntent(intent) && search) {
openSearch(context, s)
private suspend fun view(
context: Context,
url: String,
search: Boolean
) {
if (!context.openUrl(url, silent = true) && search) {
openSearch(context, url)
}
}
@@ -32,7 +33,7 @@ object OpenOrSearchAction : IAction {
if (defaultSearchUrl.isNotEmpty()) {
view(
context,
defaultSearchUrl + URLEncoder.encode(query, "utf-8"),
defaultSearchUrl + query.urlEncode(),
false
)
return
@@ -54,7 +55,7 @@ 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
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()
)
}
@@ -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
@@ -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
@@ -45,38 +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
)
}
}
}
}
// We definitely don't wan't the local format.
@SuppressLint("SimpleDateFormat")
private val dateFormats = listOf(
SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"),
SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss"),
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)
}
}
@@ -10,12 +10,6 @@ import android.support.annotation.RequiresApi
import de.markusfisch.android.binaryeye.R
import java.util.*
@RequiresApi(Build.VERSION_CODES.Q)
fun removeNetworkSuggestions(context: Context): Int =
(context.applicationContext.getSystemService(
Context.WIFI_SERVICE
) as WifiManager).removeNetworkSuggestions(listOf())
/**
* Normal:
* WIFI:S:[network SSID];T:<WPA|WEP|nopass|>;P:[network password];H:<true|false|>;;
@@ -50,8 +44,10 @@ object WifiConnector {
passwordBlock?.apply {
invoke(parsedData.password)
}
return if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
// WifiConfiguration is deprecated in Android Q.
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 {
@@ -63,33 +59,23 @@ object WifiConnector {
val wifiManager = context.applicationContext.getSystemService(
Context.WIFI_SERVICE
) as WifiManager
return if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
// WifiConfiguration is deprecated in Android Q.
@Suppress("DEPRECATION")
val wifiConfig = config as WifiConfiguration
if (wifiManager.enableWifi() &&
wifiManager.removeOldNetwork(wifiConfig) &&
wifiManager.enableNewNetwork(wifiConfig)
) {
R.string.wifi_added
} else {
R.string.wifi_config_failed
}
// WifiConfiguration is deprecated in Android Q.
@Suppress("DEPRECATION")
return if ((config is WifiConfiguration &&
wifiManager.enableWifi() &&
wifiManager.removeOldNetwork(config) &&
wifiManager.enableNewNetwork(config)) ||
(Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q &&
config is WifiNetworkSuggestion.Builder &&
wifiManager.addNetworkFromBuilder(config))
) {
R.string.wifi_added
} else {
val suggestion = (config as WifiNetworkSuggestion.Builder).build()
val suggestions = listOf(suggestion)
// Remove previous conflicting network suggestion.
wifiManager.removeNetworkSuggestions(suggestions)
val result = wifiManager.addNetworkSuggestions(suggestions)
if (result == WifiManager.STATUS_NETWORK_SUGGESTIONS_SUCCESS) {
R.string.wifi_added
} else {
R.string.wifi_config_failed
}
R.string.wifi_config_failed
}
}
internal fun parseMap(string: String): Map<String, String>? {
internal fun parseMap(input: String): Map<String, String>? {
// Normally those codes should have the last semicolon, but many
// generators don't add it.
val wifiRegex = """^WIFI:((?:.+?:(?:[^\\;]|\\.)*;)+);?$""".toRegex()
@@ -97,7 +83,7 @@ object WifiConnector {
// and : because many QR Code creators don't escape properly.
val pairRegex = """(.+?):((?:[^\\;]|\\.)*);""".toRegex()
return wifiRegex.matchEntire(
string
input.trimAndTerminate()
)?.groupValues?.get(1)?.let { pairs ->
pairRegex.findAll(pairs).map { pair ->
pair.groupValues[1].uppercase(Locale.US) to pair.groupValues[2]
@@ -126,7 +112,7 @@ object WifiConnector {
requireSdk(Build.VERSION_CODES.JELLY_BEAN_MR2) {
WifiEnterpriseConfig.Eap.NONE
}
} else when (inputMap["E"]) {
} else when (inputMap["E"]?.uppercase()) {
"AKA" -> requireSdk(Build.VERSION_CODES.LOLLIPOP) { WifiEnterpriseConfig.Eap.AKA }
"AKA_PRIME" -> requireSdk(Build.VERSION_CODES.M) { WifiEnterpriseConfig.Eap.AKA_PRIME }
"NONE" -> requireSdk(Build.VERSION_CODES.JELLY_BEAN_MR2) { WifiEnterpriseConfig.Eap.NONE }
@@ -143,7 +129,7 @@ object WifiConnector {
requireSdk(Build.VERSION_CODES.JELLY_BEAN_MR2) {
WifiEnterpriseConfig.Phase2.NONE
}
} else when (inputMap["PH2"]) {
} else when (inputMap["PH2"]?.uppercase()) {
"AKA" -> requireSdk(Build.VERSION_CODES.O) { WifiEnterpriseConfig.Phase2.AKA }
"AKA_PRIME" -> requireSdk(Build.VERSION_CODES.O) { WifiEnterpriseConfig.Phase2.AKA_PRIME }
"GTC" -> requireSdk(Build.VERSION_CODES.JELLY_BEAN_MR2) { WifiEnterpriseConfig.Phase2.GTC }
@@ -179,7 +165,7 @@ object WifiConnector {
data: SimpleDataAccessor
): WifiNetworkSuggestion.Builder? {
try {
when (data.securityType) {
when (data.securityType.uppercase()) {
"WPA", "WPA2" -> {
data.password?.let { setWpa2Passphrase(it) }
}
@@ -240,8 +226,8 @@ object WifiConnector {
fun WifiConfiguration.applySecurity(
data: SimpleDataAccessor
): WifiConfiguration? {
when (data.securityType) {
"", "nopass" -> allowedKeyManagement.set(WifiConfiguration.KeyMgmt.NONE)
when (data.securityType.uppercase()) {
"", "NOPASS" -> allowedKeyManagement.set(WifiConfiguration.KeyMgmt.NONE)
"WEP" -> @Suppress("DEPRECATION") /* WEP as insecure */ {
passwordWithQuotes?.let {
wepKeys[0] = it
@@ -303,11 +289,32 @@ object WifiConnector {
}
}
@RequiresApi(Build.VERSION_CODES.Q)
private fun WifiManager.addNetworkFromBuilder(
builder: WifiNetworkSuggestion.Builder
): Boolean {
val suggestion = builder.build()
val suggestions = listOf(suggestion)
// Remove previous conflicting network suggestion.
removeNetworkSuggestions(suggestions)
val result = addNetworkSuggestions(suggestions)
return result == WifiManager.STATUS_NETWORK_SUGGESTIONS_SUCCESS
}
// Make sure the string ends with a semicolon.
private fun String.trimAndTerminate(): String {
var trimmed = trim()
if (trimmed.isNotEmpty() && trimmed.last() != ';') {
trimmed += ";"
}
return trimmed
}
// Keep possibility of wrongly unescaped \ by explicitly searching
// for special chars.
private val escapedRegex = """\\([\\;,":])""".toRegex()
private val String.unescape: String
get() = this.replace(escapedRegex) { escaped ->
get() = replace(escapedRegex) { escaped ->
escaped.groupValues[1]
}
@@ -1,85 +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.graphics.RectF
import android.hardware.Camera
import android.media.AudioManager
import android.media.ToneGenerator
import android.net.Uri
import android.os.Bundle
import android.os.Vibrator
import android.support.design.widget.FloatingActionButton
import android.support.v7.app.AppCompatActivity
import android.support.v7.widget.Toolbar
import android.support.v8.renderscript.RSRuntimeException
import android.support.v8.renderscript.RenderScript
import android.view.Menu
import android.view.MenuItem
import android.view.MotionEvent
import android.view.View
import android.widget.SeekBar
import com.google.zxing.Result
import com.google.zxing.ResultMetadataType
import de.markusfisch.android.binaryeye.R
import de.markusfisch.android.binaryeye.adapter.prettifyFormatName
import de.markusfisch.android.binaryeye.app.*
import de.markusfisch.android.binaryeye.bluetooth.sendBluetoothAsync
import de.markusfisch.android.binaryeye.content.copyToClipboard
import de.markusfisch.android.binaryeye.content.execShareIntent
import de.markusfisch.android.binaryeye.database.Scan
import de.markusfisch.android.binaryeye.graphics.getFrameToViewMatrix
import de.markusfisch.android.binaryeye.graphics.map
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.os.error
import de.markusfisch.android.binaryeye.os.vibrate
import de.markusfisch.android.binaryeye.rs.Preprocessor
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.binaryeye.zxing.Zxing
import de.markusfisch.android.cameraview.widget.CameraView
import java.net.URLEncoder
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 { point ->
point ?: return@Zxing
val matrix = getMappingMatrix() ?: return@Zxing
detectorView.post {
detectorView.update(
matrix.map(
arrayOf(point),
detectorView.coordinates
)
)
}
}
private val frameRoi = Rect()
private val matrix = Matrix()
private lateinit var rs: RenderScript
private lateinit var vibrator: Vibrator
private lateinit var cameraView: CameraView
private lateinit var detectorView: DetectorView
private lateinit var zoomBar: SeekBar
private lateinit var flashFab: FloatingActionButton
private var preprocessor: Preprocessor? = null
private var nativeMappingMatrix: Matrix? = null
private var rotatedMappingMatrix: Matrix? = null
private var recreatePreprocessor = false
private var rotate = false
private var invert = false
private var flash = false
private var 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
@@ -92,8 +81,7 @@ class CameraActivity : AppCompatActivity() {
PERMISSION_CAMERA -> if (grantResults.isNotEmpty() &&
grantResults[0] != PackageManager.PERMISSION_GRANTED
) {
toast(R.string.no_camera_no_fun)
finish()
toast(R.string.camera_error)
}
}
}
@@ -128,9 +116,6 @@ class CameraActivity : AppCompatActivity() {
// custom locale.
setTitle(R.string.scan_code)
rs = RenderScript.create(this)
vibrator = getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
initSystemBars(this)
setSupportActionBar(findViewById(R.id.toolbar) as Toolbar)
@@ -152,47 +137,53 @@ class CameraActivity : AppCompatActivity() {
override fun onDestroy() {
super.onDestroy()
resetPreProcessor()
fallbackBuffer = null
saveZoom()
saveCropHandlePos()
rs.destroy()
detectorView.saveCropHandlePos()
releaseToneGenerators()
}
override fun onResume() {
super.onResume()
System.gc()
zxing.updateHints(
prefs.tryHarder,
prefs.barcodeFormats
)
updateHints()
if (prefs.bulkMode && bulkMode != prefs.bulkMode) {
bulkMode = prefs.bulkMode
invalidateOptionsMenu()
ignoreNext = null
}
setReturnTarget(intent)
if (hasCameraPermission(this)) {
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
}
isReturnUrl(intent) -> intent?.data?.let {
returnUrlTemplate = it.getQueryParameter("ret")
intent?.dataString?.isReturnUrl() == true -> {
finishAfterShowingResult = true
returnUrlTemplate = intent.data?.getQueryParameter("ret")
}
}
}
private fun resetPreProcessor() {
preprocessor?.destroy()
preprocessor = null
}
private fun openCamera() {
cameraView.openAsync(
CameraView.findCameraId(
@@ -213,8 +204,6 @@ class CameraActivity : AppCompatActivity() {
private fun closeCamera() {
cameraView.close()
// Closing the camera will also shut off the flash.
flash = false
}
override fun onRestoreInstanceState(savedState: Bundle) {
@@ -223,6 +212,7 @@ class CameraActivity : AppCompatActivity() {
zoomBar.progress = savedState.getInt(ZOOM_LEVEL)
frontFacing = savedState.getBoolean(FRONT_FACING)
bulkMode = savedState.getBoolean(BULK_MODE)
restrictFormat = savedState.getString(RESTRICT_FORMAT)
}
override fun onSaveInstanceState(outState: Bundle) {
@@ -230,6 +220,7 @@ class CameraActivity : AppCompatActivity() {
outState.putInt(ZOOM_LEVEL, zoomBar.progress)
outState.putBoolean(FRONT_FACING, frontFacing)
outState.putBoolean(BULK_MODE, bulkMode)
outState.putString(RESTRICT_FORMAT, restrictFormat)
super.onSaveInstanceState(outState)
}
@@ -259,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
@@ -280,6 +271,10 @@ class CameraActivity : AppCompatActivity() {
ignoreNext = null
true
}
R.id.restrict_format -> {
showRestrictionDialog()
true
}
R.id.preferences -> {
startActivity(MainActivity.getPreferencesIntent(this))
true
@@ -302,6 +297,27 @@ class CameraActivity : AppCompatActivity() {
openCamera()
}
private fun showRestrictionDialog() {
val names = resources.getStringArray(
R.array.barcode_formats_names
).toMutableList()
val formats = resources.getStringArray(
R.array.barcode_formats_values
).toMutableList()
if (restrictFormat != null) {
names.add(0, getString(R.string.remove_restriction))
formats.add(0, null)
}
AlertDialog.Builder(this).apply {
setTitle(R.string.restrict_format)
setItems(names.toTypedArray()) { _, which ->
restrictFormat = formats[which]
updateHints()
}
show()
}
}
private fun openReadme() {
val intent = Intent(
Intent.ACTION_VIEW,
@@ -368,7 +384,7 @@ class CameraActivity : AppCompatActivity() {
override fun onConfigureParameters(
parameters: Camera.Parameters
) {
if (parameters.isZoomSupported) {
zoomBar.visibility = if (parameters.isZoomSupported) {
val max = parameters.maxZoom
if (zoomBar.max != max) {
zoomBar.max = max
@@ -376,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 {
@@ -394,27 +411,48 @@ class CameraActivity : AppCompatActivity() {
override fun onCameraError() {
this@CameraActivity.toast(R.string.camera_error)
finish()
}
override fun onCameraReady(camera: Camera) {
// Reset preprocessor to make sure it always fits the current
// frame orientation. Important for landscape to landscape
// orientation changes.
resetPreProcessor()
val frameWidth = cameraView.frameWidth
val frameHeight = cameraView.frameHeight
val frameOrientation = cameraView.frameOrientation
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,
maxNumberOfSymbols = 1
)
var useLocalAverage = false
camera.setPreviewCallback { frameData, _ ->
if (decoding) {
decodeFrame(
useLocalAverage = useLocalAverage xor true
ZxingCpp.readByteArray(
frameData,
frameWidth,
frameHeight,
frameOrientation
)?.let { 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 { results ->
val result = results.first()
if (result.text != ignoreNext) {
postResult(result)
decoding = false
@@ -441,7 +479,7 @@ class CameraActivity : AppCompatActivity() {
progress: Int,
fromUser: Boolean
) {
setZoom(progress)
cameraView.camera?.setZoom(progress)
}
override fun onStartTrackingTouch(seekBar: SeekBar) {}
@@ -452,16 +490,13 @@ class CameraActivity : AppCompatActivity() {
}
@Suppress("DEPRECATION")
private fun setZoom(zoom: Int) {
val camera: Camera? = cameraView.camera
camera?.let {
try {
val params = camera.parameters
params.zoom = zoom
camera.parameters = params
} catch (e: RuntimeException) {
// Ignore. There's nothing we can do.
}
private fun Camera.setZoom(zoom: Int) {
try {
val params = parameters
params.zoom = zoom
parameters = params
} catch (e: RuntimeException) {
// Ignore. There's nothing we can do.
}
}
@@ -486,25 +521,21 @@ class CameraActivity : AppCompatActivity() {
}
detectorView.onRoiChanged = {
decoding = true
recreatePreprocessor = true
updateFrameRoiAndMappingMatrix()
}
detectorView.setPaddingFromWindowInsets()
restoreCropHandlePos()
detectorView.restoreCropHandlePos()
}
private fun saveCropHandlePos() {
val pos = detectorView.getCropHandlePos()
prefs.cropHandleX = pos.x
prefs.cropHandleY = pos.y
prefs.cropHandleOrientation = detectorView.currentOrientation
}
private fun restoreCropHandlePos() {
detectorView.setCropHandlePos(
prefs.cropHandleX,
prefs.cropHandleY,
prefs.cropHandleOrientation
)
private fun updateFrameRoiAndMappingMatrix() {
val viewRect = cameraView.previewRect
val viewRoi = if (detectorView.roi.width() < 1) {
viewRect
} else {
detectorView.roi
}
frameRoi.setFrameRoi(frameMetrics, viewRect, viewRoi)
matrix.setFrameToView(frameMetrics, viewRect, viewRoi)
}
private fun updateFlashFab(unavailable: Boolean) {
@@ -519,179 +550,31 @@ class CameraActivity : AppCompatActivity() {
@Suppress("DEPRECATION")
private fun toggleTorchMode() {
val camera = cameraView.camera
val parameters = camera?.parameters
parameters?.flashMode = if (flash) {
val camera = cameraView.camera ?: return
val parameters = camera.parameters ?: return
parameters.flashMode = if (
parameters.flashMode != Camera.Parameters.FLASH_MODE_OFF
) {
Camera.Parameters.FLASH_MODE_OFF
} else {
Camera.Parameters.FLASH_MODE_TORCH
}
parameters?.let {
try {
camera.parameters = parameters
flash = flash xor true
} catch (e: RuntimeException) {
// Ignore. There's nothing we can do.
}
}
}
private fun decodeFrame(
frameData: ByteArray?,
frameWidth: Int,
frameHeight: Int,
frameOrientation: Int
): Result? {
frameData ?: return null
rotate = if (prefs.autoRotate) {
rotate xor true
} else {
invert = invert xor true
isPortrait(frameOrientation)
}
return try {
if (recreatePreprocessor) {
resetPreProcessor()
recreatePreprocessor = false
}
val pp = preprocessor ?: createPreprocessorAndMapping(
frameWidth,
frameHeight,
frameOrientation
)
val w: Int
val h: Int
if (rotate) {
pp.resizeAndRotate(frameData)
w = pp.outHeight
h = pp.outWidth
} else {
invert = invert xor true
pp.resizeOnly(frameData)
w = pp.outWidth
h = pp.outHeight
}
preprocessor = pp
zxing.decode(frameData, w, h, invert)
} catch (e: RSRuntimeException) {
prefs.forceCompat = prefs.forceCompat xor true
// Now the only option is to restart the app because
// RenderScript.forceCompat() needs to be called before
// RenderScript is initialized.
restartApp(this)
null
}
}
private fun createPreprocessorAndMapping(
frameWidth: Int,
frameHeight: Int,
frameOrientation: Int
): Preprocessor {
val frameRoi = calculateFrameRect(
frameWidth,
frameHeight,
frameOrientation
)
val pp = Preprocessor(
rs,
frameWidth,
frameHeight,
frameRoi
)
val viewRect = if (frameRoi != null) {
detectorView.roi
} else {
cameraView.previewRect
}
nativeMappingMatrix = getFrameToViewMatrix(
pp.outWidth,
pp.outHeight,
frameOrientation,
viewRect
)
rotatedMappingMatrix = getFrameToViewMatrix(
pp.outHeight,
pp.outWidth,
(frameOrientation - 90 + 360) % 360,
viewRect
)
return pp
}
private fun calculateFrameRect(
frameWidth: Int,
frameHeight: Int,
frameOrientation: Int
): Rect? {
val viewRoi = detectorView.roi
return if (
viewRoi.width() == 0 ||
viewRoi.height() == 0
) {
null
} else {
// Map ROI in detectorView to cameraView.
val previewRect = cameraView.previewRect
// previewRect may be larger than the screen (and thus as the
// detectorView) in which case its left and/or top coordinate
// will be negative which must then be clamped to the
// screen/DetectorView.
val previewLeft = max(0, previewRect.left)
val previewTop = max(0, previewRect.top)
val previewRoi = Rect(
previewLeft + viewRoi.left,
previewTop + viewRoi.top,
previewLeft + viewRoi.right,
previewTop + viewRoi.bottom
)
val previewRectWidth = previewRect.width().toFloat()
val previewRectHeight = previewRect.height().toFloat()
val normalizedRoi = RectF(
previewRoi.left.toFloat() / previewRectWidth,
previewRoi.top.toFloat() / previewRectHeight,
previewRoi.right.toFloat() / previewRectWidth,
previewRoi.bottom.toFloat() / previewRectHeight
)
// Since the ROI is always centered and symmetric, we don't
// need to distinguish between 0 and 180 or 90 and 270 degree.
val errectedRoi = if (isPortrait(frameOrientation)) {
RectF(
normalizedRoi.top,
normalizedRoi.left,
normalizedRoi.bottom,
normalizedRoi.right
)
} else {
normalizedRoi
}
Rect(
(errectedRoi.left * frameWidth.toFloat()).roundToInt(),
(errectedRoi.top * frameHeight.toFloat()).roundToInt(),
(errectedRoi.right * frameWidth.toFloat()).roundToInt(),
(errectedRoi.bottom * frameHeight.toFloat()).roundToInt()
)
try {
camera.parameters = parameters
} catch (e: RuntimeException) {
toast(e.message ?: getString(R.string.error_flash))
}
}
private fun postResult(result: Result) {
// Get matrix for the current rotate value.
val matrix = getMappingMatrix()
val resultPoints = result.resultPoints
if (matrix == null ||
resultPoints == null ||
resultPoints.isEmpty()
) {
return
}
cameraView.post {
detectorView.update(
matrix.map(
resultPoints,
matrix.mapPosition(
result.position,
detectorView.coordinates
)
)
vibrator.vibrate()
scanFeedback()
val returnUri = returnUrlTemplate?.let {
try {
completeUrl(it, result)
@@ -710,15 +593,20 @@ class CameraActivity : AppCompatActivity() {
returnUri != null -> execShareIntent(
Intent(Intent.ACTION_VIEW, returnUri)
)
else -> showResult(
this@CameraActivity,
result,
vibrator,
bulkMode
)
else -> {
showResult(result, bulkMode)
// If this app was invoked via a deep link but without
// a return URI, we probably don't want to return to
// the camera screen after scanning, but to the caller.
if (finishAfterShowingResult) {
finish()
}
}
}
if (bulkMode) {
ignoreNext = result.text
if (prefs.ignoreConsecutiveDuplicates) {
ignoreNext = result.text
}
if (prefs.showToastInBulkMode) {
toast(result.text)
}
@@ -729,135 +617,100 @@ class CameraActivity : AppCompatActivity() {
}
}
private fun getMappingMatrix() = if (rotate) {
rotatedMappingMatrix
} else {
nativeMappingMatrix
}
companion object {
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,
fun Activity.showResult(
result: Result,
vibrator: Vibrator,
bulkMode: Boolean = false,
) {
if (prefs.copyImmediately) {
activity.copyToClipboard(result.text)
copyToClipboard(result.text)
}
val scan = Scan(result)
if (prefs.sendScanUrl.isNotEmpty()) {
val scan = result.toScan()
if (prefs.useHistory) {
scan.id = db.insertScan(scan)
}
if (prefs.sendScanActive && prefs.sendScanUrl.isNotEmpty()) {
if (prefs.sendScanType == "4") {
openUrl(
prefs.sendScanUrl + scan.content.urlEncode()
)
return
}
scan.sendAsync(
prefs.sendScanUrl,
prefs.sendScanType
) { code, body ->
if (code == null || code < 200 || code > 299) {
vibrator.error()
beepBeepBeep()
errorFeedback()
}
if (body != null && body.isNotEmpty()) {
activity.toast(body)
toast(body)
} else if (code == null || code > 299) {
activity.toast(R.string.background_request_failed)
toast(R.string.background_request_failed)
}
}
}
if (prefs.useHistory) {
scan.id = db.insertScan(scan)
if (prefs.sendScanBluetooth &&
prefs.sendScanBluetoothHost.isNotEmpty() &&
hasBluetoothPermission()
) {
scan.sendBluetoothAsync(
prefs.sendScanBluetoothHost
) { connected, sent ->
toast(
when {
!connected -> {
errorFeedback()
R.string.bluetooth_connect_fail
}
!sent -> {
errorFeedback()
R.string.bluetooth_send_fail
}
else -> R.string.bluetooth_send_success
}
)
}
}
if (!bulkMode) {
activity.startActivity(
MainActivity.getDecodeIntent(activity, scan)
startActivity(
MainActivity.getDecodeIntent(this, scan)
)
}
}
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)
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)
}
result.resultMetadata?.let { metadata ->
metadata[ResultMetadataType.UPC_EAN_EXTENSION]?.let {
intent.putExtra(
"SCAN_RESULT_ORIENTATION",
it.toString()
)
}
metadata[ResultMetadataType.ERROR_CORRECTION_LEVEL]?.let {
intent.putExtra(
"SCAN_RESULT_ERROR_CORRECTION_LEVEL",
it.toString()
)
}
metadata[ResultMetadataType.UPC_EAN_EXTENSION]?.let {
intent.putExtra(
"SCAN_RESULT_UPC_EAN_EXTENSION",
it.toString()
)
}
metadata[ResultMetadataType.BYTE_SEGMENTS]?.let {
var i = 0
@Suppress("UNCHECKED_CAST")
for (seg in it as Iterable<ByteArray>) {
intent.putExtra("SCAN_RESULT_BYTE_SEGMENTS_$i", seg)
++i
}
}
}
return intent
}
private fun isReturnUrl(intent: Intent?): Boolean {
val dataString = intent?.dataString
return listOf(
"binaryeye://scan",
"http://markusfisch.de/BinaryEye",
"https://markusfisch.de/BinaryEye"
).firstOrNull {
val result = dataString?.startsWith(it) == true
result
} != null
}
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}", URLEncoder.encode(result.text, "UTF-8"))
.replace("{RESULT_BYTES}", result.rawBytes?.toHexString() ?: "")
.replace("{RESULT}", result.text.urlEncode())
.replace("{RESULT_BYTES}", result.rawBytes.toHexString())
.replace(
"{FORMAT}", URLEncoder.encode(
result.barcodeFormat.toString(), "UTF-8"
)
)
.replace(
"{META}", URLEncoder.encode(
result.resultMetadata?.toString() ?: "", "UTF-8"
)
"{FORMAT}", result.format.urlEncode()
)
// And support {CODE} from the old ZXing app, too.
.replace("{CODE}", URLEncoder.encode(result.text, "UTF-8"))
.replace("{CODE}", result.text.urlEncode())
)
private fun beepBeepBeep() {
ToneGenerator(AudioManager.STREAM_ALARM, 100).startTone(
ToneGenerator.TONE_CDMA_CONFIRM,
1000
)
}
private fun isPortrait(
orientation: Int
) = orientation == 90 || orientation == 270
@@ -65,7 +65,7 @@ class MainActivity : AppCompatActivity() {
}
if (state == null) {
setFragment(supportFragmentManager, getFragmentForIntent(intent))
supportFragmentManager?.setFragment(getFragmentForIntent(intent))
}
}
@@ -81,7 +81,7 @@ class MainActivity : AppCompatActivity() {
intent.hasExtra(PREFERENCES) -> PreferencesFragment()
intent.hasExtra(HISTORY) -> HistoryFragment()
intent.hasExtra(ENCODE) -> EncodeFragment.newInstance(
intent.getStringExtra(ENCODE)!!
intent.getStringExtra(ENCODE)
)
intent.hasExtra(DECODED) -> DecodeFragment.newInstance(
intent.getParcelableExtra(DECODED)!!
@@ -3,45 +3,53 @@ 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.os.Vibrator
import android.support.v7.app.AppCompatActivity
import android.support.v7.widget.Toolbar
import android.support.v8.renderscript.RenderScript
import android.view.Menu
import android.view.MenuItem
import com.google.zxing.Result
import de.markusfisch.android.binaryeye.R
import de.markusfisch.android.binaryeye.app.*
import de.markusfisch.android.binaryeye.app.applyLocale
import de.markusfisch.android.binaryeye.app.prefs
import de.markusfisch.android.binaryeye.graphics.crop
import de.markusfisch.android.binaryeye.graphics.fixTransparency
import de.markusfisch.android.binaryeye.graphics.loadImageUri
import de.markusfisch.android.binaryeye.graphics.mapResultToView
import de.markusfisch.android.binaryeye.os.vibrate
import de.markusfisch.android.binaryeye.rs.fixTransparency
import de.markusfisch.android.binaryeye.view.colorSystemAndToolBars
import de.markusfisch.android.binaryeye.view.initSystemBars
import de.markusfisch.android.binaryeye.view.recordToolbarHeight
import de.markusfisch.android.binaryeye.view.setPaddingFromWindowInsets
import de.markusfisch.android.binaryeye.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.widget.DetectorView
import de.markusfisch.android.binaryeye.widget.toast
import de.markusfisch.android.binaryeye.zxing.Zxing
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
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,
maxNumberOfSymbols = 1,
formats = prefs.barcodeFormats.joinToString()
)
private lateinit var rs: RenderScript
private lateinit var vibrator: Vibrator
private lateinit var cropImageView: CropImageView
private lateinit var detectorView: DetectorView
private lateinit var freeRotationItem: MenuItem
private var result: Result? = null
@@ -58,13 +66,6 @@ class PickActivity : AppCompatActivity() {
// locale.
setTitle(R.string.pick_code_to_scan)
zxing.updateHints(
true,
prefs.barcodeFormats
)
rs = RenderScript.create(this)
vibrator = getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
initSystemBars(this)
val toolbar = findViewById(R.id.toolbar) as Toolbar
recordToolbarHeight(toolbar)
@@ -74,11 +75,7 @@ class PickActivity : AppCompatActivity() {
colorSystemAndToolBars(this@PickActivity)
}
val bitmap = fixTransparency(
rs,
getBitmapFromIntent()
)
val bitmap = getBitmapFromIntent()?.fixTransparency()
if (bitmap == null) {
applicationContext.toast(R.string.error_no_content)
finish()
@@ -87,6 +84,7 @@ class PickActivity : AppCompatActivity() {
cropImageView = findViewById(R.id.image) as CropImageView
cropImageView.restrictTranslation = false
cropImageView.freeRotation = prefs.freeRotation
cropImageView.setImageBitmap(bitmap)
cropImageView.onScan = {
scanWithinBounds(bitmap)
@@ -97,6 +95,7 @@ class PickActivity : AppCompatActivity() {
scanWithinBounds(bitmap)
}
detectorView.setPaddingFromWindowInsets()
detectorView.restoreCropHandlePos()
findViewById(R.id.scan).setOnClickListener {
showResult()
@@ -118,37 +117,45 @@ class PickActivity : AppCompatActivity() {
}
private fun scanWithinBounds(bitmap: Bitmap) {
val roi = detectorView.roi
val rectInView = if (roi.width() < 1) {
val viewRoi = if (detectorView.roi.width() < 1) {
cropImageView.getBoundsRect()
} else {
roi
detectorView.roi
}
val imageRect = cropImageView.mappedRect
val rectInImage = normalizeRoi(imageRect, rectInView)
val cropped = crop(
bitmap,
rectInImage,
cropImageView.imageRotation
val mappedRect = cropImageView.mappedRect
val cropped = bitmap.crop(
getNormalizedRoi(mappedRect, viewRoi),
cropImageView.imageRotation,
cropImageView.pivotX,
cropImageView.pivotY
) ?: return
CoroutineScope(Dispatchers.IO).launch(Dispatchers.IO) {
result = zxing.decodePositiveNegative(cropped)
result?.let {
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()?.first()?.let {
withContext(Dispatchers.Main) {
if (isFinishing) {
return@withContext
}
vibrator.vibrate()
val rc = Rect()
imageRect.round(rc)
rectInView.intersect(rc)
result = it
scanFeedback()
detectorView.update(
mapResultToView(
cropped.width,
cropped.height,
0,
rectInView,
it.resultPoints,
matrix.mapPosition(
it.position,
detectorView.coordinates
)
)
@@ -157,13 +164,38 @@ class PickActivity : AppCompatActivity() {
}
}
// By default, ZXing uses LOCAL_AVERAGE, but this does not work
// well with inverted barcodes on low-contrast backgrounds.
private fun Bitmap.decode() = ZxingCpp.readBitmap(
this,
0, 0,
width, height,
0,
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()
rs.destroy()
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
}
@@ -173,10 +205,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
}
@@ -185,25 +233,26 @@ class PickActivity : AppCompatActivity() {
val uri = intent.getParcelableExtra<Parcelable>(
Intent.EXTRA_STREAM
) as? Uri ?: return null
return loadImageUri(contentResolver, uri)
return contentResolver.loadImageUri(uri)
}
private fun loadImageToOpen(intent: Intent): Bitmap? {
val uri = intent.data ?: return null
return loadImageUri(contentResolver, uri)
return contentResolver.loadImageUri(uri)
}
private fun showResult() {
result?.let {
showResult(this, it, vibrator)
val r = result
if (r != null) {
showResult(r)
finish()
return
} else {
applicationContext.toast(R.string.no_barcode_found)
}
applicationContext.toast(R.string.no_barcode_found)
}
}
private fun normalizeRoi(imageRect: RectF, roi: Rect): RectF {
private fun getNormalizedRoi(imageRect: RectF, roi: Rect): RectF {
val w = imageRect.width()
val h = imageRect.height()
return RectF(
@@ -3,7 +3,6 @@ package de.markusfisch.android.binaryeye.activity
import android.content.Intent
import android.os.Bundle
import android.support.v7.app.AppCompatActivity
import de.markusfisch.android.binaryeye.app.setRestartCount
class SplashActivity : AppCompatActivity() {
override fun onCreate(state: Bundle?) {
@@ -13,7 +12,6 @@ class SplashActivity : AppCompatActivity() {
// because that would happen after the app is fully
// initialized what is too late.
setRestartCount(intent)
startActivity(Intent(applicationContext, CameraActivity::class.java))
finish()
}
@@ -2,6 +2,7 @@ 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
@@ -10,41 +11,71 @@ import android.widget.TextView
import de.markusfisch.android.binaryeye.R
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) {
var selectedScanId = 0L
var selectedScanPosition = -1
private val idIndex = cursor.getColumnIndex(Database.SCANS_ID)
private val timeIndex = cursor.getColumnIndex(Database.SCANS_DATETIME)
private val nameIndex = cursor.getColumnIndex(Database.SCANS_NAME)
private val contentIndex = cursor.getColumnIndex(Database.SCANS_CONTENT)
private val formatIndex = cursor.getColumnIndex(Database.SCANS_FORMAT)
private val selections = mutableMapOf<Long, Int>()
private val selectedColor = ContextCompat.getColor(
context, R.color.selected_row
)
fun select(id: Long, position: Int) {
selectedScanId = id
selectedScanPosition = position
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() {
selectedScanId = 0L
selectedScanPosition = -1
selections.clear()
}
fun getSelectedContent() = if (selectedScanPosition < 0) {
null
} else {
val cursor = getItem(selectedScanPosition)
if (cursor is Cursor) {
cursor.getString(contentIndex)
} else {
null
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,
@@ -58,38 +89,45 @@ class ScansAdapter(context: Context, cursor: Cursor) :
context: Context,
cursor: Cursor
) {
val holder = getViewHolder(view)
holder.timeView.text = formatDateTime(cursor.getString(timeIndex))
val name = cursor.getString(nameIndex)
val content = cursor.getString(contentIndex)
var icon = 0
holder.contentView.text = when {
name?.isNotEmpty() == true -> {
icon = R.drawable.ic_label
name
getViewHolder(view).apply {
timeView.text = formatDateTime(cursor.getString(timeIndex))
val name = cursor.getString(nameIndex)
val content = cursor.getString(contentIndex)
var icon = 0
contentView.text = when {
name?.isNotEmpty() == true -> {
icon = R.drawable.ic_label
name
}
content?.isEmpty() == true -> context.getString(
R.string.binary_data
)
else -> content
}
content?.isEmpty() == true -> context.getString(R.string.binary_data)
else -> content
contentView.setCompoundDrawablesWithIntrinsicBounds(
icon, 0, 0, 0
)
formatView.text = prettifyFormatName(cursor.getString(formatIndex))
}
holder.contentView.setCompoundDrawablesWithIntrinsicBounds(
icon, 0, 0, 0
)
holder.formatView.text = prettifyFormatName(cursor.getString(formatIndex))
// view.isSelected needs to be put on the queue to work.
val selected = cursor.getLong(idIndex) == selectedScanId
val selected = selections[cursor.getLong(idIndex)] != null
view.post {
view.isSelected = selected
// Needs to be put on the queue to work.
view.select(selected)
}
}
private fun getViewHolder(view: View): ViewHolder {
return view.tag as ViewHolder? ?: ViewHolder(
view.findViewById(R.id.time),
view.findViewById(R.id.content),
view.findViewById(R.id.format)
).also {
view.tag = it
}
private fun 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(
@@ -102,11 +140,15 @@ class ScansAdapter(context: Context, cursor: Cursor) :
fun prettifyFormatName(format: String) = format.replace("_", " ")
private fun formatDateTime(rfc: String): String {
SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.US).parse(rfc)?.let {
return DateFormat.getDateTimeInstance(
DateFormat.LONG,
DateFormat.SHORT
).format(it)
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
}
@@ -1,7 +1,7 @@
package de.markusfisch.android.binaryeye.app
import android.app.AlertDialog
import android.content.Context
import android.support.v7.app.AlertDialog
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlin.coroutines.resume
@@ -1,7 +1,6 @@
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
@@ -13,9 +12,5 @@ class BinaryEyeApp : Application() {
super.onCreate()
db.open(this)
prefs.init(this)
if (prefs.forceCompat) {
RenderScript.forceCompat()
}
}
}
@@ -1,14 +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
}
fun ByteArray.toHexString() = joinToString("") { "%02X".format(it) }
@@ -5,16 +5,15 @@ import android.support.v4.app.Fragment
import android.support.v4.app.FragmentManager
import de.markusfisch.android.binaryeye.R
fun setFragment(fm: FragmentManager?, fragment: Fragment) {
fm?.let { getTransaction(fm, fragment).commit() }
fun FragmentManager.setFragment(fragment: Fragment) {
getTransaction(fragment).commit()
}
fun addFragment(fm: FragmentManager?, fragment: Fragment) {
fm?.let { getTransaction(fm, fragment).addToBackStack(null).commit() }
fun FragmentManager.addFragment(fragment: Fragment) {
getTransaction(fragment).addToBackStack(null).commit()
}
@SuppressLint("CommitTransaction")
private fun getTransaction(
fm: FragmentManager,
private fun FragmentManager.getTransaction(
fragment: Fragment
) = fm.beginTransaction().replace(R.id.content_frame, fragment)
) = beginTransaction().replace(R.id.content_frame, fragment)
@@ -1,29 +0,0 @@
package de.markusfisch.android.binaryeye.app
import java.util.regex.Pattern
private val urlPattern: Pattern = Pattern.compile(
"(?:^|[\\W])((ht|f)tp(s?):\\/\\/|www\\.)" +
"(([\\w\\-]+\\.){1,}?([\\w\\-.~]+\\/?)*" +
"[\\p{Alnum}.,%_=?&#\\-+()\\[\\]\\*$~@!:/{};']*)",
Pattern.CASE_INSENSITIVE or Pattern.MULTILINE or Pattern.DOTALL
)
fun getLinks(text: String): List<String> {
val matcher = urlPattern.matcher(text)
val urls = mutableListOf<String>()
while (matcher.find()) {
val matchStart = matcher.start(1)
val matchEnd = matcher.end()
var url = text.substring(matchStart, matchEnd)
if (!url.startsWith("http://") && !url.startsWith("https://")) {
url = "https://$url"
}
urls.add(url)
}
return urls
}
@@ -3,49 +3,58 @@ package de.markusfisch.android.binaryeye.app
import android.Manifest
import android.app.Activity
import android.content.pm.PackageManager
import android.os.Build
import android.support.v4.app.ActivityCompat
import android.support.v4.content.ContextCompat
var permissionGrantedCallback: (() -> Any)? = null
const val PERMISSION_CAMERA = 1
fun hasCameraPermission(activity: Activity) = hasPermission(
activity,
fun Activity.hasCameraPermission() = hasPermission(
Manifest.permission.CAMERA,
PERMISSION_CAMERA
)
const val PERMISSION_WRITE = 2
fun hasWritePermission(activity: Activity, callback: () -> Any): Boolean {
fun Activity.hasWritePermission(callback: () -> Any): Boolean {
permissionGrantedCallback = callback
return hasPermission(
activity,
Manifest.permission.WRITE_EXTERNAL_STORAGE,
PERMISSION_WRITE
)
}
const val PERMISSION_LOCATION = 3
fun hasLocationPermission(activity: Activity, callback: () -> Any): Boolean {
fun Activity.hasLocationPermission(callback: () -> Any): Boolean {
permissionGrantedCallback = callback
return hasPermission(
activity,
Manifest.permission.ACCESS_FINE_LOCATION,
PERMISSION_LOCATION
)
}
private fun hasPermission(
activity: Activity,
const val PERMISSION_BLUETOOTH = 4
fun Activity.hasBluetoothPermission() = if (
Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
) {
hasPermission(
Manifest.permission.BLUETOOTH_CONNECT,
PERMISSION_BLUETOOTH
)
} else {
false
}
private fun Activity.hasPermission(
permission: String,
requestCode: Int
) = if (ContextCompat.checkSelfPermission(
activity,
this,
permission
) != PackageManager.PERMISSION_GRANTED
) {
ActivityCompat.requestPermissions(
activity,
this,
arrayOf(permission),
requestCode
)
@@ -1,26 +0,0 @@
package de.markusfisch.android.binaryeye.app
import android.app.Activity
import android.content.Intent
import de.markusfisch.android.binaryeye.activity.SplashActivity
private const val RESTART_COUNT = "restart_count"
private var restartCount = 0
fun setRestartCount(intent: Intent?) {
intent?.let {
restartCount = intent.getIntExtra(RESTART_COUNT, restartCount)
}
}
fun restartApp(activity: Activity? = null) {
if (activity != null && restartCount < 3) {
val intent = Intent(activity, SplashActivity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
intent.putExtra(RESTART_COUNT, ++restartCount)
activity.startActivity(intent)
activity.finish()
}
Runtime.getRuntime().exit(0)
}
@@ -0,0 +1,116 @@
package de.markusfisch.android.binaryeye.bluetooth
import android.bluetooth.BluetoothAdapter
import android.bluetooth.BluetoothDevice
import android.bluetooth.BluetoothSocket
import android.support.v7.preference.ListPreference
import de.markusfisch.android.binaryeye.database.Scan
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.IOException
import java.io.OutputStreamWriter
import java.util.*
fun Scan.sendBluetoothAsync(
host: String,
callback: (Boolean, Boolean) -> Unit
) {
CoroutineScope(Dispatchers.IO).launch(Dispatchers.IO) {
val connected = if (isConnected) {
true
} else {
connect(host)
}
val sent = if (connected) {
send(content)
} else {
false
}
withContext(Dispatchers.Main) {
callback(connected, sent)
}
}
}
fun setBluetoothHosts(listPref: ListPreference) {
val devices = try {
BluetoothAdapter.getDefaultAdapter().bondedDevices
} catch (e: SecurityException) {
// Do nothing, either the user has denied Bluetooth access
// or the permission was removed by the system. We're catching
// the exception to keep the app from crashing.
null
} ?: return
listPref.entries = devices.map {
it.name
}.toTypedArray()
listPref.entryValues = devices.map {
it.address
}.toTypedArray()
listPref.callChangeListener(listPref.value)
}
private var socket: BluetoothSocket? = null
private var writer: OutputStreamWriter? = null
private val blue = BluetoothAdapter.getDefaultAdapter()
private val uuid = UUID.fromString(
"8a8478c9-2ca8-404b-a0de-101f34ab71ae"
)
private var isConnected = false
private fun connect(deviceName: String): Boolean = try {
val device = findByName(deviceName) ?: throw RuntimeException(
"Bluetooth device not found"
)
socket = device.createRfcommSocketToServiceRecord(uuid)
socket?.connect()
writer = socket?.outputStream?.writer()
isConnected = true
true
} catch (e: Exception) {
close()
false
}
private fun send(message: String): Boolean = try {
writer?.apply {
write(message)
write("\n")
flush()
}
true
} catch (e: Exception) {
close()
false
}
private fun close() {
try {
writer?.close()
} catch (e: IOException) {
// Catch exception if writer wasn't initialized.
}
try {
socket?.close()
} catch (e: IOException) {
// Nothing we can do about this but keep
// the app from crashing.
}
writer = null
socket = null
isConnected = false
}
private fun findByName(findableName: String): BluetoothDevice? {
val deviceList = blue.bondedDevices
for (device in deviceList) {
if (device.address == findableName) {
return device
}
}
return null
}
@@ -1,17 +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) {
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)
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) }
@@ -11,13 +11,16 @@ import de.markusfisch.android.binaryeye.R
import de.markusfisch.android.binaryeye.widget.toast
import java.io.File
fun Context.execShareIntent(intent: Intent) {
if (!startIntent(intent)) {
toast(R.string.cannot_resolve_action)
}
fun Context.execShareIntent(intent: Intent): Boolean = if (
!startIntent(intent)
) {
toast(R.string.cannot_resolve_action)
false
} else {
true
}
fun Context.startIntent(intent: Intent) = try {
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
@@ -32,35 +35,52 @@ fun Context.startIntent(intent: Intent) = try {
false
}
fun shareText(context: Context, text: String, type: String = "text/plain") {
val intent = Intent(Intent.ACTION_SEND)
intent.putExtra(Intent.EXTRA_TEXT, text)
intent.type = type
context.execShareIntent(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
context.execShareIntent(intent)
}
fun shareFile(context: Context, file: File, type: String) {
getUriForFile(context, file)?.let {
shareUri(context, it, type)
}
}
fun getUriForFile(context: Context, file: File): Uri? {
return if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
Uri.fromFile(file)
fun Context.openUrl(url: String, silent: Boolean = false): Boolean {
val intent = Intent(Intent.ACTION_VIEW, url.parseAndNormalizeUri())
return if (silent) {
startIntent(intent)
} else {
FileProvider.getUriForFile(
context,
BuildConfig.APPLICATION_ID + ".provider",
file
)
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
})
}
@@ -2,23 +2,22 @@ package de.markusfisch.android.binaryeye.database
import android.content.Context
import android.database.Cursor
import de.markusfisch.android.binaryeye.app.toHexString
import de.markusfisch.android.binaryeye.content.toHexString
import de.markusfisch.android.binaryeye.io.writeExternalFile
import java.io.ByteArrayOutputStream
import java.io.OutputStream
fun exportCsv(
context: Context,
fun Context.exportCsv(
name: String,
cursor: Cursor,
delimiter: String
) = writeExternalFile(context, name, "text/csv") { outputStream ->
) = writeExternalFile(name, "text/csv") { outputStream ->
exportCsv(outputStream, cursor, delimiter)
}
fun exportCsv(cursor: Cursor, delimiter: String): String {
fun Cursor.exportCsv(delimiter: String): String {
val outputStream = ByteArrayOutputStream()
exportCsv(outputStream, cursor, delimiter)
exportCsv(outputStream, this, delimiter)
return outputStream.toString()
}
@@ -35,13 +34,14 @@ private fun exportCsv(
Database.SCANS_FORMAT,
Database.SCANS_CONTENT,
Database.SCANS_ERROR_CORRECTION_LEVEL,
Database.SCANS_ISSUE_NUMBER,
Database.SCANS_ORIENTATION,
Database.SCANS_OTHER_META_DATA,
Database.SCANS_PDF417_EXTRA_METADATA,
Database.SCANS_POSSIBLE_COUNTRY,
Database.SCANS_SUGGESTED_PRICE,
Database.SCANS_UPC_EAN_EXTENSION
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)
@@ -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)
}
@@ -5,7 +5,6 @@ import android.content.Context
import android.database.Cursor
import android.database.sqlite.SQLiteDatabase
import android.database.sqlite.SQLiteOpenHelper
import de.markusfisch.android.binaryeye.app.hasNonPrintableCharacters
import de.markusfisch.android.binaryeye.app.prefs
class Database {
@@ -25,7 +24,7 @@ class Database {
FROM $SCANS
${getWhereClause(query)}
ORDER BY $SCANS_DATETIME DESC
""", getWhereArguments(query)
""".trimMargin(), getWhereArguments(query)
)
fun getScansDetailed(query: String? = null): Cursor? = db.rawQuery(
@@ -37,17 +36,18 @@ class Database {
$SCANS_RAW,
$SCANS_FORMAT,
$SCANS_ERROR_CORRECTION_LEVEL,
$SCANS_ISSUE_NUMBER,
$SCANS_ORIENTATION,
$SCANS_OTHER_META_DATA,
$SCANS_PDF417_EXTRA_METADATA,
$SCANS_POSSIBLE_COUNTRY,
$SCANS_SUGGESTED_PRICE,
$SCANS_UPC_EAN_EXTENSION
$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
""", getWhereArguments(query)
""".trimMargin(), getWhereArguments(query)
)
private fun getWhereClause(
@@ -78,16 +78,17 @@ class Database {
$SCANS_RAW,
$SCANS_FORMAT,
$SCANS_ERROR_CORRECTION_LEVEL,
$SCANS_ISSUE_NUMBER,
$SCANS_ORIENTATION,
$SCANS_OTHER_META_DATA,
$SCANS_PDF417_EXTRA_METADATA,
$SCANS_POSSIBLE_COUNTRY,
$SCANS_SUGGESTED_PRICE,
$SCANS_UPC_EAN_EXTENSION
$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 = ?
""", arrayOf("$id")
""".trimMargin(), arrayOf("$id")
)?.use {
if (it.moveToFirst()) {
Scan(
@@ -95,13 +96,14 @@ class Database {
it.getBlob(SCANS_RAW),
it.getString(SCANS_FORMAT),
it.getString(SCANS_ERROR_CORRECTION_LEVEL),
it.getString(SCANS_ISSUE_NUMBER),
it.getString(SCANS_ORIENTATION),
it.getString(SCANS_OTHER_META_DATA),
it.getString(SCANS_PDF417_EXTRA_METADATA),
it.getString(SCANS_POSSIBLE_COUNTRY),
it.getString(SCANS_SUGGESTED_PRICE),
it.getString(SCANS_UPC_EAN_EXTENSION),
it.getString(SCANS_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)
)
@@ -110,47 +112,49 @@ class Database {
}
}
fun insertScan(scan: Scan): Long {
val cv = ContentValues()
cv.put(SCANS_DATETIME, scan.timestamp)
val isRaw = hasNonPrintableCharacters(scan.content)
if (isRaw) {
cv.put(SCANS_CONTENT, "")
cv.put(SCANS_RAW, scan.raw ?: scan.content.toByteArray())
} else {
cv.put(SCANS_CONTENT, scan.content)
}
cv.put(SCANS_FORMAT, scan.format)
scan.errorCorrectionLevel?.let {
cv.put(SCANS_ERROR_CORRECTION_LEVEL, it)
}
scan.issueNumber?.let { cv.put(SCANS_ISSUE_NUMBER, it) }
scan.orientation?.let { cv.put(SCANS_ORIENTATION, it) }
scan.otherMetaData?.let { cv.put(SCANS_OTHER_META_DATA, it) }
scan.pdf417ExtraMetaData?.let { cv.put(SCANS_PDF417_EXTRA_METADATA, it) }
scan.possibleCountry?.let { cv.put(SCANS_POSSIBLE_COUNTRY, it) }
scan.suggestedPrice?.let { cv.put(SCANS_SUGGESTED_PRICE, it) }
scan.upcEanExtension?.let { cv.put(SCANS_UPC_EAN_EXTENSION, it) }
if (prefs.ignoreConsecutiveDuplicates) {
val id = getIdOfLastScan(
cv.get(SCANS_CONTENT) as String,
if (isRaw) cv.get(SCANS_RAW) as ByteArray else null,
scan.format
)
if (id > 0L) {
return id
fun insertScan(scan: Scan): Long = db.insert(
SCANS,
null,
ContentValues().apply {
put(SCANS_DATETIME, scan.dateTime)
val isRaw = scan.raw != null
if (isRaw) {
put(SCANS_CONTENT, "")
put(SCANS_RAW, scan.raw)
} else {
put(SCANS_CONTENT, scan.content)
}
put(SCANS_FORMAT, scan.format)
scan.errorCorrectionLevel?.let {
put(SCANS_ERROR_CORRECTION_LEVEL, it)
}
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
}
}
}
return db.insert(SCANS, null, cv)
}
)
private fun getIdOfLastScan(
content: String,
raw: ByteArray?,
format: String
): Long {
return db.rawQuery(
"""SELECT
): Long = db.rawQuery(
"""SELECT
$SCANS_ID,
$SCANS_CONTENT,
$SCANS_RAW,
@@ -158,21 +162,20 @@ class Database {
FROM $SCANS
ORDER BY $SCANS_ID DESC
LIMIT 1
""", null
)?.use {
if (it.count > 0 &&
it.moveToFirst() &&
it.getString(it.getColumnIndex(SCANS_CONTENT)) == content &&
(raw == null || it.getBlob(it.getColumnIndex(SCANS_RAW))
?.contentEquals(raw) == true) &&
it.getString(it.getColumnIndex(SCANS_FORMAT)) == format
) {
it.getLong(it.getColumnIndex(SCANS_ID))
} else {
0L
}
} ?: 0L
}
""".trimMargin(), null
)?.use {
if (it.count > 0 &&
it.moveToFirst() &&
it.getString(SCANS_CONTENT) == content &&
(raw == null || it.getBlob(SCANS_RAW)
?.contentEquals(raw) == true) &&
it.getString(SCANS_FORMAT) == format
) {
it.getLong(SCANS_ID)
} else {
0L
}
} ?: 0L
fun removeScan(id: Long) {
db.delete(SCANS, "$SCANS_ID = ?", arrayOf("$id"))
@@ -189,9 +192,9 @@ class Database {
}
private class OpenHelper(context: Context) :
SQLiteOpenHelper(context, FILE_NAME, null, 4) {
SQLiteOpenHelper(context, FILE_NAME, null, 6) {
override fun onCreate(db: SQLiteDatabase) {
createScans(db)
db.createScans()
}
override fun onUpgrade(
@@ -200,13 +203,19 @@ class Database {
newVersion: Int
) {
if (oldVersion < 2) {
addRawColumn(db)
db.addRawColumn()
}
if (oldVersion < 3) {
addMetaDataColumns(db)
db.addMetaDataColumns()
}
if (oldVersion < 4) {
addNameColumn(db)
db.addNameColumn()
}
if (oldVersion < 5) {
db.migrateToZxingCpp()
}
if (oldVersion < 6) {
db.migrateToVersionString()
}
}
}
@@ -221,6 +230,8 @@ class Database {
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"
@@ -228,10 +239,17 @@ class Database {
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 createScans(db: SQLiteDatabase) {
db.execSQL("DROP TABLE IF EXISTS $SCANS")
db.execSQL(
private fun SQLiteDatabase.createScans() {
execSQL("DROP TABLE IF EXISTS $SCANS".trimMargin())
execSQL(
"""CREATE TABLE $SCANS (
$SCANS_ID INTEGER PRIMARY KEY AUTOINCREMENT,
$SCANS_DATETIME DATETIME NOT NULL,
@@ -240,46 +258,57 @@ class Database {
$SCANS_RAW BLOB,
$SCANS_FORMAT TEXT NOT NULL,
$SCANS_ERROR_CORRECTION_LEVEL TEXT,
$SCANS_ISSUE_NUMBER INT,
$SCANS_ORIENTATION INT,
$SCANS_OTHER_META_DATA TEXT,
$SCANS_PDF417_EXTRA_METADATA TEXT,
$SCANS_POSSIBLE_COUNTRY TEXT,
$SCANS_SUGGESTED_PRICE TEXT,
$SCANS_UPC_EAN_EXTENSION TEXT
)"""
$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 addRawColumn(db: SQLiteDatabase) {
db.execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_RAW BLOB")
private fun SQLiteDatabase.addRawColumn() {
execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_RAW BLOB".trimMargin())
}
private fun addMetaDataColumns(db: SQLiteDatabase) {
db.execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_ERROR_CORRECTION_LEVEL TEXT")
db.execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_ISSUE_NUMBER INT")
db.execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_ORIENTATION INT")
db.execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_OTHER_META_DATA TEXT")
db.execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_PDF417_EXTRA_METADATA TEXT")
db.execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_POSSIBLE_COUNTRY TEXT")
db.execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_SUGGESTED_PRICE TEXT")
db.execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_UPC_EAN_EXTENSION TEXT")
private fun SQLiteDatabase.addMetaDataColumns() {
execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_ERROR_CORRECTION_LEVEL TEXT".trimMargin())
execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_ISSUE_NUMBER INT".trimMargin())
execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_ORIENTATION INT".trimMargin())
execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_OTHER_META_DATA TEXT".trimMargin())
execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_PDF417_EXTRA_METADATA TEXT".trimMargin())
execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_POSSIBLE_COUNTRY TEXT".trimMargin())
execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_SUGGESTED_PRICE TEXT".trimMargin())
execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_UPC_EAN_EXTENSION TEXT".trimMargin())
}
private fun addNameColumn(db: SQLiteDatabase) {
db.execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_NAME TEXT")
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")
}
}
}
private fun Cursor.getString(name: String) = this.getString(
this.getColumnIndex(name)
)
private fun Cursor.getBlob(name: String) = this.getBlob(
this.getColumnIndex(name)
)
private fun Cursor.getLong(name: String) = this.getLong(
this.getColumnIndex(name)
)
@@ -6,16 +6,15 @@ import de.markusfisch.android.binaryeye.io.writeExternalFile
import java.io.File
import java.io.FileInputStream
fun exportDatabase(activity: Activity, fileName: String): Boolean {
fun Activity.exportDatabase(fileName: String): Boolean {
val dbFile = File(
Environment.getDataDirectory(),
"//data//${activity.packageName}//databases//${Database.FILE_NAME}"
"//data//${packageName}//databases//${Database.FILE_NAME}"
)
if (!dbFile.exists()) {
return false
}
return writeExternalFile(
activity,
fileName,
"application/vnd.sqlite3"
) {
@@ -2,24 +2,23 @@ package de.markusfisch.android.binaryeye.database
import android.content.Context
import android.database.Cursor
import de.markusfisch.android.binaryeye.app.toHexString
import de.markusfisch.android.binaryeye.content.toHexString
import de.markusfisch.android.binaryeye.io.writeExternalFile
import org.json.JSONArray
import org.json.JSONException
import org.json.JSONObject
fun exportJson(
context: Context,
fun Context.exportJson(
name: String,
cursor: Cursor
) = writeExternalFile(context, name, "application/json") { outputStream ->
exportJson(cursor)?.let {
) = writeExternalFile(name, "application/json") { outputStream ->
cursor.exportJson()?.let {
outputStream.write(it.toByteArray())
}
}
fun exportJson(cursor: Cursor): String? {
if (!cursor.moveToFirst()) {
fun Cursor.exportJson(): String? {
if (!moveToFirst()) {
return null
}
val columns = arrayOf(
@@ -27,30 +26,31 @@ fun exportJson(cursor: Cursor): String? {
Database.SCANS_FORMAT,
Database.SCANS_CONTENT,
Database.SCANS_ERROR_CORRECTION_LEVEL,
Database.SCANS_ISSUE_NUMBER,
Database.SCANS_ORIENTATION,
Database.SCANS_OTHER_META_DATA,
Database.SCANS_PDF417_EXTRA_METADATA,
Database.SCANS_POSSIBLE_COUNTRY,
Database.SCANS_SUGGESTED_PRICE,
Database.SCANS_UPC_EAN_EXTENSION
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(cursor.getColumnIndex(it), it)
Pair(getColumnIndex(it), it)
}
val contentIndex = cursor.getColumnIndex(Database.SCANS_CONTENT)
val rawIndex = cursor.getColumnIndex(Database.SCANS_RAW)
val contentIndex = getColumnIndex(Database.SCANS_CONTENT)
val rawIndex = getColumnIndex(Database.SCANS_RAW)
val root = JSONArray()
do {
var deviation: Pair<Int, String>? = null
if (cursor.getString(contentIndex)?.isEmpty() == true) {
if (getString(contentIndex)?.isEmpty() == true) {
deviation = Pair(
contentIndex,
cursor.getBlob(rawIndex).toHexString()
getBlob(rawIndex).toHexString()
)
}
root.put(cursor.toJsonObject(indices, deviation))
} while (cursor.moveToNext())
root.put(toJsonObject(indices, deviation))
} while (moveToNext())
return root.toString()
}
@@ -1,42 +1,33 @@
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 com.google.zxing.Result
import com.google.zxing.ResultMetadataType
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?,
val issueNumber: String?,
val orientation: String?,
val otherMetaData: String?,
val pdf417ExtraMetaData: String?,
val possibleCountry: String?,
val suggestedPrice: String?,
val upcEanExtension: String?,
val timestamp: String = getDateTime(),
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 {
constructor(result: Result) : this(
result.text,
getRawBytes(result),
result.barcodeFormat.toString(),
result.getMetaString(ResultMetadataType.ERROR_CORRECTION_LEVEL),
result.getMetaString(ResultMetadataType.ISSUE_NUMBER),
result.getMetaString(ResultMetadataType.ORIENTATION),
result.getMetaString(ResultMetadataType.OTHER),
result.getMetaString(ResultMetadataType.PDF417_EXTRA_METADATA),
result.getMetaString(ResultMetadataType.POSSIBLE_COUNTRY),
result.getMetaString(ResultMetadataType.SUGGESTED_PRICE),
result.getMetaString(ResultMetadataType.UPC_EAN_EXTENSION)
)
// Needs to be overwritten manually, as ByteArray is an array and
// this isn't handled well by Kotlin
// this isn't handled well by Kotlin.
override fun equals(other: Any?): Boolean {
if (this === other) {
return true
@@ -48,70 +39,76 @@ data class Scan(
other as Scan
return id == other.id &&
timestamp == other.timestamp &&
dateTime == other.dateTime &&
content == other.content &&
((raw == null && other.raw == null) ||
(raw != null && other.raw != null && raw.contentEquals(other.raw))) &&
format == other.format &&
errorCorrectionLevel == other.errorCorrectionLevel &&
issueNumber == other.issueNumber &&
orientation == other.orientation &&
otherMetaData == other.otherMetaData &&
pdf417ExtraMetaData == other.pdf417ExtraMetaData &&
possibleCountry == other.possibleCountry &&
suggestedPrice == other.suggestedPrice &&
upcEanExtension == other.upcEanExtension
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
// this isn't handled well by Kotlin.
override fun hashCode(): Int {
var result = id.hashCode()
result = 31 * result + timestamp.hashCode()
result = 31 * result + dateTime.hashCode()
result = 31 * result + content.hashCode()
result = 31 * result + (raw?.contentHashCode() ?: 0)
result = 31 * result + format.hashCode()
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)
result = 31 * result + (orientation?.hashCode() ?: 0)
result = 31 * result + (otherMetaData?.hashCode() ?: 0)
result = 31 * result + (pdf417ExtraMetaData?.hashCode() ?: 0)
result = 31 * result + (possibleCountry?.hashCode() ?: 0)
result = 31 * result + (suggestedPrice?.hashCode() ?: 0)
result = 31 * result + (upcEanExtension?.hashCode() ?: 0)
return result
}
private constructor(parcel: Parcel) : this(
content = parcel.readString() ?: "",
raw = readByteArray(parcel),
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(),
orientation = parcel.readString(),
otherMetaData = parcel.readString(),
pdf417ExtraMetaData = parcel.readString(),
possibleCountry = parcel.readString(),
suggestedPrice = parcel.readString(),
upcEanExtension = parcel.readString(),
timestamp = parcel.readString() ?: "",
dateTime = parcel.readString() ?: "",
id = parcel.readLong()
)
override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeString(content)
writeByteArray(parcel, raw)
parcel.writeString(format)
parcel.writeString(errorCorrectionLevel)
parcel.writeString(issueNumber)
parcel.writeString(orientation)
parcel.writeString(otherMetaData)
parcel.writeString(pdf417ExtraMetaData)
parcel.writeString(possibleCountry)
parcel.writeString(suggestedPrice)
parcel.writeString(upcEanExtension)
parcel.writeString(timestamp)
parcel.writeLong(id)
parcel.apply {
writeString(content)
writeSizedByteArray(raw)
writeString(format)
writeString(errorCorrectionLevel)
writeString(version)
writeInt(sequenceSize)
writeInt(sequenceIndex)
writeString(sequenceId)
writeString(country)
writeString(addOn)
writeString(price)
writeString(issueNumber)
writeString(dateTime)
writeLong(id)
}
}
override fun describeContents() = 0
@@ -125,47 +122,98 @@ data class Scan(
}
}
private fun getDateTime(time: Long = System.currentTimeMillis()) = DateFormat.format(
"yyyy-MM-dd HH:mm:ss",
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" -> 4
"Q" -> 6
"H" -> 8
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 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
}
// If the byte segments are shorter than the converted string, the
// content of the QR Code has been encoded with different encoding
// modes (e.g. some parts in alphanumeric, some in byte encoding).
// This is because Zxing only records byte segments for byte encoded
// parts. Please note the byte segments can actually be longer than
// the string because Zxing cuts off prefixes like "WIFI:".
return if (bytes.size >= result.text.length) bytes else null
}
private fun Result.getMetaString(
key: ResultMetadataType
): String? = this.resultMetadata?.let {
it[key]?.toString()
}
private fun writeByteArray(parcel: Parcel, array: ByteArray?) {
private fun Parcel.writeSizedByteArray(array: ByteArray?) {
val size = array?.size ?: 0
parcel.writeInt(size)
writeInt(size)
if (size > 0) {
parcel.writeByteArray(array)
writeByteArray(array)
}
}
private fun readByteArray(parcel: Parcel): ByteArray? {
val size = parcel.readInt()
private fun Parcel.readSizedByteArray(): ByteArray? {
val size = readInt()
return if (size > 0) {
val array = ByteArray(size)
parcel.readByteArray(array)
readByteArray(array)
array
} else {
null
@@ -9,13 +9,13 @@ import android.os.Bundle
import android.support.v4.app.Fragment
import android.view.*
import android.widget.EditText
import com.google.zxing.BarcodeFormat
import com.google.zxing.EncodeHintType
import de.markusfisch.android.binaryeye.R
import de.markusfisch.android.binaryeye.app.hasWritePermission
import de.markusfisch.android.binaryeye.content.copyToClipboard
import de.markusfisch.android.binaryeye.content.shareFile
import de.markusfisch.android.binaryeye.content.shareText
import de.markusfisch.android.binaryeye.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
@@ -23,28 +23,25 @@ import de.markusfisch.android.binaryeye.view.doOnApplyWindowInsets
import de.markusfisch.android.binaryeye.view.setPaddingFromWindowInsets
import de.markusfisch.android.binaryeye.widget.ConfinedScalingImageView
import de.markusfisch.android.binaryeye.widget.toast
import de.markusfisch.android.binaryeye.zxing.encodeAsBitmap
import de.markusfisch.android.binaryeye.zxing.encodeAsSvg
import de.markusfisch.android.binaryeye.zxing.encodeAsText
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
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 enum class FileType {
PNG, SVG, TXT
}
private var barcodeBitmap: Bitmap? = null
private var barcodeSvg: String? = null
private var barcodeTxt: String? = null
private var content: String = ""
private var format: BarcodeFormat? = null
private val parentJob = Job()
private val scope = CoroutineScope(Dispatchers.IO + parentJob)
private lateinit var barcode: Barcode
override fun onCreate(state: Bundle?) {
super.onCreate(state)
@@ -65,15 +62,13 @@ 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 hints = args.getSerializable(HINTS) as EnumMap<EncodeHintType, Any>?
val size = args.getInt(SIZE)
val bitmap: Bitmap
try {
barcodeBitmap = encodeAsBitmap(content, format, size, size, hints)
barcodeSvg = encodeAsSvg(content, format, hints)
barcodeTxt = encodeAsText(content, format, hints)
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()) {
@@ -85,21 +80,24 @@ class BarcodeFragment : Fragment() {
fragmentManager.popBackStack()
return null
}
this.content = content
this.format = format
val imageView = view.findViewById<ConfinedScalingImageView>(
R.id.barcode
)
imageView.setImageBitmap(barcodeBitmap)
imageView.setImageBitmap(bitmap)
imageView.post {
// Make sure to invoke this after ScalingImageView.onLayout().
imageView.minWidth /= 2f
imageView.minWidth = min(
imageView.minWidth / 2f,
barcode.size.toFloat()
)
}
view.findViewById<View>(R.id.share).setOnClickListener {
pickFileType(context, R.string.share_as) {
shareAs(it)
it.context?.apply {
pickFileType(R.string.share_as) { fileType ->
shareAs(fileType)
}
}
}
@@ -111,6 +109,25 @@ class BarcodeFragment : Fragment() {
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)
}
@@ -118,14 +135,14 @@ class BarcodeFragment : Fragment() {
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.copy_to_clipboard -> {
barcodeTxt?.let {
context.copyToClipboard(it)
context.toast(R.string.copied_to_clipboard)
context.apply {
copyToClipboard(barcode.text())
toast(R.string.copied_to_clipboard)
}
true
}
R.id.export_to_file -> {
pickFileType(context, R.string.export_as) {
context.pickFileType(R.string.export_as) {
askForFileNameAndSave(it)
}
true
@@ -134,13 +151,12 @@ class BarcodeFragment : Fragment() {
}
}
private fun pickFileType(
context: Context,
private fun Context.pickFileType(
title: Int,
action: (FileType) -> Unit
) {
val fileTypes = FileType.values()
AlertDialog.Builder(context)
AlertDialog.Builder(this)
.setTitle(title)
.setItems(fileTypes.map { it.name }.toTypedArray()) { _, which ->
action(fileTypes[which])
@@ -152,15 +168,17 @@ class BarcodeFragment : Fragment() {
@SuppressLint("InflateParams")
private fun askForFileNameAndSave(fileType: FileType) {
val ac = activity ?: return
// write permission is only required before Android Q
// Write permission is only required before Android Q.
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q &&
!hasWritePermission(ac) { askForFileNameAndSave(fileType) }
!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("${format.toString()}_$content"))
editText.setText(
encodeFileName("${barcode.format}_${barcode.content}")
)
AlertDialog.Builder(ac)
.setView(view)
.setPositiveButton(android.R.string.ok) { _, _ ->
@@ -170,23 +188,19 @@ class BarcodeFragment : Fragment() {
addSuffixIfNotGiven(fileName, ".png"),
MIME_PNG
) {
barcodeBitmap?.saveAsPng(it)
barcode.bitmap().saveAsPng(it)
}
FileType.SVG -> saveAs(
addSuffixIfNotGiven(fileName, ".svg"),
MIME_SVG
) { outputStream ->
barcodeSvg?.let {
outputStream.write(it.toByteArray())
}
outputStream.write(barcode.svg().toByteArray())
}
FileType.TXT -> saveAs(
addSuffixIfNotGiven(fileName, ".txt"),
MIME_TXT
) { outputStream ->
barcodeTxt?.let {
outputStream.write(it.toByteArray())
}
outputStream.write(barcode.text().toByteArray())
}
}
}
@@ -201,27 +215,26 @@ class BarcodeFragment : Fragment() {
write: (outputStream: OutputStream) -> Unit
) {
val ac = activity ?: return
GlobalScope.launch(Dispatchers.IO) {
val message = writeExternalFile(ac, fileName, mimeType, write).toSaveResult()
launch(Dispatchers.Main) {
scope.launch(Dispatchers.IO) {
val message = ac.writeExternalFile(fileName, mimeType, write).toSaveResult()
withContext(Dispatchers.Main) {
ac.toast(message)
}
}
}
private fun shareAs(fileType: FileType) {
private fun Context.shareAs(fileType: FileType) {
when (fileType) {
FileType.PNG -> barcodeBitmap?.let { share(it) }
FileType.SVG -> barcodeSvg?.let { shareText(context, it, MIME_SVG) }
FileType.TXT -> barcodeTxt?.let { shareText(context, it) }
FileType.PNG -> share(barcode.bitmap())
FileType.SVG -> shareText(barcode.svg(), MIME_SVG)
FileType.TXT -> shareText(barcode.text())
}
}
private fun share(bitmap: Bitmap) {
val ctx = context ?: return
GlobalScope.launch(Dispatchers.IO) {
private fun Context.share(bitmap: Bitmap) {
scope.launch(Dispatchers.IO) {
val file = File(
ctx.externalCacheDir,
externalCacheDir,
"shared_barcode.png"
)
val success = try {
@@ -232,11 +245,11 @@ class BarcodeFragment : Fragment() {
} catch (e: IOException) {
false
}
launch(Dispatchers.Main) {
withContext(Dispatchers.Main) {
if (success) {
shareFile(ctx, file, "image/png")
shareFile(file, "image/png")
} else {
ctx.toast(R.string.error_saving_file)
toast(R.string.error_saving_file)
}
}
}
@@ -245,25 +258,26 @@ class BarcodeFragment : Fragment() {
companion object {
private const val CONTENT = "content"
private const val FORMAT = "format"
private const val HINTS = "hints"
private const val SIZE = "size"
private const val 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,
format: Format,
size: Int,
hints: EnumMap<EncodeHintType, Any>? = null
ecLevel: Int = -1,
colors: Int = 0
): Fragment {
val args = Bundle()
args.putString(CONTENT, content)
args.putSerializable(FORMAT, format)
hints?.let {
args.putSerializable(HINTS, it)
}
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
@@ -271,10 +285,68 @@ class BarcodeFragment : Fragment() {
}
}
private fun Bitmap.saveAsPng(outputStream: OutputStream, quality: Int = 90) {
this.compress(Bitmap.CompressFormat.PNG, quality, outputStream)
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())
@@ -5,15 +5,14 @@ import android.os.Bundle
import android.support.design.widget.FloatingActionButton
import android.support.v4.app.Fragment
import android.text.Editable
import android.text.Html
import android.text.TextWatcher
import android.text.method.LinkMovementMethod
import android.view.*
import android.widget.EditText
import android.widget.TableLayout
import android.widget.TableRow
import android.widget.TextView
import 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
@@ -21,16 +20,17 @@ import de.markusfisch.android.binaryeye.adapter.prettifyFormatName
import de.markusfisch.android.binaryeye.app.*
import de.markusfisch.android.binaryeye.content.copyToClipboard
import de.markusfisch.android.binaryeye.content.shareText
import de.markusfisch.android.binaryeye.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.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlinx.coroutines.*
import kotlin.math.roundToInt
class DecodeFragment : Fragment() {
private lateinit var contentView: EditText
@@ -38,20 +38,24 @@ class DecodeFragment : Fragment() {
private lateinit var dataView: TableLayout
private lateinit var metaView: TableLayout
private lateinit var hexView: TextView
private lateinit var linksView: TextView
private lateinit var format: String
private lateinit var stampView: TextView
private lateinit var recreationView: ImageView
private lateinit var fab: FloatingActionButton
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 raw: ByteArray = ByteArray(0)
private var originalBytes: ByteArray = ByteArray(0)
private var id = 0L
private var recreation: Recreation? = null
override fun onCreate(state: Bundle?) {
super.onCreate(state)
@@ -74,75 +78,42 @@ class DecodeFragment : Fragment() {
closeAutomatically = prefs.closeAutomatically &&
activity?.intent?.hasExtra(MainActivity.DECODED) == true
val scan = arguments?.getParcelable(SCAN) as Scan?
?: throw IllegalArgumentException("DecodeFragment needs a Scan")
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
val inputContent = scan.content
isBinary = hasNonPrintableCharacters(
inputContent
) or inputContent.isEmpty()
raw = scan.raw ?: inputContent.toByteArray()
format = scan.format
contentView = view.findViewById(R.id.content)
linksView = view.findViewById(R.id.links)
fab = view.findViewById(R.id.open)
if (!isBinary) {
contentView.setText(inputContent)
contentView.addTextChangedListener(object : TextWatcher {
override fun afterTextChanged(s: Editable?) {
updateViewsAndAction(content.toByteArray())
}
override fun beforeTextChanged(
s: CharSequence?,
start: Int,
count: Int,
after: Int
) {
}
override fun onTextChanged(
s: CharSequence?,
start: Int,
before: Int,
count: Int
) {
}
})
fab.setOnClickListener {
executeAction(content.toByteArray())
}
if (prefs.openImmediately) {
executeAction(content.toByteArray())
}
linksView.text = getLinks(inputContent).joinToString("\n")
linksView.movementMethod = LinkMovementMethod.getInstance()
} else {
contentView.setText(R.string.binary_data)
contentView.isEnabled = false
fab.setImageResource(R.drawable.ic_action_save)
fab.setOnClickListener {
askForFileNameAndSave(raw)
}
}
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)
updateViewsAndAction(raw)
if (!isBinary) {
fillDataView(dataView, inputContent)
}
initContentAndFab(originalContent)
if (prefs.showMetaData) {
fillMetaView(metaView, scan)
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()
@@ -155,91 +126,175 @@ class DecodeFragment : Fragment() {
parentJob.cancel()
}
private fun updateViewsAndAction(bytes: ByteArray) {
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?) {
updateViewsAndFab(s?.toString() ?: "")
}
override fun beforeTextChanged(
s: CharSequence?,
start: Int,
count: Int,
after: Int
) = Unit
override fun onTextChanged(
s: CharSequence?,
start: Int,
before: Int,
count: Int
) = Unit
})
fab.setOnClickListener {
executeAction(content)
}
if (prefs.openImmediately) {
executeAction(content)
}
}
}
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,
prettifyFormatName(format),
bytes.size
)
hexView.text = if (prefs.showHexDump) hexDump(bytes) else ""
if (prevAction !== action) {
fab.setImageResource(action.iconResId)
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
fab.setOnLongClickListener { v ->
v.context.toast(action.titleResId)
true
}
} else {
fab.tooltipText = getString(action.titleResId)
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 fillDataTable(tableLayout: TableLayout, items: LinkedHashMap<Int, String?>?) {
val ctx = tableLayout.context
val spaceBetween = (16f * ctx.resources.displayMetrics.density).toInt()
var hasEntries = false
items?.forEach { item ->
item.value?.let {
val tr = TableRow(ctx)
val keyView = TextView(ctx)
keyView.setText(item.key)
val valueView = TextView(ctx)
valueView.setPadding(spaceBetween, 0, 0, 0)
valueView.text = it
tr.addView(keyView)
tr.addView(valueView)
tableLayout.addView(tr)
hasEntries = true
}
}
if (hasEntries) {
tableLayout.setPadding(0, 0, 0, spaceBetween)
}
}
private fun fillDataView(tableLayout: TableLayout, content: String) {
private fun TableLayout.fillDataView(text: String) {
val items = LinkedHashMap<Int, String?>()
if (action is WifiAction) {
val wifiData = WifiConnector.parseMap(content)
if (wifiData != null) {
items[R.string.entry_type] = getString(R.string.wifi_network)
items[R.string.wifi_ssid] = wifiData["S"]
items[R.string.wifi_password] = wifiData["P"]
items[R.string.wifi_type] = wifiData["T"]
items[R.string.wifi_hidden] = wifiData["H"]
items[R.string.wifi_eap] = wifiData["E"]
items[R.string.wifi_identity] = wifiData["I"]
items[R.string.wifi_anonymous_identity] = wifiData["A"]
items[R.string.wifi_phase2] = wifiData["PH2"]
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"]
)
)
}
}
fillDataTable(tableLayout, items)
fill(items)
}
private fun fillMetaView(tableLayout: TableLayout, scan: Scan) {
fillDataTable(
tableLayout, linkedMapOf(
R.string.error_correction_level to scan.errorCorrectionLevel,
R.string.issue_number to scan.issueNumber,
R.string.orientation to scan.orientation,
R.string.other_meta_data to scan.otherMetaData,
R.string.pdf417_extra_metadata to scan.pdf417ExtraMetaData,
R.string.possible_country to scan.possibleCountry,
R.string.suggested_price to scan.suggestedPrice,
R.string.upc_ean_extension to scan.upcEanExtension
)
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) {
@@ -269,15 +324,14 @@ class DecodeFragment : Fragment() {
true
}
R.id.share -> {
context?.also {
shareText(it, textOrHex())
context?.apply {
shareText(textOrHex())
maybeBackOrFinish()
}
true
}
R.id.create -> {
addFragment(
fragmentManager,
fragmentManager?.addFragment(
EncodeFragment.newInstance(content, format)
)
true
@@ -292,7 +346,7 @@ class DecodeFragment : Fragment() {
}
private fun textOrHex() = if (isBinary) {
raw.toHexString()
originalBytes.toHexString()
} else {
content
}
@@ -302,16 +356,16 @@ class DecodeFragment : Fragment() {
if (ac is WifiAction) {
ac.password?.let { password ->
activity?.apply {
copyToClipboard(password)
copyToClipboard(password, isSensitive = true)
toast(R.string.copied_password_to_clipboard)
}
}
}
}
private fun copyToClipboard(text: String) {
private fun copyToClipboard(text: String, isSensitive: Boolean = false) {
activity?.apply {
copyToClipboard(text)
copyToClipboard(text, isSensitive)
toast(R.string.copied_to_clipboard)
}
}
@@ -331,17 +385,17 @@ class DecodeFragment : Fragment() {
}
}
private fun executeAction(content: ByteArray) {
private fun executeAction(content: String) {
val ac = activity ?: return
if (content.isNotEmpty()) {
if (action is WifiAction &&
Build.VERSION.SDK_INT < Build.VERSION_CODES.Q &&
!hasLocationPermission(ac) { executeAction(content) }
!ac.hasLocationPermission { executeAction(content) }
) {
return
}
scope.launch {
action.execute(ac, content)
action.execute(ac, content.toByteArray())
maybeBackOrFinish()
}
}
@@ -349,19 +403,25 @@ class DecodeFragment : Fragment() {
private fun askForFileNameAndSave(raw: ByteArray) {
val ac = activity ?: return
if (!hasWritePermission(ac) { askForFileNameAndSave(raw) }) {
// Write permission is only required before Android Q.
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q &&
!ac.hasWritePermission { askForFileNameAndSave(raw) }
) {
return
}
scope.launch(Dispatchers.Main) {
val name = ac.askForFileName() ?: return@launch
val message = writeExternalFile(
ac,
name,
"application/octet-stream"
) {
it.write(raw)
}.toSaveResult()
ac.toast(message)
withContext(Dispatchers.IO) {
val message = ac.writeExternalFile(
name,
"application/octet-stream"
) {
it.write(raw)
}.toSaveResult()
withContext(Dispatchers.Main) {
ac.toast(message)
}
}
}
}
@@ -378,6 +438,23 @@ class DecodeFragment : Fragment() {
}
}
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 ""
@@ -416,3 +493,92 @@ private fun hexDump(bytes: ByteArray, charsPerLine: Int = 33): 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)
}
@@ -1,42 +1,45 @@
package de.markusfisch.android.binaryeye.fragment
import android.content.Context
import android.os.Bundle
import android.support.v4.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.*
import com.google.zxing.BarcodeFormat
import com.google.zxing.EncodeHintType
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel
import de.markusfisch.android.binaryeye.R
import de.markusfisch.android.binaryeye.adapter.prettifyFormatName
import de.markusfisch.android.binaryeye.app.addFragment
import de.markusfisch.android.binaryeye.app.prefs
import de.markusfisch.android.binaryeye.text.unescape
import de.markusfisch.android.binaryeye.view.hideSoftKeyboard
import de.markusfisch.android.binaryeye.view.setPaddingFromWindowInsets
import java.util.*
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 errorCorrectionLabel: TextView
private lateinit var errorCorrectionLevel: 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 lateinit var unescapeCheckBox: CheckBox
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
private val formats = arrayListOf(
Format.AZTEC,
Format.CODABAR,
Format.CODE_39,
Format.CODE_128,
Format.DATA_MATRIX,
Format.EAN_8,
Format.EAN_13,
Format.ITF,
Format.PDF_417,
Format.QR_CODE,
Format.UPC_A
)
override fun onCreateView(
@@ -57,7 +60,7 @@ class EncodeFragment : Fragment() {
val formatAdapter = ArrayAdapter(
ac,
android.R.layout.simple_spinner_item,
writers.map { prettifyFormatName(it.name) }
formats.map { prettifyFormatName(it.name) }
)
formatAdapter.setDropDownViewResource(
android.R.layout.simple_spinner_dropdown_item
@@ -70,38 +73,72 @@ class EncodeFragment : Fragment() {
position: Int,
id: Long
) {
val visibility = if (
writers[position] == BarcodeFormat.QR_CODE
) {
View.VISIBLE
} else {
View.GONE
val format = formats[position]
val arrayId = when (format) {
Format.AZTEC -> R.array.aztec_error_correction_levels
Format.QR_CODE -> R.array.qr_error_correction_levels
Format.PDF_417 -> R.array.pdf417_error_correction_levels
else -> 0
}
errorCorrectionLabel.visibility = visibility
errorCorrectionLevel.visibility = visibility
if (arrayId > 0) {
ecSpinner.setEntries(arrayId)
val idx = format.unpackEcLevel(
prefs.indexOfLastSelectedEcLevel
)
if (idx < ecSpinner.adapter.count) {
ecSpinner.setSelection(idx)
}
true
} else {
false
}.setVisibility(ecLabel, ecSpinner)
format.canBeInverted().setVisibility(
colorsLabel, colorsSpinner
)
}
override fun onNothingSelected(parentView: AdapterView<*>?) {}
}
errorCorrectionLabel = view.findViewById(R.id.error_correction_label)
errorCorrectionLevel = view.findViewById(R.id.error_correction_level)
ecLabel = view.findViewById(R.id.error_correction_label)
ecSpinner = view.findViewById(R.id.error_correction_level)
ecSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(
parentView: AdapterView<*>?,
selectedItemView: View?,
position: Int,
id: Long
) {
val format = formats[formatView.selectedItemPosition]
prefs.indexOfLastSelectedEcLevel = format.packEcLevel(
prefs.indexOfLastSelectedEcLevel,
position
)
}
override fun onNothingSelected(parentView: AdapterView<*>?) {}
}
colorsLabel = view.findViewById(R.id.colors_label)
colorsSpinner = view.findViewById(R.id.colors)
sizeView = view.findViewById(R.id.size_display)
sizeBarView = view.findViewById(R.id.size_bar)
initSizeBar()
contentView = view.findViewById(R.id.content)
unescapeCheckBox = view.findViewById(R.id.unescape)
unescapeCheckBox.isChecked = prefs.expandEscapeSequences
val args = arguments
args?.also {
contentView.setText(it.getString(CONTENT))
args?.getString(CONTENT)?.let {
contentView.setText(it)
}
val barcodeFormat = args?.getString(FORMAT)
if (barcodeFormat != null) {
formatView.setSelection(
writers.indexOf(resolveFormat(barcodeFormat))
formats.indexOf(barcodeFormat.toFormat())
)
} else if (state == null) {
formatView.post {
@@ -110,7 +147,7 @@ class EncodeFragment : Fragment() {
}
view.findViewById<View>(R.id.encode).setOnClickListener {
encode()
it.context.encode()
}
(view.findViewById(R.id.inset_layout) as View).setPaddingFromWindowInsets()
@@ -122,34 +159,36 @@ class EncodeFragment : Fragment() {
override fun onPause() {
super.onPause()
prefs.indexOfLastSelectedFormat = formatView.selectedItemPosition
prefs.expandEscapeSequences = unescapeCheckBox.isChecked
}
private fun encode() {
val hints = EnumMap<EncodeHintType, Any>(EncodeHintType::class.java)
val format = writers[formatView.selectedItemPosition]
if (format == BarcodeFormat.QR_CODE) {
hints[EncodeHintType.ERROR_CORRECTION] = arrayListOf(
ErrorCorrectionLevel.L,
ErrorCorrectionLevel.M,
ErrorCorrectionLevel.Q,
ErrorCorrectionLevel.H
)[errorCorrectionLevel.selectedItemPosition]
private fun Context.encode() {
var content = contentView.text.toString()
if (unescapeCheckBox.isChecked) {
try {
content = content.unescape()
} catch (e: IllegalArgumentException) {
toast(e.message ?: "Invalid escape sequence")
return
}
}
val size = getSize(sizeBarView.progress)
val content = contentView.text.toString()
if (content.isEmpty()) {
Toast.makeText(
context,
R.string.error_no_content,
Toast.LENGTH_SHORT
).show()
} else {
activity?.hideSoftKeyboard(contentView)
addFragment(
fragmentManager,
BarcodeFragment.newInstance(content, format, size, hints)
)
toast(R.string.error_no_content)
return
}
hideSoftKeyboard(contentView)
val format = formats[formatView.selectedItemPosition]
fragmentManager?.addFragment(
BarcodeFragment.newInstance(
content,
format,
getSize(sizeBarView.progress),
format.getErrorCorrectionLevel(ecSpinner.selectedItemPosition),
if (format.canBeInverted()) {
colorsSpinner.selectedItemPosition
} else 0
)
)
}
private fun initSizeBar() {
@@ -175,18 +214,16 @@ class EncodeFragment : Fragment() {
sizeView.text = getString(R.string.width_by_height, size, size)
}
private fun getSize(power: Int) = 128 * (power + 1)
companion object {
private const val CONTENT = "content"
private const val FORMAT = "format"
fun newInstance(
content: String,
content: String? = null,
format: String? = null
): Fragment {
val args = Bundle()
args.putString(CONTENT, content)
content?.let { args.putString(CONTENT, content) }
format?.let { args.putString(FORMAT, it) }
val fragment = EncodeFragment()
fragment.arguments = args
@@ -195,8 +232,57 @@ class EncodeFragment : Fragment() {
}
}
private fun resolveFormat(name: String): BarcodeFormat = try {
BarcodeFormat.valueOf(name)
} catch (_: IllegalArgumentException) {
BarcodeFormat.QR_CODE
private fun Boolean.setVisibility(vararg views: View) {
val visibility = if (this) View.VISIBLE else View.GONE
for (view in views) {
view.visibility = visibility
}
}
private fun Format.packEcLevel(packed: Int, level: Int): Int {
val s = ecLevelShift()
return (level shl s) or (packed and (15 shl s).inv())
}
private fun Format.unpackEcLevel(packed: Int) =
(packed shr ecLevelShift()) and 15
private fun Format.ecLevelShift() = when (this) {
Format.AZTEC -> 0
Format.QR_CODE -> 4
Format.PDF_417 -> 8
else -> throw IllegalArgumentException("$this does not have error levels")
}
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
}
private fun Format.getErrorCorrectionLevel(position: Int) = when (this) {
Format.AZTEC -> position
Format.QR_CODE -> (position + 1) * 2
Format.PDF_417 -> position
else -> 0
}.coerceIn(0, 8)
private fun Spinner.setEntries(resId: Int) = ArrayAdapter.createFromResource(
this.context,
resId,
android.R.layout.simple_spinner_item
).also { aa ->
aa.setDropDownViewResource(
android.R.layout.simple_spinner_dropdown_item
)
adapter = aa
}
private fun getSize(power: Int) = 128 * (power + 1)
@@ -23,10 +23,7 @@ import de.markusfisch.android.binaryeye.adapter.ScansAdapter
import de.markusfisch.android.binaryeye.app.*
import de.markusfisch.android.binaryeye.content.copyToClipboard
import de.markusfisch.android.binaryeye.content.shareText
import de.markusfisch.android.binaryeye.database.Database
import de.markusfisch.android.binaryeye.database.exportCsv
import de.markusfisch.android.binaryeye.database.exportDatabase
import de.markusfisch.android.binaryeye.database.exportJson
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.*
@@ -75,7 +72,7 @@ class HistoryFragment : Fragment() {
val ac = activity ?: return false
return when (item.itemId) {
R.id.copy_scan -> {
scansAdapter?.getSelectedContent()?.let {
scansAdapter?.getSelectedContent("\n")?.let {
ac.copyToClipboard(it)
ac.toast(R.string.copied_to_clipboard)
}
@@ -83,19 +80,21 @@ class HistoryFragment : Fragment() {
true
}
R.id.edit_scan -> {
scansAdapter?.let {
askForName(
ac,
it.selectedScanId,
getScanName(it.selectedScanPosition)
scansAdapter?.forSelection { id, position ->
ac.askForName(
id,
scansAdapter?.getName(position),
scansAdapter?.getContent(position)
)
}
closeActionMode()
true
}
R.id.remove_scan -> {
scansAdapter?.let {
askToRemoveScan(ac, it.selectedScanId)
scansAdapter?.getSelectedIds()?.let {
if (it.isNotEmpty()) {
ac.askToRemoveScans(it)
}
}
closeActionMode()
true
@@ -145,8 +144,7 @@ class HistoryFragment : Fragment() {
showScan(id)
}
listView.setOnItemLongClickListener { _, v, position, id ->
v.isSelected = true
scansAdapter?.select(id, position)
scansAdapter?.select(v, id, position)
if (actionMode == null && ac is AppCompatActivity) {
actionMode = ac.delegate.startSupportActionMode(
actionModeCallback
@@ -158,7 +156,7 @@ class HistoryFragment : Fragment() {
fab = view.findViewById(R.id.share)
fab.setOnClickListener { v ->
pickListSeparatorAndShare(v.context)
v.context.pickListSeparatorAndShare()
}
progressView = view.findViewById(R.id.progress_view)
@@ -220,7 +218,7 @@ class HistoryFragment : Fragment() {
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.clear -> {
askToRemoveScans(context)
context.askToRemoveScans()
true
}
R.id.export_history -> {
@@ -293,29 +291,28 @@ class HistoryFragment : Fragment() {
private fun showScan(id: Long) = db.getScan(id)?.also { scan ->
closeActionMode()
try {
addFragment(
fragmentManager,
DecodeFragment.newInstance(scan)
)
fragmentManager?.addFragment(DecodeFragment.newInstance(scan))
} catch (e: IllegalArgumentException) {
// Ignore, can never happen.
}
}
private fun getScanName(position: Int): String? {
val cursor = scansAdapter?.getItem(position) as Cursor?
return cursor?.getString(cursor.getColumnIndex(Database.SCANS_NAME))
}
// Dialogs don't have a parent layout.
@SuppressLint("InflateParams")
private fun askForName(context: Context, id: Long, text: String?) {
val view = LayoutInflater.from(context).inflate(
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(context)
AlertDialog.Builder(this)
.setTitle(
if (content.isNullOrEmpty()) {
getString(R.string.binary_data)
} else {
content
}
)
.setView(view)
.setPositiveButton(android.R.string.ok) { _, _ ->
val name = nameView.text.toString()
@@ -326,11 +323,17 @@ class HistoryFragment : Fragment() {
.show()
}
private fun askToRemoveScan(context: Context, id: Long) {
AlertDialog.Builder(context)
.setMessage(R.string.really_remove_scan)
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) { _, _ ->
db.removeScan(id)
ids.forEach { db.removeScan(it) }
if (scansAdapter?.count == 1) {
updateAndClearFilter()
} else {
@@ -342,8 +345,8 @@ class HistoryFragment : Fragment() {
.show()
}
private fun askToRemoveScans(context: Context) {
AlertDialog.Builder(context)
private fun Context.askToRemoveScans() {
AlertDialog.Builder(this)
.setMessage(
if (filter == null) {
R.string.really_remove_all_scans
@@ -364,13 +367,16 @@ class HistoryFragment : Fragment() {
scope.launch {
val ac = activity ?: return@launch
progressView.useVisibility {
if (!hasWritePermission(ac) { askToExportToFile() }) {
// Write permission is only required before Android Q.
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q &&
!ac.hasWritePermission { askToExportToFile() }
) {
return@useVisibility
}
val options = context.resources.getStringArray(
val options = ac.resources.getStringArray(
R.array.export_options_values
)
val delimiter = alertDialog<String>(context) { resume ->
val delimiter = alertDialog<String>(ac) { resume ->
setTitle(R.string.export_as)
setItems(R.array.export_options_names) { _, which ->
resume(options[which])
@@ -386,11 +392,11 @@ class HistoryFragment : Fragment() {
)
} ?: return@useVisibility
val message = when (delimiter) {
"db" -> exportDatabase(ac, name)
"db" -> ac.exportDatabase(name)
else -> db.getScansDetailed(filter)?.use {
when (delimiter) {
"json" -> exportJson(context, name, it)
else -> exportCsv(context, name, it, delimiter)
"json" -> ac.exportJson(name, it)
else -> ac.exportCsv(name, it, delimiter)
}
} ?: false
}.toSaveResult()
@@ -401,11 +407,11 @@ class HistoryFragment : Fragment() {
}
}
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])
@@ -419,31 +425,31 @@ class HistoryFragment : Fragment() {
db.getScansDetailed(filter)?.use { cursor ->
val details = format.split(":")
text = when (details[0]) {
"text" -> exportText(cursor, details[1])
"csv" -> exportCsv(cursor, details[1])
else -> exportJson(cursor)
"text" -> cursor.exportText(details[1])
"csv" -> cursor.exportCsv(details[1])
else -> cursor.exportJson()
}
}
text?.let {
withContext(Dispatchers.Main) {
shareText(context, it)
context.shareText(it)
}
}
}
}
}
private fun exportText(cursor: Cursor, separator: String): String {
private fun Cursor.exportText(separator: String): String {
val sb = StringBuilder()
val contentIndex = cursor.getColumnIndex(Database.SCANS_CONTENT)
if (cursor.moveToFirst()) {
val contentIndex = getColumnIndex(Database.SCANS_CONTENT)
if (contentIndex > -1 && moveToFirst()) {
do {
val content = cursor.getString(contentIndex)
val content = getString(contentIndex)
if (content?.isNotEmpty() == true) {
sb.append(content)
sb.append(separator)
}
} while (cursor.moveToNext())
} while (moveToNext())
}
return sb.toString()
}
@@ -0,0 +1,92 @@
package de.markusfisch.android.binaryeye.fragment
import android.content.Context
import android.net.wifi.WifiManager
import android.net.wifi.WifiNetworkSuggestion
import android.os.Build
import android.os.Bundle
import android.support.annotation.RequiresApi
import android.support.v4.app.Fragment
import android.view.*
import android.widget.ArrayAdapter
import android.widget.ListView
import de.markusfisch.android.binaryeye.R
import de.markusfisch.android.binaryeye.view.setPaddingFromWindowInsets
import de.markusfisch.android.binaryeye.view.systemBarListViewScrollListener
import de.markusfisch.android.binaryeye.widget.toast
class NetworkSuggestionsFragment : Fragment() {
@RequiresApi(Build.VERSION_CODES.R)
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
state: Bundle?
): View? {
val ac = activity ?: return null
ac.setTitle(R.string.network_suggestions)
val view = inflater.inflate(
R.layout.fragment_network_suggestions,
container,
false
)
val wm = ac.applicationContext.getSystemService(
Context.WIFI_SERVICE
) as WifiManager
val suggestionArrayAdapter = ArrayAdapter(
ac,
android.R.layout.simple_list_item_checked,
wm.networkSuggestions.map {
Suggestion(
it.ssid ?: it.toString(),
it
)
}
)
val listView = view.findViewById<ListView>(R.id.suggestions)
listView.emptyView = view.findViewById(R.id.no_suggestions)
listView.adapter = suggestionArrayAdapter
listView.setOnScrollListener(systemBarListViewScrollListener)
(view.findViewById(R.id.inset_layout) as View).setPaddingFromWindowInsets()
listView.setPaddingFromWindowInsets()
view.findViewById<View>(R.id.remove).setOnClickListener {
val removeList = ArrayList<WifiNetworkSuggestion>()
val removeFromAdapter = ArrayList<Suggestion>()
val checked = listView.checkedItemPositions
val size = checked.size()
for (i in 0 until size) {
if (checked.valueAt(i)) {
val pos = checked.keyAt(i)
listView.setItemChecked(pos, false)
val suggestion = listView.getItemAtPosition(
pos
) as Suggestion
removeList.add(suggestion.suggestion)
removeFromAdapter.add(suggestion)
}
}
if (removeList.isNotEmpty()) {
removeFromAdapter.forEach {
suggestionArrayAdapter.remove(it)
}
wm.removeNetworkSuggestions(removeList)
} else {
ac.toast(
R.string.clear_network_suggestions_nothing_to_remove
)
}
}
return view
}
}
private data class Suggestion(
val label: String,
val suggestion: WifiNetworkSuggestion
) {
override fun toString() = label
}
@@ -16,13 +16,16 @@ import android.support.v7.preference.Preference
import android.support.v7.preference.PreferenceFragmentCompat
import android.support.v7.preference.PreferenceGroup
import de.markusfisch.android.binaryeye.R
import de.markusfisch.android.binaryeye.actions.wifi.removeNetworkSuggestions
import de.markusfisch.android.binaryeye.activity.SplashActivity
import de.markusfisch.android.binaryeye.app.addFragment
import de.markusfisch.android.binaryeye.app.hasBluetoothPermission
import de.markusfisch.android.binaryeye.app.prefs
import de.markusfisch.android.binaryeye.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
import de.markusfisch.android.binaryeye.bluetooth.setBluetoothHosts
class PreferencesFragment : PreferenceFragmentCompat() {
private val changeListener = object : OnSharedPreferenceChangeListener {
@@ -32,48 +35,82 @@ class PreferencesFragment : PreferenceFragmentCompat() {
) {
val preference = findPreference(key) ?: return
prefs.update()
if (preference.key == "custom_locale") {
activity?.restartApp()
} else {
setSummary(preference)
when (preference.key) {
"custom_locale" -> activity?.restartApp()
"beep_tone_name" -> {
beepConfirm()
setSummary(preference)
}
"send_scan_bluetooth" -> {
if (prefs.sendScanBluetooth &&
activity?.hasBluetoothPermission() == false
) {
prefs.sendScanBluetooth = false
}
setSummary(preference)
}
else -> setSummary(preference)
}
}
}
override fun onCreatePreferences(state: Bundle?, rootKey: String?) {
addPreferencesFromResource(R.xml.preferences)
activity?.setTitle(R.string.preferences)
setBluetoothResources()
wireClearNetworkPreferences()
}
private fun setBluetoothResources() {
if (prefs.sendScanBluetooth &&
activity?.hasBluetoothPermission() == true
) {
setBluetoothHosts(
findPreference("send_scan_bluetooth_host") as ListPreference
)
}
}
private fun wireClearNetworkPreferences() {
val pref = findPreference("clear_network_suggestions") ?: return
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
pref.isVisible = false
} else {
pref.setOnPreferenceClickListener {
askToClearNetworkSuggestions(context)
true
findPreference("clear_network_suggestions").apply {
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.Q) {
// From R+ we can query past network suggestions and
// make them editable.
setOnPreferenceClickListener {
fragmentManager.addFragment(NetworkSuggestionsFragment())
true
}
} else if (Build.VERSION.SDK_INT == Build.VERSION_CODES.Q) {
// On Q, we can only clear *all* suggestions.
// Note that previous versions of this app allowed
// adding network suggestions on Q as well, so we
// need to keep this option.
setOnPreferenceClickListener {
context.askToClearNetworkSuggestions()
true
}
} else {
// There are no network suggestions below Q.
isVisible = false
}
}
}
@RequiresApi(Build.VERSION_CODES.Q)
private fun askToClearNetworkSuggestions(context: Context) {
AlertDialog.Builder(context)
private fun Context.askToClearNetworkSuggestions() {
AlertDialog.Builder(this)
.setMessage(R.string.really_remove_all_networks)
.setPositiveButton(android.R.string.ok) { _, _ ->
clearNetworkSuggestions(context)
clearNetworkSuggestions()
}
.setNegativeButton(android.R.string.cancel) { _, _ -> }
.show()
}
@RequiresApi(Build.VERSION_CODES.Q)
private fun clearNetworkSuggestions(context: Context) {
context.toast(
if (
removeNetworkSuggestions(context) == WifiManager.STATUS_NETWORK_SUGGESTIONS_SUCCESS
private fun Context.clearNetworkSuggestions() {
toast(
if (removeAllNetworkSuggestions() ==
WifiManager.STATUS_NETWORK_SUGGESTIONS_SUCCESS
) {
R.string.clear_network_suggestions_success
} else {
@@ -84,6 +121,7 @@ class PreferencesFragment : PreferenceFragmentCompat() {
override fun onResume() {
super.onResume()
activity?.setTitle(R.string.preferences)
listView.setPaddingFromWindowInsets()
listView.removeOnScrollListener(systemBarRecyclerViewScrollListener)
listView.addOnScrollListener(systemBarRecyclerViewScrollListener)
@@ -105,6 +143,12 @@ class PreferencesFragment : PreferenceFragmentCompat() {
setTargetFragment(this@PreferencesFragment, 0)
show(fm, null)
}
} else if (preference.key == "send_scan_bluetooth_host") {
val ac = activity ?: return
if (ac.hasBluetoothPermission()) {
setBluetoothHosts(preference as ListPreference)
}
super.onDisplayPreferenceDialog(preference)
} else {
super.onDisplayPreferenceDialog(preference)
}
@@ -152,3 +196,9 @@ private fun Activity.restartApp() {
// Restart to begin with an unmodified Locale to follow system settings.
Runtime.getRuntime().exit(0)
}
@RequiresApi(Build.VERSION_CODES.Q)
private fun Context.removeAllNetworkSuggestions(): Int =
(applicationContext.getSystemService(
Context.WIFI_SERVICE
) as WifiManager).removeNetworkSuggestions(listOf())
@@ -50,15 +50,7 @@ class UrlDialogFragment : PreferenceDialogFragmentCompat() {
Scan(
"test",
null,
"none",
null,
null,
null,
null,
null,
null,
null,
null
"none"
).sendAsync(url, prefs.sendScanType) { code, body ->
textView.text = when {
code != null -> "$code"
@@ -11,9 +11,9 @@ import kotlin.math.max
import kotlin.math.min
import kotlin.math.roundToInt
fun loadImageUri(cr: ContentResolver, uri: Uri): Bitmap? = try {
fun ContentResolver.loadImageUri(uri: Uri): Bitmap? = try {
val options = BitmapFactory.Options()
cr.openInputStream(uri)?.use {
openInputStream(uri)?.use {
options.inJustDecodeBounds = true
BitmapFactory.decodeStream(it, null, options)
options.inSampleSize = calculateInSampleSize(
@@ -22,7 +22,7 @@ fun loadImageUri(cr: ContentResolver, uri: Uri): Bitmap? = try {
)
options.inJustDecodeBounds = false
}
cr.openInputStream(uri)?.use {
openInputStream(uri)?.use {
BitmapFactory.decodeStream(it, null, options)
}
} catch (e: IOException) {
@@ -49,8 +49,13 @@ private fun calculateInSampleSize(
return inSampleSize
}
fun crop(bitmap: Bitmap, rect: RectF, rotation: Float) = try {
val erected = erect(bitmap, rotation)
fun Bitmap.crop(
rect: RectF,
rotation: Float,
pivotX: Float,
pivotY: Float
) = try {
val erected = erect(rotation, pivotX, pivotY)
val w = erected.width
val h = erected.height
val x = max(0, (rect.left * w).roundToInt())
@@ -59,8 +64,8 @@ fun crop(bitmap: Bitmap, rect: RectF, rotation: Float) = try {
erected,
x,
y,
min(w - x, (rect.width() * w).roundToInt()),
min(h - y, (rect.height() * h).roundToInt())
min(w - x, (rect.right * w).roundToInt() - x),
min(h - y, (rect.bottom * h).roundToInt() - y)
)
} catch (e: OutOfMemoryError) {
null
@@ -68,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()
@@ -2,61 +2,86 @@ package de.markusfisch.android.binaryeye.graphics
import android.graphics.Matrix
import android.graphics.Rect
import com.google.zxing.ResultPoint
import android.graphics.RectF
import de.markusfisch.android.zxingcpp.ZxingCpp.Position
import kotlin.math.roundToInt
fun mapResultToView(
frameWidth: Int,
frameHeight: Int,
frameOrientation: Int,
viewRect: Rect,
resultPoints: Array<ResultPoint?>,
targetArray: FloatArray
): Int = getFrameToViewMatrix(
frameWidth,
frameHeight,
frameOrientation,
viewRect
).map(
resultPoints,
targetArray
data class FrameMetrics(
var width: Int = 0,
var height: Int = 0,
var orientation: Int = 0
)
fun getFrameToViewMatrix(
frameWidth: Int,
frameHeight: Int,
frameOrientation: Int,
viewRect: Rect
) = Matrix().apply {
// Normalize to frame dimensions for rotation.
postScale(
1f / frameWidth,
1f / frameHeight
)
// Rotate around center.
postRotate(frameOrientation.toFloat(), 0.5f, 0.5f)
// Scale up to view size.
postScale(viewRect.width().toFloat(), viewRect.height().toFloat())
// Apply view displacement.
postTranslate(viewRect.left.toFloat(), viewRect.top.toFloat())
fun 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.map(
resultPoints: Array<ResultPoint?>,
targetArray: FloatArray
): Int {
val max = targetArray.size
var i = 0
for (resultPoint in resultPoints) {
// Because ZXing apparently returns null in this array sometimes.
if (resultPoint == null) {
continue
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
}
targetArray[i++] = resultPoint.x
targetArray[i++] = resultPoint.y
if (i >= max) {
break
else -> {
uprightWidth = frameMetrics.width
uprightHeight = frameMetrics.height
}
}
mapPoints(targetArray, 0, targetArray, 0, i)
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
)
@@ -46,20 +46,18 @@ fun Boolean.toSaveResult() = if (this) {
R.string.error_saving_file
}
fun writeExternalFile(
context: Context,
fun Context.writeExternalFile(
fileName: String,
mimeType: String,
write: (outputStream: OutputStream) -> Unit
): Boolean = try {
openExternalOutputStream(context, fileName, mimeType).use { write(it) }
openExternalOutputStream(fileName, mimeType).use { write(it) }
true
} catch (e: IOException) {
false
}
private fun openExternalOutputStream(
context: Context,
private fun Context.openExternalOutputStream(
fileName: String,
mimeType: String
): OutputStream = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
@@ -75,7 +73,7 @@ private fun openExternalOutputStream(
}
FileOutputStream(file)
} else {
val resolver = context.contentResolver
val resolver = contentResolver
val uri = resolver.insert(
MediaStore.Downloads.EXTERNAL_CONTENT_URI,
ContentValues().apply {
@@ -0,0 +1,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
}
@@ -1,6 +1,6 @@
package de.markusfisch.android.binaryeye.net
import de.markusfisch.android.binaryeye.app.toHexString
import de.markusfisch.android.binaryeye.content.toHexString
import de.markusfisch.android.binaryeye.database.Scan
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@@ -14,9 +14,12 @@ import java.io.InputStreamReader
import java.net.HttpURLConnection
import java.net.ProtocolException
import java.net.URL
import java.net.URLEncoder
fun Scan.sendAsync(url: String, type: String, callback: (Int?, String?) -> Unit) {
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) {
@@ -47,34 +50,33 @@ private fun Scan.send(url: String, type: String): Response {
}
}
else -> request(
url + urlEncode(content)
url + content.urlEncode()
)
}
}
private fun Scan.asUrlArguments(): String = getMap().map { (k, v) ->
"${k}=${urlEncode(v)}"
"${k}=${v.urlEncode()}"
}.joinToString("&")
private fun Scan.asJson() = JSONObject().apply {
getMap().forEach { (k, v) -> put(k, v) }
}
private fun urlEncode(s: String) = URLEncoder.encode(s, "utf-8")
private fun Scan.getMap(): Map<String, String> = mapOf(
"content" to content,
"raw" to raw?.toHexString(),
"format" to format,
"errorCorrectionLevel" to errorCorrectionLevel,
"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,
"orientation" to orientation,
"otherMetaData" to otherMetaData,
"pdf417ExtraMetaData" to pdf417ExtraMetaData,
"possibleCountry" to possibleCountry,
"suggestedPrice" to suggestedPrice,
"upcEanExtension" to upcEanExtension,
"timestamp" to timestamp
"timestamp" to dateTime
).filterNullValues()
private fun Map<String, String?>.filterNullValues() =
@@ -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
}
@@ -1,11 +1,13 @@
package de.markusfisch.android.binaryeye.os
import android.content.Context
import android.os.Build
import android.os.VibrationEffect
import android.os.Vibrator
import de.markusfisch.android.binaryeye.app.prefs
private const val MILLISECONDS = 100L
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
)
@@ -13,17 +15,35 @@ 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 (!prefs.vibrate) {
return
}
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
if (beforeO) {
@Suppress("DEPRECATION")
vibrate(MILLISECONDS)
vibrate(DURATION_IN_MS)
} else {
vibrate(
VibrationEffect.createOneShot(
MILLISECONDS,
DURATION_IN_MS,
VibrationEffect.DEFAULT_AMPLITUDE
)
)
@@ -31,10 +51,7 @@ fun Vibrator.vibrate() {
}
fun Vibrator.error() {
if (!prefs.vibrate) {
return
}
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
if (beforeO) {
@Suppress("DEPRECATION")
vibrate(errorPatternTimings, -1)
} else {
@@ -2,31 +2,33 @@ 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 barcodeFormats = setOf(
"AZTEC",
"CODABAR",
"CODE_39",
"CODE_93",
"CODE_128",
"DATA_MATRIX",
"EAN_8",
"EAN_13",
"ITF",
"MAXICODE",
"PDF_417",
"QR_CODE",
"RSS_14",
"RSS_EXPANDED",
"UPC_A",
"UPC_E",
"UPC_EAN_EXTENSION"
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) {
@@ -58,39 +60,29 @@ class Preferences {
apply(ZOOM_BY_SWIPING, value)
field = value
}
var autoRotate = false
var autoRotate = true
set(value) {
// Immediately save this setting before it shouldn't change
// on the fly while scanning.
commit(AUTO_ROTATE, value)
apply(AUTO_ROTATE, value)
field = value
}
var tryHarder = false
set(value) {
// Immediately save this setting because it's only ever read
// before the camera is opened.
commit(TRY_HARDER, value)
apply(TRY_HARDER, value)
field = value
}
var bulkMode = false
set(value) {
// Immediately save this setting because it's only ever read
// before the camera is opened.
commit(BULK_MODE, value)
apply(BULK_MODE, value)
field = value
}
var bulkModeDelay = "500"
set(value) {
// Immediately save this setting because it's only ever read
// before the camera is opened.
commit(BULK_MODE_DELAY, value)
apply(BULK_MODE_DELAY, value)
field = value
}
var showToastInBulkMode = true
set(value) {
// Immediately save this setting because it's only ever read
// before the camera is opened.
commit(SHOW_TOAST_IN_BULK_MODE, value)
apply(SHOW_TOAST_IN_BULK_MODE, value)
field = value
}
var vibrate = true
@@ -98,6 +90,16 @@ class Preferences {
apply(VIBRATE, value)
field = value
}
var beep = false
set(value) {
apply(BEEP, value)
field = value
}
var beepToneName = "tone_prop_beep"
set(value) {
apply(BEEP_TONE_NAME, value)
field = value
}
var useHistory = false
set(value) {
apply(USE_HISTORY, value)
@@ -128,6 +130,11 @@ class Preferences {
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)
@@ -143,6 +150,11 @@ class Preferences {
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)
@@ -153,9 +165,21 @@ class Preferences {
apply(SEND_SCAN_TYPE, value)
field = value
}
var sendScanBluetooth = false
set(value) {
apply(SEND_SCAN_BLUETOOTH, value)
field = value
}
var sendScanBluetoothHost: String = ""
set(value) {
apply(SEND_SCAN_BLUETOOTH_HOST, value)
field = value
}
var customLocale: String = ""
set(value) {
apply(CUSTOM_LOCALE, value)
// Make sure this setting is written immediately because
// the app is about to restart.
commit(CUSTOM_LOCALE, value)
field = value
}
var indexOfLastSelectedFormat: Int = 0
@@ -163,11 +187,19 @@ class Preferences {
apply(INDEX_OF_LAST_SELECTED_FORMAT, value)
field = value
}
var forceCompat: Boolean = false
var indexOfLastSelectedEcLevel: Int = 0
set(value) {
// Since the app may be about to crash when forceCompat is set,
// it's necessary to `commit()` this synchronously.
commit(FORCE_COMPAT, value)
apply(INDEX_OF_LAST_SELECTED_EC_LEVEL, value)
field = value
}
var freeRotation = true
set(value) {
apply(FREE_ROTATION, value)
field = value
}
var expandEscapeSequences = true
set(value) {
apply(EXPAND_ESCAPE_SEQUENCES, value)
field = value
}
@@ -205,6 +237,10 @@ class Preferences {
showToastInBulkMode
)
vibrate = preferences.getBoolean(VIBRATE, vibrate)
beep = preferences.getBoolean(BEEP, beep)
preferences.getString(BEEP_TONE_NAME, beepToneName)?.also {
beepToneName = it
}
useHistory = preferences.getBoolean(USE_HISTORY, useHistory)
ignoreConsecutiveDuplicates = preferences.getBoolean(
IGNORE_CONSECUTIVE_DUPLICATES,
@@ -220,6 +256,7 @@ class Preferences {
)
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
@@ -230,12 +267,26 @@ class Preferences {
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
}
sendScanBluetooth = preferences.getBoolean(
SEND_SCAN_BLUETOOTH,
sendScanBluetooth
)
preferences.getString(
SEND_SCAN_BLUETOOTH_HOST,
sendScanBluetoothHost
)?.also {
sendScanBluetoothHost = it
}
preferences.getString(CUSTOM_LOCALE, customLocale)?.also {
customLocale = it
}
@@ -243,7 +294,24 @@ class Preferences {
INDEX_OF_LAST_SELECTED_FORMAT,
indexOfLastSelectedFormat
)
forceCompat = preferences.getBoolean(FORCE_COMPAT, forceCompat)
indexOfLastSelectedEcLevel = preferences.getInt(
INDEX_OF_LAST_SELECTED_EC_LEVEL,
indexOfLastSelectedEcLevel
)
freeRotation = preferences.getBoolean(FREE_ROTATION, freeRotation)
expandEscapeSequences = preferences.getBoolean(
EXPAND_ESCAPE_SEQUENCES,
expandEscapeSequences
)
}
fun beepTone() = when (beepToneName) {
"tone_cdma_confirm" -> ToneGenerator.TONE_CDMA_CONFIRM
"tone_sup_radio_ack" -> ToneGenerator.TONE_SUP_RADIO_ACK
"tone_prop_ack" -> ToneGenerator.TONE_PROP_ACK
"tone_prop_beep" -> ToneGenerator.TONE_PROP_BEEP
"tone_prop_beep2" -> ToneGenerator.TONE_PROP_BEEP2
else -> ToneGenerator.TONE_PROP_BEEP
}
private fun put(label: String, value: Boolean) =
@@ -252,14 +320,6 @@ class Preferences {
private fun put(label: String, value: String) =
preferences.edit().putString(label, value)
private fun commit(label: String, value: Boolean) {
put(label, value).commit()
}
private fun commit(label: String, value: String) {
put(label, value).commit()
}
private fun apply(label: String, value: Boolean) {
put(label, value).apply()
}
@@ -272,37 +332,49 @@ class Preferences {
preferences.edit().putInt(label, value).apply()
}
private fun commit(label: String, value: String) {
preferences.edit().putString(label, value).commit()
}
@RequiresApi(Build.VERSION_CODES.HONEYCOMB)
private fun apply(label: String, value: Set<String>) {
preferences.edit().putStringSet(label, value).apply()
}
companion object {
const val BARCODE_FORMATS = "barcode_formats"
const val CROP_HANDLE_X = "crop_handle_x"
const val CROP_HANDLE_Y = "crop_handle_y"
const val CROP_HANDLE_ORIENTATION = "crop_handle_orientation"
const val SHOW_CROP_HANDLE = "show_crop_handle"
const val ZOOM_BY_SWIPING = "zoom_by_swiping"
const val AUTO_ROTATE = "auto_rotate"
const val TRY_HARDER = "try_harder"
const val BULK_MODE = "bulk_mode"
const val BULK_MODE_DELAY = "bulk_mode_delay"
const val SHOW_TOAST_IN_BULK_MODE = "show_toast_in_bulk_mode"
const val VIBRATE = "vibrate"
const val USE_HISTORY = "use_history"
const val IGNORE_CONSECUTIVE_DUPLICATES = "ignore_consecutive_duplicates"
const val OPEN_IMMEDIATELY = "open_immediately"
const val COPY_IMMEDIATELY = "copy_immediately"
const val SHOW_META_DATA = "show_meta_data"
const val SHOW_HEX_DUMP = "show_hex_dump"
const val CLOSE_AUTOMATICALLY = "close_automatically"
const val DEFAULT_SEARCH_URL = "default_search_url"
const val OPEN_WITH_URL = "open_with_url"
const val SEND_SCAN_URL = "send_scan_url"
const val SEND_SCAN_TYPE = "send_scan_type"
const val CUSTOM_LOCALE = "custom_locale"
const val INDEX_OF_LAST_SELECTED_FORMAT = "index_of_last_selected_format"
const val FORCE_COMPAT = "force_compat"
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 SEND_SCAN_BLUETOOTH = "send_scan_bluetooth"
private const val SEND_SCAN_BLUETOOTH_HOST = "send_scan_bluetooth_host"
private const val CUSTOM_LOCALE = "custom_locale"
private const val INDEX_OF_LAST_SELECTED_FORMAT = "index_of_last_selected_format"
private const val INDEX_OF_LAST_SELECTED_EC_LEVEL = "index_of_last_selected_ec_level"
private const val FREE_ROTATION = "free_rotation"
private const val EXPAND_ESCAPE_SEQUENCES = "expand_escape_sequences"
}
}
@@ -1,42 +0,0 @@
package de.markusfisch.android.binaryeye.rs
import android.graphics.Bitmap
import android.graphics.Canvas
import android.support.v8.renderscript.Allocation
import android.support.v8.renderscript.RenderScript
fun fixTransparency(rs: RenderScript, bitmap: Bitmap?): Bitmap? {
bitmap ?: return null
val result = analyze(rs, bitmap)
return if (result.hasTransparentPixels) {
val copy = Bitmap.createBitmap(
bitmap.width,
bitmap.height,
bitmap.config
)
Canvas(copy).apply {
drawColor(result.backgroundColor)
drawBitmap(bitmap, 0f, 0f, null)
}
copy
} else {
bitmap
}
}
private fun analyze(rs: RenderScript, bitmap: Bitmap): Result {
val analyzeScript = ScriptC_analyze(rs)
val bitmapAllocation = Allocation.createFromBitmap(rs, bitmap)
val result = analyzeScript.reduce_analyze(bitmapAllocation).get()
bitmapAllocation.destroy()
analyzeScript.destroy()
return Result(
result.x > 0,
if (result.y > 0) 0xff000000.toInt() else 0xffffffff.toInt()
)
}
private data class Result(
val hasTransparentPixels: Boolean,
val backgroundColor: Int
)
@@ -1,156 +0,0 @@
package de.markusfisch.android.binaryeye.rs
import android.graphics.Rect
import android.support.v8.renderscript.*
import kotlin.math.max
import kotlin.math.roundToInt
private const val SCALE_FACTOR = .75f
class Preprocessor(
rs: RenderScript,
width: Int,
height: Int,
private val roi: Rect?
) {
var outWidth = 0
var outHeight = 0
private val resizeScript = ScriptIntrinsicResize.create(rs)
private val rotateScript = ScriptC_rotate(rs)
private var yuvType: Type? = null
private var yuvAlloc: Allocation? = null
private var roiType: Type? = null
private var roiAlloc: Allocation? = null
private var resizedType: Type? = null
private var resizedAlloc: Allocation? = null
private var rotatedType: Type? = null
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
)
if (roi != null) {
val roiWidth = roi.width()
val roiHeight = roi.height()
roiType = Type.createXY(
rs,
Element.U8(rs),
roiWidth,
roiHeight
)
roiAlloc = Allocation.createTyped(
rs,
roiType,
Allocation.USAGE_SCRIPT
)
outWidth = (roiWidth * SCALE_FACTOR).roundToInt()
outHeight = (roiHeight * SCALE_FACTOR).roundToInt()
// Make sure the dimensions are always a multiple of 4.
outWidth -= outWidth % 4
outHeight -= outHeight % 4
// Make sure the dimensions are always greater than 4.
outWidth = max(4, outWidth)
outHeight = max(4, outHeight)
} else {
outWidth = (width * SCALE_FACTOR).roundToInt()
outHeight = (height * SCALE_FACTOR).roundToInt()
}
resizedType = Type.createXY(
rs,
Element.U8(rs),
outWidth,
outHeight
)
resizedAlloc = Allocation.createTyped(
rs,
resizedType,
Allocation.USAGE_SCRIPT
)
rotatedType = Type.createXY(
rs,
Element.U8(rs),
outHeight,
outWidth
)
rotatedAlloc = Allocation.createTyped(
rs,
rotatedType,
Allocation.USAGE_SCRIPT
)
}
fun destroy() {
yuvType?.destroy()
yuvType = null
yuvAlloc?.destroy()
yuvAlloc = null
roiType?.destroy()
roiType = null
roiAlloc?.destroy()
roiAlloc = null
resizedType?.destroy()
resizedType = null
resizedAlloc?.destroy()
resizedAlloc = null
rotatedType?.destroy()
rotatedType = null
rotatedAlloc?.destroy()
rotatedAlloc = null
resizeScript.destroy()
rotateScript.destroy()
}
fun resizeOnly(frame: ByteArray) {
resize(frame)
resizedAlloc?.copyTo(frame)
}
fun resizeAndRotate(frame: ByteArray) {
resize(frame)
val t = resizedType ?: return
rotateScript._inImage = resizedAlloc
rotateScript._inWidth = t.x
rotateScript._inHeight = t.y
rotateScript.forEach_rotate90(
rotatedAlloc, // Ignored in kernel, just to satisfy forEach.
rotatedAlloc
)
rotatedAlloc?.copyTo(frame)
}
private fun resize(frame: ByteArray) {
yuvAlloc?.copyFrom(frame)
resizeScript.setInput(
if (roi != null) {
roiAlloc?.copy2DRangeFrom(
0, 0,
roi.width(), roi.height(),
yuvAlloc,
max(0, roi.left), max(0, roi.top)
)
roiAlloc
} else {
yuvAlloc
}
)
resizeScript.forEach_bicubic(resizedAlloc)
}
}
@@ -8,11 +8,10 @@ import de.markusfisch.android.binaryeye.activity.CameraActivity
@RequiresApi(Build.VERSION_CODES.N)
class ScanTileService : TileService() {
@RequiresApi(Build.VERSION_CODES.N)
override fun onClick() {
super.onClick()
val intent = Intent(applicationContext, CameraActivity::class.java)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
startActivityAndCollapse(intent)
}
}
}
@@ -0,0 +1,74 @@
package de.markusfisch.android.binaryeye.text
/**
* Copyright (c) 2000, Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// This code is based on Google's own implementation:
// http://www.java2s.com/example/android-utility-method/string-escape/javascriptunescape-string-s-dc359.html
fun String.unescape(): String {
val len = length
var i = 0
fun String.expand() = when (val ec = get(i++)) {
'n' -> '\n'
'r' -> '\r'
't' -> '\t'
'b' -> '\b'
'\\', '\"', '\'', '<', '>', '|' -> ec
'0', '1', '2', '3', '4', '5', '6', '7' -> {
val limit = if (ec < '4') 3 else 2
var l = 1
--i // Back to index of first digit.
while (l < limit &&
i + l < len &&
get(i + l) in '0'..'7'
) {
++l
}
val from = i
i += l
substring(from, i).toInt(8).toChar()
}
'x', 'u' -> {
val l = if (ec == 'u') 4 else 2
val hexCode = try {
substring(i, i + l)
} catch (_: IndexOutOfBoundsException) {
throw IllegalArgumentException(
"Invalid unicode sequence [${substring(i)}] at index $i"
)
}
val unicodeValue = try {
hexCode.toInt(16)
} catch (_: NumberFormatException) {
throw IllegalArgumentException(
"Invalid unicode sequence [$hexCode] at index $i"
)
}
i += l
unicodeValue.toChar()
}
else -> throw IllegalArgumentException(
"Unknown escape code [$ec] at index $i"
)
}
val sb = StringBuilder()
while (i < len) {
val ch = get(i++)
sb.append(if (ch == '\\' && i < len) expand() else ch)
}
return sb.toString()
}
@@ -0,0 +1,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 (prefs.beep && !isSilent()) {
beepError()
}
}
private fun Context.isSilent(): Boolean {
val am = getSystemService(Context.AUDIO_SERVICE) as AudioManager
return when (am.ringerMode) {
AudioManager.RINGER_MODE_SILENT,
AudioManager.RINGER_MODE_VIBRATE -> true
else -> false
}
}
@@ -21,12 +21,12 @@ import kotlin.math.roundToInt
class DetectorView : View {
val coordinates = FloatArray(32)
val currentOrientation = resources.configuration.orientation
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()
@@ -78,25 +78,41 @@ class DetectorView : View {
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) :
super(context, attrs, defStyleAttr)
fun setCropHandlePos(x: Int, y: Int, orientation: Int) {
if (orientation == currentOrientation) {
handlePos.set(x, y)
} else {
handlePos.set(y, x)
}
if (x > -1) {
handleActive = true
}
fun saveCropHandlePos() {
val pos = getCropHandlePos()
prefs.cropHandleX = pos.x
prefs.cropHandleY = pos.y
prefs.cropHandleOrientation = currentOrientation
}
fun getCropHandlePos() = if (handleActive) {
private fun getCropHandlePos() = if (handleActive) {
handlePos
} else {
Point(-1, -1)
}
fun update(numberOfResultPoints: Int) {
coordinatesLast = numberOfResultPoints * 2
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)
@@ -149,8 +165,8 @@ class DetectorView : View {
MotionEvent.ACTION_MOVE -> {
if (handleGrabbed) {
handlePos.set(x, y)
handleActive =
handleActive or (distSq(handlePos, touchDown) > minMoveThresholdSq)
handleActive = handleActive ||
distSq(handlePos, touchDown) > minMoveThresholdSq
if (handleActive) {
updateClipRect()
invalidate()
@@ -201,10 +217,10 @@ class DetectorView : View {
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
// 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
// Check if handle is close to a screen corner.
((abs(cy - minY) < distToFull || abs(maxY - cy) < distToFull) &&
abs(dx - center.x) < distToFull)
) {
@@ -245,23 +261,18 @@ class DetectorView : View {
override fun onDraw(canvas: Canvas) {
if (handleActive) {
drawClip(canvas)
canvas.drawClip()
}
drawDots(canvas)
canvas.drawDots()
if (prefs.showCropHandle) {
canvas.drawBitmap(
handleBitmap,
(handlePos.x - handleXRadius).toFloat(),
(handlePos.y - handleYRadius).toFloat(),
null
)
canvas.drawHandle()
}
}
private fun drawDots(canvas: Canvas) {
private fun Canvas.drawDots() {
var i = 0
while (i < coordinatesLast) {
canvas.drawCircle(
drawCircle(
coordinates[i++],
coordinates[i++],
dotRadius,
@@ -270,15 +281,15 @@ class DetectorView : View {
}
}
private fun drawClip(canvas: Canvas) {
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()
canvas.save()
canvas.clipOutPathCompat(
save()
clipOutPathCompat(
calculateRoundedRectPath(
roi.left.toFloat(),
roi.top.toFloat(),
@@ -288,13 +299,22 @@ class DetectorView : View {
radius
)
)
canvas.drawColor(shadeColor)
canvas.restore()
drawColor(shadeColor)
restore()
} else {
canvas.drawRect(roi, roiPaint)
drawRect(roi, roiPaint)
}
}
private fun Canvas.drawHandle() {
drawBitmap(
handleBitmap,
(handlePos.x - handleXRadius).toFloat(),
(handlePos.y - handleYRadius).toFloat(),
null
)
}
private fun updateClipRect() {
clampHandlePos()
val dx = abs(handlePos.x - center.x)
@@ -1,208 +0,0 @@
package de.markusfisch.android.binaryeye.zxing
import android.graphics.Bitmap
import com.google.zxing.*
import com.google.zxing.common.BitMatrix
import com.google.zxing.common.HybridBinarizer
import java.util.*
class Zxing(possibleResultPoint: ResultPointCallback? = null) {
private val hints = EnumMap<DecodeHintType, Any>(DecodeHintType::class.java)
private val multiFormatReader = MultiFormatReader()
init {
possibleResultPoint?.let {
hints[DecodeHintType.NEED_RESULT_POINT_CALLBACK] = it
}
}
fun updateHints(tryHarder: Boolean, formats: Set<String>) {
if (tryHarder) {
hints[DecodeHintType.TRY_HARDER] = java.lang.Boolean.TRUE
} else {
hints.remove(DecodeHintType.TRY_HARDER)
}
setDecodeFormats(formats)
}
private fun setDecodeFormats(formats: Set<String>) {
val decodeFormats = EnumSet.noneOf(
BarcodeFormat::class.java
)
decodeFormats.addAll(
EnumSet.copyOf(
formats.map { BarcodeFormat.valueOf(it) }
)
)
hints[DecodeHintType.POSSIBLE_FORMATS] = decodeFormats
}
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? =
decode(bitmap, false) ?: decode(bitmap, true)
private fun decode(bitmap: Bitmap, invert: Boolean = false): Result? {
val pixels = IntArray(bitmap.width * bitmap.height)
return decode(pixels, bitmap, invert)
}
private fun decode(
pixels: IntArray,
bitmap: Bitmap,
invert: Boolean = false
): Result? {
val width = bitmap.width
val height = bitmap.height
if (bitmap.config != Bitmap.Config.ARGB_8888) {
bitmap.copy(Bitmap.Config.ARGB_8888, true)
} else {
bitmap
}.getPixels(pixels, 0, width, 0, 0, width, height)
return decodeLuminanceSource(
RGBLuminanceSource(width, height, pixels),
invert
)
}
private fun decodeLuminanceSource(
source: LuminanceSource,
invert: Boolean
): Result? {
return decodeLuminanceSource(
if (invert) {
source.invert()
} else {
source
}
)
}
private fun decodeLuminanceSource(source: LuminanceSource): Result? {
val bitmap = BinaryBitmap(HybridBinarizer(source))
return try {
multiFormatReader.decode(bitmap, hints)
} catch (e: Exception) {
// Usually it's bad practice to blindly catch *all* exceptions
// because this can hide problems we want to know about. But
// since ZXing has some errors (like all software) and I don't
// want this app to break in the hands of my users, which won't
// see the stack trace anyway, it's better to just catch all
// exceptions here and live on.
null
} finally {
multiFormatReader.reset()
}
}
}
private const val BLACK = 0xff000000.toInt()
private const val WHITE = 0xffffffff.toInt()
fun encodeAsBitmap(
text: String,
format: BarcodeFormat,
width: Int,
height: Int,
hints: EnumMap<EncodeHintType, Any>? = null
): Bitmap? {
val bitMatrix = encode(text, format, hints, width, height)
val w = bitMatrix.width
val h = bitMatrix.height
val pixels = IntArray(w * h)
var offset = 0
for (y in 0 until h) {
for (x in 0 until w) {
pixels[offset + x] = if (bitMatrix.get(x, y)) {
BLACK
} else {
WHITE
}
}
offset += w
}
val bitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888)
bitmap.setPixels(pixels, 0, w, 0, 0, w, h)
return bitmap
}
fun encodeAsSvg(
text: String,
format: BarcodeFormat,
hints: EnumMap<EncodeHintType, Any>? = null
): String {
val bitMatrix = encode(text, format, hints)
val sb = StringBuilder()
val w = bitMatrix.width
var h = bitMatrix.height
val moduleHeight = if (h == 1) w / 2 else 1
for (y in 0 until h) {
for (x in 0 until w) {
if (bitMatrix.get(x, y)) {
sb.append(" M${x},${y}h1v${moduleHeight}h-1z")
}
}
}
h *= moduleHeight
return """<svg width="$w" height="$h"
viewBox="0 0 $w $h"
xmlns="http://www.w3.org/2000/svg">
<path d="$sb"/>
</svg>
"""
}
fun encodeAsText(
text: String,
format: BarcodeFormat,
hints: EnumMap<EncodeHintType, Any>? = null
): String {
val bitMatrix = encode(text, format, hints)
val w = bitMatrix.width
val h = bitMatrix.height
val sb = StringBuilder()
for (y in 0 until h) {
for (x in 0 until w) {
sb.append(if (bitMatrix.get(x, y)) "" else " ")
}
sb.append("\n")
}
return sb.toString()
}
private fun encode(
text: String,
format: BarcodeFormat,
encodeHints: EnumMap<EncodeHintType, Any>? = null,
width: Int = 0,
height: Int = 0
): BitMatrix {
val hints = encodeHints ?: EnumMap<EncodeHintType, Any>(EncodeHintType::class.java)
if (!hints.contains(EncodeHintType.CHARACTER_SET)) {
hints[EncodeHintType.CHARACTER_SET] = "utf-8"
}
return MultiFormatWriter().encode(
text,
format,
width,
height,
hints
)
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.9 KiB

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 731 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 899 B

After

Width:  |  Height:  |  Size: 583 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 755 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 899 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 698 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.3 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.8 KiB

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

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