Compare commits

...
378 Commits
Author SHA1 Message Date
Markus Fisch c9c1b2f5bf Advance version number to 1.65.1 2025-03-12 12:23:43 +01:00
Markus Fisch 1dbc0f0fa1 Fix fastlane changelogs
And insert the missing log for version code 141.
2025-03-12 12:20:20 +01:00
solokotandGitHub ab8170ee72 Update Russian translation
Fix machine translation
2025-03-12 12:11:05 +01:00
Markus Fisch 551464dc6f Refactor for an early return when content is empty
Easier to read and less indent.
2025-03-12 00:54:59 +01:00
Markus Fisch c3aac0a1e1 Set a default search URL for simple mode
So any (otherwise unregistered) content is always redirected to
the browser.
2025-03-12 00:53:37 +01:00
Markus Fisch d678e1cfd0 Hide keyboard when expanding recreated barcode
So the layout of `BarcodeFragment` isn't affected by it.
2025-03-12 00:49:30 +01:00
Markus Fisch a445ade877 Reduce height for 1D barcodes
1D barcodes don't need as much vertical space.
2025-03-12 00:48:20 +01:00
Markus Fisch 1fd0b7637f Add execute argument for binaryeye://encode
For immediate code generation.
2025-03-12 00:37:47 +01:00
Markus Fisch fdc88df897 Remember quiet zone settings
And use a quiet zone by default because it's recommended.
2025-03-12 00:19:41 +01:00
Markus Fisch d0f133ae00 Fix showing a scan from history
When `openImmediately` is `true`.

Of course, the action should only be executed when we're coming
from the scanner, not the history.
2025-03-12 00:11:14 +01:00
Markus Fisch 67646261e7 Remove unnecessary id field 2025-03-12 00:08:11 +01:00
Markus Fisch faeae9e904 Advance version number to 1.65.0 2025-03-11 23:23:55 +01:00
Markus Fisch 77b21a9e83 Add a welcome screen to choose a mode
To address these bad reviews when people don't understand that
they have to press this little green button.
2025-03-11 23:17:36 +01:00
Markus Fisch cbe7860a3d Add a deep link to encode a barcode 2025-03-11 22:58:45 +01:00
Markus Fisch fc29267d3d Update ZXing-C++ library 2025-03-11 22:58:10 +01:00
Markus Fisch 890506328b Fix show crop handle setting
This switch isn't part of the crop configuration, but controls if
the crop handle is actually visible or not.
2025-03-11 22:58:10 +01:00
Markus Fisch 04036b6cd6 Use apply scope function to initialize views
And remove all this repetition of names.
2025-03-11 22:58:10 +01:00
Markus Fisch 2602021c1c Remove unused import from PickActivity 2025-03-11 22:58:10 +01:00
Markus Fisch 17a0bdc0a9 Allow shrinking of image samples too
So they can fit in the crop window.
2025-03-11 22:58:10 +01:00
Markus Fisch d71a220eca Remember zoom level of barcodes in view
Solves #490.
2025-03-11 22:58:10 +01:00
Markus Fisch 0c2e711d79 Add a comment why Scan.text can be an empty string
So it's immediately clear why `Scan.text` can be empty.
2025-03-11 22:58:10 +01:00
Markus Fisch ca342a83d3 Configure ZXingCpp to return plain text only
And remove the HRI decoration because it's confusing people and
doesn't work well when the barcode is re-generated from this
content.

Alternatively, we could filter the HRI decoration for generation,
but that's probably overkill.

Solves #506.
2025-03-11 22:58:10 +01:00
Markus Fisch 3c44ff56eb Refactor SCAN_CONTENT to SCAN_TEXT
To match the refactoring of `Scan.text` in 5189fc.
2025-03-11 22:58:10 +01:00
bovirusandGitHub bcae64c0d0 Italian language update 2025-03-11 22:57:50 +01:00
Markus Fisch c2f673729b Advance version number to 1.64.2 2025-03-10 21:23:52 +01:00
Markus Fisch 2043e0f349 Use separate crop handle config for image scanning
These are two different use cases.
2025-03-10 21:11:41 +01:00
Markus Fisch b93eb5963e Use raw content for Barcode<*> when text is empty
So binary content is supported here, too.
2025-03-10 11:01:15 +01:00
Markus Fisch 5189fc9c5a Refactor Scan.content to Scan.text
Because `text` is more specific, and the term `content` is often
used for generics.
2025-03-10 10:54:44 +01:00
Markus Fisch 2d311ff92a Fix initializing dataMask
And show this item for QR Codes only.
2025-03-10 19:50:19 +01:00
solokotandGitHub 14bb08003d Update Russian translation 2025-03-10 10:31:50 +01:00
Markus Fisch 42f9887030 Advance version number to 1.64.1 2025-03-09 22:39:37 +01:00
Markus Fisch a59d241b50 Handle empty input for recreation 2025-03-09 22:35:28 +01:00
Markus Fisch 9f31160c7b Add missing symbol fields to initial db creation 2025-03-09 22:14:06 +01:00
Markus Fisch 2eb4e51c17 Advance version number to 1.64.0 2025-03-09 19:43:00 +01:00
Markus Fisch 53a554e7a2 Add a menu item to find a specific barcode
The searched barcode needs to include the given term.

Solves #516.
2025-03-09 19:09:19 +01:00
Markus Fisch 7f6887716a Use Barcode for recreation too
And move `Barcode` into its own file.

So a given `BitMatrix` can be used for `BarcodeFragment` (and sharing
and exporting) as well.
2025-03-09 14:19:27 +01:00
Markus Fisch 5ef93802ce Make margin optional for BarcodeFragment
Just like `ecLevel` and `colors`.
2025-03-09 12:57:23 +01:00
Markus Fisch c1eff08bb9 Remove unnecessary semicolons
Too much context change.
2025-03-09 12:56:48 +01:00
Markus Fisch 8798d44ec7 Fix showing QR Code data mask
The first data mask is 0.
2025-03-09 12:55:55 +01:00
Markus Fisch d4e31ff97e Update ZXing-C++ library
To fix data mask initialization.
2025-03-09 12:55:28 +01:00
solokotandGitHub ae8b08a85a Update Russian translation 2025-03-09 12:10:10 +01:00
Markus Fisch 09430d776e Replace margin with add quiet zone switch
Giving a margin in pixels doesn't scale with (the size) of the
barcode.
2025-03-08 22:18:04 +01:00
Markus Fisch ef88c3179d Use detected barcode matrix for recreation
If it is provided by ZXing-C++. Currently, this is only supported
for some 2D barcodes. But it's probably only really useful for them
because there's padding and and other stuff that 1D barcodes don't
have.

This may solve #507.
2025-03-08 21:57:13 +01:00
Markus Fisch c732ec726b Show data mask of QR Codes 2025-03-08 21:54:54 +01:00
Markus Fisch 801c768df4 Update ZXing-C++ library 2025-03-08 21:53:19 +01:00
Markus Fisch 552a2f6d57 Only color status bar in context mode before SDK35
Has no effect from SDK35 on anyway.
2025-03-04 21:31:55 +01:00
Markus Fisch 7fa8810197 Make getIdOfScanByContent() private
Because it can be.
2025-03-04 21:27:12 +01:00
Markus Fisch 254af25bbb Update database functions to non-nullable Cursors
Because `SQLiteDatabase.rawQuery` returns a non-nullable `Cursor` now.
2025-03-07 16:55:58 +01:00
Markus Fisch a27656c174 Add a translucent nav bar background on SDK35+
Because the system doesn't draw one anymore.
2025-03-05 15:24:44 +01:00
Markus Fisch 2ca55ebccc Fix setting window insets on SDK35+
The bottom padding from the navigation bar needs to be set, too.
2025-03-04 19:47:41 +01:00
Markus Fisch ec1d40942d Optimize vector drawables with avocado 2025-03-04 00:21:30 +01:00
Markus Fisch 7852e5ce9d Use color literal in vector drawable
Because resource references will not work correctly in images
generated for API < 21.
2025-03-04 00:20:31 +01:00
Markus Fisch f36a81d4a0 Update Kotlin version 2025-03-04 12:50:50 +01:00
Markus Fisch d86ea4073c Remove unused quantity for Brazilian Portuguese
Following the corresponding lint warning.
2025-03-04 12:50:37 +01:00
Markus Fisch a69530621d Add autoVerify for URL intent filter 2025-03-04 12:50:28 +01:00
Markus Fisch 3c0f29a768 Add targetApi notation for quick settings tile
Since this is only available from Android N+.
2025-03-04 00:09:12 +01:00
Markus Fisch ccf5157fa7 Mark autofocus as optional
As it isn't strictly required.
2025-03-04 00:08:58 +01:00
Markus Fisch c1ebb1c154 Specify an explicit locale for String.format()
To silent the corresponding lint warning.

Doesn't really matter here probably.
2025-03-04 00:07:59 +01:00
Markus Fisch a4778bcc02 Replace removeFirst with removeAt
Because `java.util.List#removeFirst` requires SDK35, and resolved
to a Kotlin stdlib extension function before.
2025-03-04 00:04:04 +01:00
Markus Fisch 24d2c6d8d6 Only color system bars before SDK35
And ignore deprecation warning in this branch.

System bars no longer have a background from SDK35+.
2025-03-03 23:59:14 +01:00
Markus Fisch 0f3a07f602 Name SDK that requires a change explicitly
Instead of using greater than, because this way we can search
for VANILLA_ICE_CREAM changes.
2025-03-04 12:34:33 +01:00
Markus Fisch 751d8932f8 Immediately replace toasts
Instead of showing them in sequence.

Because in this app, only the last message is important.

Speeds up continuous scanning. Especially when the scanned code is
also forwarded and the server result is displayed in the next toast
(see #523).
2025-03-03 23:47:16 +01:00
Markus Fisch 9c2d063c24 Update tools version 2025-03-03 20:55:16 +01:00
Markus Fisch 0f9bbf5b2f Extend toolbar under status bar on SDK35+
Because SDK 35 is edge-to-edge by default, and doesn't color the
status or navigation bar anymore.
2025-02-18 21:34:27 +01:00
Markus Fisch 530280981e Fix image rotation calculation 2025-02-18 21:30:46 +01:00
Markus Fisch ddfbb305c7 Check Bitmap.getConfig() for null before use 2025-02-18 21:27:30 +01:00
Markus Fisch 8d1a29922b Update to SDK 35 2025-02-18 21:26:52 +01:00
Markus Fisch c710f7c6ab Move Toolate link below the reference section
Reference-style links should be derefenced after the content.

Also add a proper header for the matter.
2025-02-14 20:40:33 +01:00
Yurt PageandGitHub 111c587eef Add link to Toolate in CONTRIBUTING 2025-02-14 20:39:35 +01:00
Markus Fisch 264e494487 Update tools version and gradle wrapper 2025-01-23 19:59:08 +01:00
Markus Fisch 8135b2a3dc Update build tools 2024-12-19 22:23:45 +01:00
kraloandGitHub dce62b70d3 Update README to explain non-clickable binaryeye:// url 2024-12-05 21:56:06 +01:00
Markus Fisch 533812c638 Revert removing http deep linking intent filter
This is required for "http(s)://markusfisch.de/BinaryEye?ret=".

This reverts commit 12554e63e2.
2024-12-05 19:52:56 +01:00
Markus Fisch 5dc5bed281 Fix indent in Portuguese (Brazil) translation 2024-12-01 17:58:36 +01:00
Too Late (bot)andGitHub 2a13561962 Update Portuguese (Brazil) translation 2024-12-01 17:57:46 +01:00
Markus Fisch 1476f41cda Advance version number to 1.63.13 2024-11-24 19:37:04 +01:00
Markus Fisch 0da1ffbe02 Use JavaVersion.VERSION_1_8 for compatibility
Instead of just the version literal. Using the literal breaks
when the host has a higher Java version installed.
2024-11-19 20:08:34 +01:00
Markus Fisch 980679b6a3 Avoid UTF-8 conversion for binary input data
Otherwise the data is modified and doesn't match the desired input.

Still, it's probably better to introduce another CheckBox to explicity
control charset conversion.
2024-11-17 16:50:02 +01:00
Markus Fisch 28cbb55299 Use early returns when handling escape sequences
Improves readability.
2024-11-17 16:47:23 +01:00
Markus Fisch 1a4e7d100a Improve readability of expanding escape sequences 2024-11-17 16:46:23 +01:00
Markus Fisch 265e43b439 Fix argument order for assertEquals()
In EscapeTest.
2024-11-17 16:44:56 +01:00
NathanandGitHub 03a58c3412 Update French translation 2024-11-13 21:37:17 +01:00
Markus Fisch 327cad48f0 Update coroutines library 2024-11-06 21:50:30 +01:00
Markus Fisch 8e64fe2f56 Update Kotlin version 2024-11-06 21:50:14 +01:00
Markus Fisch 368d4dbb66 Fix typo in german translation 2024-10-18 19:39:38 +02:00
Manuel CovasandGitHub c8c3c094db Fix chronological order of scans taken less than 1 second apart 2024-10-17 16:36:24 +02:00
Wiktor PerskawiecandGitHub aa4aefe35f Fix typos in README and PRIVACY 2024-10-17 16:34:34 +02:00
AhmedandGitHub 536b3d77c1 Update arabian zero plural translation 2024-10-02 16:14:54 +02:00
Markus Fisch bbf2a80981 Add missing plurals for arabic
To silence lint from complaining that these are missing.
2024-10-02 15:39:33 +02:00
AhmedandGitHub ae99214d6c Update plurals in arabic translation 2024-10-02 15:36:54 +02:00
AhmedandGitHub 73b5a0969c Add arabic translation 2024-10-02 12:42:07 +02:00
Markus Fisch bf5dc96ac9 Advance version number to 1.63.12 2024-09-26 21:30:48 +02:00
Markus Fisch 301d9c2513 Fix argument list of recreation in onClickListener 2024-09-26 21:24:49 +02:00
Markus Fisch 0a11611b8f Advance version number to 1.63.11 2024-09-21 20:20:24 +02:00
Markus Fisch 249e310e43 Simplify asking for a size when sharing/exporting 2024-09-20 12:10:06 +02:00
Sergiy StuparandGitHub 6279c5731a Update Ukrainian translation 2024-09-20 12:09:46 +02:00
bovirusandGitHub df98637951 Update Italian language 2024-09-18 12:09:08 +02:00
solokotandGitHub 08b3558e46 Update Russian translation 2024-09-18 12:08:45 +02:00
Markus Fisch 648ef6fdf8 Update Kotlin coroutines library 2024-09-17 22:38:04 +02:00
Markus Fisch 807f075a51 Update Kotlin version 2024-09-17 22:37:08 +02:00
Markus Fisch 27789beeed Simplify ellipsis regex 2024-09-17 22:31:42 +02:00
Markus Fisch 97bee27827 Replace View casts for generic denotation 2024-09-17 22:29:02 +02:00
Markus Fisch 3fee49301e Only ask for barcode size when exporting/sharing
Because this parameter is just relevant when exporting/sharing
a raster image format.
2024-09-17 22:28:37 +02:00
Markus Fisch 8a6073291b Improve size slider for barcode generation
Reduce size options to popular powers of two.

And add the option to generate a barcode in the smallest
possible size of one pixel per module.
2024-09-17 20:54:07 +02:00
Markus Fisch 47151de4a4 Fix indent in Portuguese (Brazil) translation 2024-09-06 16:19:41 +02:00
Too Late (bot)andGitHub 248c4edca1 Update Portuguese (Brazil) translation 2024-09-06 16:18:21 +02:00
Markus Fisch 2c7386d536 Fix indent in Chinese translation 2024-08-17 18:12:32 +02:00
Too Late (bot)andGitHub e19cad4c87 Update Chinese translation 2024-08-17 18:09:47 +02:00
ZahnstocherandGitHub 585e0f3372 Update parcel tracking URL 2024-07-25 12:04:37 +02:00
Markus Fisch 2b7f611774 Fix showing tooltip on action FAB 2024-07-24 20:21:50 +02:00
Markus Fisch 05b2a0d9c9 Support hyphens in the second level domain
To correctly recognize URLs as WebAction.
2024-07-24 20:08:57 +02:00
Markus Fisch e7287eb088 Update tools version and gradle wrapper 2024-07-24 19:41:43 +02:00
NathanandGitHub 4b45ad3fe7 Update French translation
Added translations for checksum calculation (1.63.8) and screen brightening (1.63.10) features.
2024-07-13 19:58:04 +02:00
Markus Fisch 449d8fdbbd Advance version number to 1.63.10 2024-06-30 19:41:13 +02:00
Markus Fisch f44cff9912 Use type in getParcelableExtra() on Tiramisu+
And ignore the deprecation warning for earlier Android versions.

Also use a scope function to remove the local variable in the
parent context.
2024-06-30 19:06:02 +02:00
ThetaDevandGitHub 24730dfde3 Allow file input from share menu
Updates the handleSendText method to support both types of intents.

Also extends the accepted file types to text/* to also accept VCards
and other text-based files.
2024-06-30 18:54:20 +02:00
Sergiy StuparandGitHub be6539d41f Update Ukrainian translation 2024-06-16 13:41:14 +02:00
bovirusandGitHub 60dd40554e Update Italian language 2024-06-12 09:05:24 +02:00
solokotandGitHub 837119b6ec Update Russian translation 2024-06-12 09:04:57 +02:00
Markus Fisch abe11b82e5 Fix indent in Polish translation 2024-07-11 12:49:33 +02:00
Markus Fisch 4fc1e36d04 Add a menu item to toggle screen brightness
So this setting can be enabled/disabled where it's required, too.
2024-06-11 14:22:22 +02:00
Markus Fisch 54aa15e1be Set maximum brightness when showing a barcode
To make scanning from screen easier.
2024-06-11 14:22:18 +02:00
Too Late (bot)andGitHub 876d8fc7b3 Update Polish translation 2024-06-10 20:31:07 +02:00
Markus Fisch 7bdcdaaa5d Advance version number to 1.63.9 2024-06-02 18:15:53 +02:00
Markus Fisch fd44728e89 Ignore "URL:" before URLs in barcode content
Apparently some people think it's necessary to add an "URL:" prefix
before an actual URL, so let's cut that rubbish.
2024-06-02 15:24:22 +02:00
Markus Fisch 8242520a0b Export/share generated barcode as JPG
On request.

Generally, using a lossless format like PNG makes more sense.
2024-06-02 14:00:27 +02:00
Markus Fisch 2e7e9a942b Fix indent in new translations 2024-06-02 13:51:43 +02:00
Too Late (bot)andGitHub dcc9d5b3ff Update Brazilian translation 2024-06-02 11:54:14 +02:00
Markus Fisch 607aab854c Update build tools 2024-05-27 19:39:46 +02:00
Markus Fisch cc2b2f076b Fix exporting/sharing multiple history items
By their ID.

`?` is only one argument so a list of numbers becomes just one ID,
which cannot work, of course.
2024-05-27 19:37:43 +02:00
Too Late (bot)andGitHub 90e9723de5 Update Chinese translation 2024-05-13 10:38:54 +02:00
Sergiy StuparandGitHub ab6787e60b Update Ukrainian translation 2024-05-12 10:40:14 +02:00
solokotandGitHub 698686a45a Update Russian translation 2024-05-11 15:35:51 +02:00
Markus Fisch 95bdaa1252 Reformat imports with Android Studio
Seems like wildcard imports are back in fashion.
2024-05-11 14:13:39 +02:00
Markus Fisch f0c02e5156 Reformat with Android Studio 2024-05-11 14:13:20 +02:00
Markus Fisch a8c50308d0 Add Ko-fi for donations 2024-05-11 13:20:31 +02:00
bovirusandGitHub 6b359d4c65 Update Italian language 2024-05-10 21:08:07 +02:00
Markus Fisch 863caed387 Advance version number to 1.63.8 2024-05-10 20:56:25 +02:00
Markus Fisch e4f17b9bdf Add an option to show a checksum
For the decoded content. On request of #452.
2024-05-10 20:37:32 +02:00
Markus Fisch c2071d8d81 Treat LF/CR/Tab as printable characters
When encoding "something\nlike\tthüs". This string should be encoded
as text rather than binary, of course.
2024-05-08 19:49:19 +02:00
Markus Fisch 4758c2d337 Update tools and gradle wrapper 2024-05-07 21:57:36 +02:00
Markus Fisch 91336fe15c Encode as ByteArray if text contains non-printable
To support escape sequences containing non-printable characters.
2024-05-07 21:54:57 +02:00
Markus Fisch 2c437f3771 Add Liberapay to README 2024-04-27 15:58:37 +02:00
bovirusandGitHub cb082b20fe Update Italian language 2024-04-21 21:27:57 +02:00
Markus Fisch bce3630686 Advance version number to 1.63.7 2024-04-17 21:48:31 +02:00
Markus Fisch 4feb4d066c Update ZXingCpp for Aztec Runes 2024-04-17 21:24:30 +02:00
Markus Fisch e7d84cd916 Update tools version 2024-04-17 21:22:53 +02:00
Giovanni DonisiandGitHub 84e2917694 Update Italian translation 2024-04-14 13:58:07 +02:00
Markus Fisch b87464ec40 Advance version number to 1.63.6 2024-04-13 14:38:27 +02:00
Markus Fisch 269e0f41a6 Correct spelling mistakes in Spanish translation
Following the recommendations from the linter.
2024-04-13 14:32:27 +02:00
NathanandGitHub aefbc4a296 Update French translation 2024-04-13 14:26:19 +02:00
Sergiy StuparandGitHub c64984dc6a Update Ukrainian translation 2024-03-31 12:49:47 +02:00
Markus Fisch 24c7ad0ba4 Return Codabar start/stop codes
As requested in #445.

The start/stop codes determine if a barcode is complete or if there
is a continuation code to scan.
2024-03-30 12:46:15 +01:00
Markus Fisch 643a935852 Move buildConfig from properties into build.gradle
Apparently setting 'android.defaults.buildfeatures.buildconfig=true'
is deprecated and will be removed in version 9.0 of the Android Gradle
plugin.
2024-03-30 12:46:15 +01:00
solokotandGitHub 4fcb836b53 Update Russian translation 2024-03-26 12:12:30 +01:00
Markus Fisch 48ed5f1a2c Add history shortcut 2024-03-25 21:36:53 +01:00
Markus Fisch 3aa2371eeb Advance version number to 1.63.5 2024-03-24 21:15:31 +01:00
solokotandGitHub 88f92943c0 Update Russian translation 2024-03-24 14:53:36 +01:00
Sergiy StuparandGitHub 39710d4ab9 Update Ukrainian translation 2024-03-24 14:53:03 +01:00
Markus Fisch 93f7cd3cb8 Map old ignore setting to new one
For existing installations that changed this value.
2024-03-23 20:21:08 +01:00
Markus Fisch 69baa3b824 Add an option to ignore any duplicates
As requested in issue #439.
2024-03-23 14:20:42 +01:00
Markus Fisch bb05e38278 Update tools version 2024-03-21 21:23:28 +01:00
KamaleiZestriandGitHub 554f5a5e73 Fix failing send due to invalid bluetooth connection
Try reconnect to socket once before failing.
2024-03-21 21:19:51 +01:00
Markus Fisch 2dcf7772f2 Update tools version and gradle wrapper 2024-03-17 13:52:49 +01:00
Markus Fisch 3c7012e3c4 Advance version number to 1.63.4 2024-03-17 13:35:31 +01:00
Markus Fisch 08e11c3c40 Update ZXingCpp to fix support for old devices 2024-03-16 21:38:51 +01:00
Unknownman820andGitHub 31854dab0c Update Chinese translation 2024-03-16 21:38:04 +01:00
Markus Fisch 4f12c3e270 Advance version number to 1.63.3 2024-03-04 21:06:50 +01:00
Markus Fisch 3ec43dabce Update AndroidX SCAN Intent sample 2024-03-03 18:34:42 +01:00
Markus Fisch 0d4f536f71 Fix returning barcode format in return Intent
The class `BarcodeFormat` is not known to callers, of course.
2024-03-03 18:30:55 +01:00
Markus Fisch b064a293fb Add donation options to the README 2024-02-29 20:25:50 +01:00
Markus Fisch d124c29919 Advance version number to 1.63.2 2024-02-19 22:08:22 +01:00
Markus Fisch 2e89f1530a Add DX_FILM_EDGE barcode format
And add it to the selected barcodes so it's immediately
available for existing installations too.
2024-02-17 13:41:49 +01:00
Markus Fisch ef7e2a4b0d Update build tools 2024-02-17 13:40:28 +01:00
Markus Fisch f680533819 Update ZXingCpp for DX_FILM_EDGE format 2024-02-17 13:21:06 +01:00
Oğuz ErsenandGitHub a8c977c40e Update Turkish translation 2024-01-21 17:31:20 +01:00
Markus Fisch 1e89b22953 Update ZXingCpp to latest version
For QRCode: fix crash reported in #700
2024-01-15 20:14:50 +01:00
Markus Fisch 11f028e0f3 Update Kotlin version 2024-01-15 20:13:37 +01:00
Markus Fisch 1f7f4a23b5 Update tools version 2024-01-15 20:11:50 +01:00
Sergiy StuparandGitHub 41581d07ec Update Ukrainian translation 2024-01-14 20:53:17 +01:00
Markus Fisch dc819a6e1a Advance version number to 1.63.1 2024-01-13 13:47:32 +01:00
Markus Fisch 5b51072f1a Include custom label in json/csv exports/shares 2024-01-13 13:29:45 +01:00
solokotandGitHub 1b10e82cb2 Update Russian translation 2024-01-12 08:55:55 +01:00
Markus Fisch eeae1eb0ad Advance version number to 1.63.0 2024-01-11 20:27:27 +01:00
Markus Fisch 21c29ca373 Set sendScanActive to false by default
Was enabled by default because this setting came after `sendScanUrl`
and I didn't want to annoy users using this feature already.

But it's probably too suspicious to have it enabled by default, even
if it does nothing, of course.
2024-01-11 17:10:22 +01:00
Markus Fisch 8bcb72e91b Use getParcelableExtra(String, Class) on Tiramisu+ 2024-01-11 00:23:15 +01:00
Markus Fisch 8f345fbfbe Remove redundant suppress annotation 2024-01-11 00:14:04 +01:00
Markus Fisch 2c88dbca02 Reformat scanWithinBounds()
Apply autoformat.
2024-01-11 00:12:45 +01:00
Markus Fisch bd1c629c85 Reformat imports
Let Android Studio do its thing.
2024-01-11 00:11:48 +01:00
Markus Fisch f89e6612c4 Replace Enum.values() with Enum.entries
Following the recommendation.
2024-01-11 00:09:27 +01:00
Markus Fisch 7a0cd0ce19 Use isNullOrEmpty instead of checking both 2024-01-11 00:08:22 +01:00
Markus Fisch 8544affa1e Edit scan label in DecodeFragment
Which makes this feature more prominent and also easier to access.
2024-01-10 23:56:44 +01:00
Markus Fisch 48d3588341 Add a button to add generated codes to the history
So generated barcodes can be saved, too.
2024-01-10 23:37:51 +01:00
Markus Fisch 783f6e4892 Refactor edit_scan to edit_label
This action is used to edit the (optional) label of a scan.
2024-01-10 23:03:24 +01:00
Markus Fisch 032cd64daf Avoid asking multiple times for camera permission
Otherwise ActivityCompat.requestPermissions() will trigger onResume()
again and again.
2024-01-11 10:59:29 +01:00
Markus Fisch 96626e6b00 Move origin subtraction out of min()
Simplifies the calculation of the width and height of the crop
section.
2024-01-10 22:38:31 +01:00
Markus Fisch 11ae65ec3e Refactor Bitmap.erect to rotate
The original task of this function was to straighten a picture
lying on its side. However, the function is now used for any rotation.
2024-01-10 22:36:02 +01:00
Markus Fisch 91e3acb647 Disable free rotation by default
This is probably too delicate to use, most barcodes are still
recognized when skewed (a little), and it can be quite an expensive
operation to rotate a very big image. So it's better to disable
this feature by default (for new installations).
2024-01-09 20:25:21 +01:00
Markus Fisch 40c2f5947b Crop imported images in background thread
Because rotating and cropping a big photo can take some time.
2024-01-09 20:03:26 +01:00
Markus Fisch 58b9466cfc Fix margin/error correction for recreated codes
The `margin` argument position was incorrectly used for the
error correction level.
2024-01-09 19:28:50 +01:00
Markus Fisch 12210e1768 Update CameraView library 2024-01-09 12:13:12 +01:00
Sergiy StuparandGitHub ad2ac457c9 Update Ukrainian translation 2024-01-09 12:06:05 +01:00
Markus Fisch 634b6e8d2b Limit duration of error beep to 1s
Since `ToneGenerator.TONE_SUP_ERROR` keeps on playing which
is quite annoying.
2024-01-03 19:21:29 +01:00
CertainBotandGitHub 6dd47f4dd0 Update Spanish translation 2023-12-19 22:00:31 +01:00
Markus Fisch 8426f66cfb Fix indent in settings.gradle
Match this with the rest of the project.
2023-12-18 12:27:34 +01:00
Meen BeeseandGitHub 3dfa84a33e Simplification and clean up of Gradle files
* Some linting suggestions by Android Studio
* Move all repository declarations to settings
2023-12-18 09:41:07 +01:00
Markus Fisch 38f658e253 Advance version number to 1.62.3 2023-12-15 12:18:14 +01:00
Markus Fisch 287d99e2b4 Make clicks select list items when in action mode
Avoids having to do long presses for every item you want to add
to the selection.
2023-12-14 22:57:31 +01:00
Markus Fisch 5cfcde9d4a Update pt-br translation
Thanks to m_s_
2023-12-12 23:05:47 +01:00
Markus Fisch b2730259fc Advance version number to 1.62.2 2023-12-12 21:56:37 +01:00
Markus Fisch 61a2d3023c Add rMQR Code on existing installations
So it's working immediately after an update.
2023-12-12 21:50:28 +01:00
CertainBotandGitHub 239ac7bd1a Update Spanish translation
- New strings translated. Typos ammended.
- Términos nuevos traducidos. Corrección de erratas.
2023-12-11 21:58:35 +01:00
Markus Fisch 4048314cf7 Advance version number to 1.62.1 2023-12-11 20:19:18 +01:00
Markus Fisch 3dbd772847 Add rMQR Code to barcode format resources
To make it selectable, too.
2023-12-11 20:12:23 +01:00
Markus Fisch 3a08465c35 Add rMQR Code list of supported codes in README 2023-12-10 18:24:08 +01:00
Markus Fisch 92e3669ee5 Advance version number to 1.62.0 2023-12-10 15:49:33 +01:00
Markus Fisch 8b297f4768 Ignore plurals warning for qr_version_and_modules
Because a QR Code always has more than one module.
2023-12-10 15:45:13 +01:00
Markus Fisch 42b7f1179b Remove incomplete zh-rTW locales translation
Because this got incomplete and so won't work with the values
array anymore.

Also it's probably a really bad idea to translate locales anyway.
If you pick a wrong one by accident, you'll have a hard time
changing it back to something you can understand.
2023-12-10 15:40:37 +01:00
Markus Fisch 9d6854c282 Suppress false-positive warning about TileService
`TileService#startActivityAndCollapse(Intent)` is only ever run
below UpsideDownCake, but Lint doesn't understand this yet.
2023-12-10 15:37:48 +01:00
Markus Fisch d943b7deff Suppress warning about commit()ing prefs
Because `commit()` is intentional where it's used.
2023-12-10 15:36:10 +01:00
Markus Fisch a138da9d9b Update Kotlin version 2023-12-10 15:35:28 +01:00
Markus Fisch 194338a0d2 Remove "many" quantity from French plural
Lint says this isn't relevant to French.
2023-12-10 15:34:27 +01:00
Markus Fisch 7eb8fdcce1 Add Brave search to and de/fa/tr locales
This was missing from the original commit.
2023-12-10 15:33:21 +01:00
Markus Fisch 2bf1796340 Improve naming of shared wrapper files
And add extensions so it's easier to deal with these files
in other contexts.
2023-12-10 15:14:31 +01:00
Markus Fisch cc42015da2 Wipe share file in HistoryFragment too 2023-12-09 22:20:33 +01:00
Markus Fisch 648c39c439 Rename share file to just "contents"
A better fit when sharing the history in this file.
2023-12-09 22:20:00 +01:00
Markus Fisch 8c25b05cda Remove menu group to make actions fold 2023-12-09 22:16:16 +01:00
Markus Fisch 780bebc9d1 Share history as a file
To make sharing the history via Bluetooth more easily, as Android
converts plain text to HTML there.
2023-12-09 22:01:51 +01:00
Markus Fisch bf7eb7efd5 Update tools version and gradle wrapper
And finally add the `compileOptions` block to make Gradle 8
work, because the Google guys are still unable to fix their bugs.
2023-12-09 16:29:48 +01:00
Markus Fisch d00d08bae3 Update to latest ZXingCpp 2023-12-09 15:13:45 +01:00
Markus Fisch 379256a7fe Share selected scans only from history
If there is a selection. Otherwise use the search filter as before.
2023-11-26 19:54:08 +01:00
Markus Fisch 180339269f Advance version number to 1.61.2 2023-11-22 19:16:16 +01:00
v72807647andGitHub 49af17cba8 Fix quick setting tile failed to launch on Android 14
SDK 34 deprecates startActivityAndCollapse(Intent), making it
throws an UnsupportedOperationException on Android 14 devices
instead of launching the app.

Added new function call for API 34, tested with Android 14 and
Android 9 devices.
2023-11-22 09:24:30 +01:00
Markus Fisch a03a187d2d Escape apostrophes in french translation
Apostrophe characters need to be escaped in XML.
2023-11-20 10:55:29 +01:00
solokotandGitHub 84806e70d7 Update Russian translation 2023-11-20 10:09:02 +01:00
NathanandGitHub 058de5a141 Update french locale 2023-11-18 12:28:07 +01:00
Kryštof ČernýandGitHub d330fbd195 Update CZ translation 2023-11-16 13:45:59 +01:00
Markus Fisch f5bc6f664f Fix Xiaomi MIUI lockscreen barcode shortcut 2023-11-15 22:07:50 +01:00
Markus Fisch 464279807c Advance version number to 1.61.1 2023-11-14 23:10:09 +01:00
Markus Fisch f7e5ef473a Add menu item to share scanned contents as a file
Because when sharing plain text via Bluetooth, Android explicitly
converts anything to HTML, which is not always what we want.

Details here:
https://stackoverflow.com/q/29907030

This feature is also useful to share binary contents more easily.
2023-11-13 20:26:31 +01:00
Markus Fisch ff733ef59e Fix opening recreated barcodes with binary content
Use the raw bytes not only for the recreated image but also
when opening the barcode in BarcodeFragment.
2023-11-10 21:16:05 +01:00
Markus Fisch 35b110a472 Fix key of general format restriction preference
Makes this setting have an effect again.

I somehow forgot to update the keys in the XMLs as well.
2023-11-08 20:41:32 +01:00
Markus Fisch 58eabf4754 Update to SDK 34 2023-11-01 18:12:57 +01:00
Markus Fisch 1f273a48a5 Catch IAE when parsing a WIFI configuration
Because some arguments may be out of range.

For example, SSIDs cannot be longer than 32 characters in some
versions of Android.

See: https://github.com/markusfisch/BinaryEye/issues/402
2023-10-29 13:53:58 +01:00
solokotandGitHub 785a64725d Update Russian translation 2023-10-24 08:56:32 +02:00
Markus Fisch 7a42e1de73 Recreate binary barcodes too
Since this is easily possible now.
2023-10-23 21:11:01 +02:00
7heMechandGitHub 9856ab7b6b Add Bulgarian translation and Brave search engine 2023-10-20 14:14:36 +02:00
Markus Fisch 746ff143d8 Advance version number to 1.61.0 2023-10-13 23:49:38 +02:00
Markus Fisch b0d4d94973 Add test for a VCARD with an explicit TYPE
And make the other tests fail if null was returned.
2023-10-13 21:11:22 +02:00
Markus Fisch c604ad5ef2 Fix type regex for VCARDs
The previous regex captured just the first letter of the type argument
because it was modified to be not greedy.
2023-10-13 21:08:01 +02:00
Markus Fisch 91c2b324ac Refactor pick_file to pick_image_file
And use `pick_file` to pick any file, not just an image.
2023-10-12 22:23:27 +02:00
Markus Fisch df59fd7446 Update Kotlin and coroutines library version 2023-10-12 22:15:19 +02:00
Markus Fisch 449fd1f26c Limit read binary data for encoding
And use just the first 4096 bytes, which is way too optimistic
as there is no barcode format available that can save that much
data.

This is just to avoid an OutOfMemoryError. Maybe it's better to
just show an error message for files that are too big?
2023-10-13 13:16:24 +02:00
Markus Fisch 72d839c6e2 Support encoding binary data from a file
So binary data can finally be re-encoded too.
2023-10-13 12:48:13 +02:00
Markus Fisch ab8c993a81 Add padding for spinners on encode fragment
Enlarges the touch area and makes the spinners easier to use.
2023-10-13 11:47:36 +02:00
Markus Fisch 5f5bd913d6 Move content EditText to the top
As this is probably the most important input field.
2023-10-13 11:47:36 +02:00
Markus Fisch 92fe4dd55f Reformat code with Android Studio
And apply the latest style changes.
2023-10-12 20:32:01 +02:00
Markus Fisch dab95176f0 Remember last selected margin for generation
More convenient.

Maybe we should store these settings for each barcode format.
2023-10-12 20:29:23 +02:00
Markus Fisch 650503ba70 Update camera library to latest version 2023-10-08 20:17:58 +02:00
solokotandGitHub 61078f214e Update Russian translation 2023-10-02 13:33:45 +02:00
Markus Fisch a4b639edfe Add margin parameter for encoding barcodes 2023-09-20 13:46:34 +02:00
Markus Fisch f37878dbb8 Replace toUpperCase() with uppercase()
Because toUpperCase(Locale) is deprecated.
2023-09-19 22:19:35 +02:00
Markus Fisch 10fde7c3d6 Add intent for MIUI fingerprint shortcut
So it's possible to start BinaryEye from the MIUI of Xiaomi devices.
2023-09-19 14:46:34 +02:00
Markus Fisch a098fcb48c Always save and use rawBytes from ZXingCpp
If available.

Because the text member may already be UTF-8 encoded, and this
cannot simply be converted back into a byte array for the hex dump.

So for ContentType.GS1, ContentType.ISO15434 and ContentType.TEXT
there could be inconsistencies between the displayed content and the
hex dump, because the latter was generated from the (already encoded)
content.
2023-09-15 19:08:29 +02:00
m_s_andMarkus Fisch f162977640 Update Brazilian Portuguese translation 2023-09-03 20:18:50 +02:00
Oleg KoretskyandGitHub 66cab58d7b Fix missing Copy button for binary QRs 2023-08-31 13:54:29 +02:00
John VenessandGitHub e99e2d7dcc Update English strings 2023-08-20 11:23:53 +02:00
Sergiy StuparandGitHub 9045a27a5d Update Ukrainian translation 2023-08-14 12:00:14 +02:00
Markus Fisch 9e1208c72b Update ZXing C++ wrapper 2023-08-13 19:09:32 +02:00
Markus Fisch 90410f232a Update Kotlin version 2023-08-13 18:09:29 +02:00
ddarfantasyandGitHub 6769fa7e37 Update zh-CN translation 2023-08-13 17:50:26 +02:00
Markus Fisch 72444f5081 Update ZXing C++ wrapper 2023-07-06 12:40:12 +02:00
Markus Fisch 9a3ca4ffa3 Update ZXing C++ wrapper 2023-05-20 14:24:34 +02:00
Markus Fisch f572923720 Add changelog for latest version for f-droid 2023-05-19 18:57:15 +02:00
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
243 changed files with 6668 additions and 1876 deletions
+203
View File
@@ -1,5 +1,208 @@
# Change Log
## 1.65.1
* Improve simple mode
* Remember quiet zone settings
* Reduce height for 1D barcodes
* Add execute argument for binaryeye://encode
* Update Russian translation
## 1.65.0
* Add a welcome screen to choose a mode
* Add a deep link to encode a barcode
* Remember zoom level of generated barcodes
* Remove Human Readable Interpretation (HRI) decoration
* Update Italian translation
## 1.64.2
* Separate crop window settings for image scanning
* Fix initializing dataMask
* Update Russian translation
## 1.64.1
* Handle empty input for recreation
* Fix setting up database
## 1.64.0
* Find barcode by content
* Immediately replace toasts
* Use detected barcode for recreation
* Show data mask of QR Codes
* Fix deep linking
* Replace margin with quiet zone switch when generating barcodes
* Update Russian translation
* Update Portuguese (Brazil) translation
## 1.63.13
* Fix UTF-8 conversion of binary escape sequences when creating barcodes
* Fix scan order for intervals under one second
* Add arabic translation
* Update French translation
## 1.63.12
* Fix magnifying barcode recreation
## 1.63.11
* Update parcel tracking URL
* Update multiple translations
* Only ask for barcode size when exporting/sharing
## 1.63.10
* Use maximum brightness when showing a generated barcode
* Allow file input from share menu
* Update multiple translations
## 1.63.9
* Export/share as JPEG
* Ignore "URL:" before URLs in barcode content
* Fix exporting/sharing multiple history items
* Update multiple translations
## 1.63.8
* Add an option to show a checksum for the decoded content
* Add support for encoding escape sequences for non-printable characters
* Update Italian translation
## 1.63.7
* Update ZXingCpp for Aztec Runes
* Update Italian translation
## 1.63.6
* Add history shortcut
* Return Codabar start/stop codes
* Update French, Russian, Spanish and Ukrainian translations
## 1.63.5
* Add an option to ignore any duplicates
* Fix invalid bluetooth connection
* Update Russian translation
* Update Ukrainian translation
## 1.63.4
* Update Chinese translation
* Improve support for old devices
## 1.63.3
* Fix SCAN Intent
## 1.63.2
* Add DX Film Edge barcode format
* Update Turkish translation
* Update Ukrainian translation
## 1.63.1
* Include scan label in exports/shares
* Update Russian translation
## 1.63.0
* Edit scan label in decoding screen
* Add a button to add generated codes to the history
* Improve scanning from images
* Improve asking for camera permission
* Fix margin for recreated barcodes
* Limit duration of error beep to 1s
* Update Ukrainian translation
* Update Spanish translation
## 1.62.3
* Extend selection with single taps in history listing
* Update Brazilian Portuguese translation
## 1.62.2
* Include rMQR Code for updates
## 1.62.1
* Make rMQR Code selectable
## 1.62.0
* Add support for rMQR (rectangular Micro QR) Code
* Share history as a file
* Only share selected scans from history
## 1.61.2
* Fix quick settings tile for Android 14
* Fix Xiaomi MIUI lockscreen barcode shortcut
* Update French translation
* Update Czech translation
* Update Russian translation
## 1.61.1
* Share scanned contents as a file
* Recreate binary barcodes too
* Add Bulgarian translation and Brave search engine
* Update Russian translation
* Fix general format restriction from preferences
## 1.61.0
* Support encoding binary data
* Add margin parameter for encoding
* Add copying binary data as hex dump
* Fix parsing TYPE argument in VCARDs
* Update Russian translation
* Update Brazilian Portuguese translation
* Update Ukrainian translation
* Update Chinese translation
## 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
+6 -1
View File
@@ -16,5 +16,10 @@ to squash when merging into master:
Then write a [good commit message][commit_messages] to keep the history
meaningful and useful. One feature, one commit.
## Translations
For translations use [Toolate][toolate].
[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
[toolate]: https://toolate.othing.xyz/projects/binaryeye/
+15
View File
@@ -28,11 +28,26 @@ testscan:
-c android.intent.category.BROWSABLE \
-d 'binaryeye://scan/?ret=http%3A%2F%2Fmarkusfisch.de%2F%3Fresult%3D{RESULT}'
testencode:
adb shell am start -a android.intent.action.VIEW \
-c android.intent.category.BROWSABLE \
-d 'binaryeye://encode?content=Test\&format=QRCode'
testencodeexec:
adb shell am start -a android.intent.action.VIEW \
-c android.intent.category.BROWSABLE \
-d 'binaryeye://encode?content=Test\&format=QRCode\&execute=true'
testurl:
adb shell am start -a android.intent.action.VIEW \
-c android.intent.category.BROWSABLE \
-d 'http://markusfisch.de/BinaryEye?ret=http%3A%2F%2Fmarkusfisch.de%2F%3Fresult%3D{RESULT}'
testxiaomi:
adb shell am start -a android.intent.action.VIEW \
-c android.intent.category.BROWSABLE \
-d 'market://details?id=com.xiaomi.scanner'
install:
adb $(TARGET) install -r app/build/outputs/apk/debug/app-debug.apk
+19 -4
View File
@@ -12,10 +12,17 @@ hesitate to contact me.
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.
On Android Q and better, this permission is not requested nor required.
### [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]
@@ -46,15 +53,23 @@ which requires `CHANGE_WIFI_STATE`.
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`.
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
[configuredNetworks]: https://developer.android.com/reference/android/net/wifi/WifiManager#getConfiguredNetworks()
[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()
+43 -8
View File
@@ -2,7 +2,7 @@
Yet another barcode scanner for Android. As if there weren't [enough][play].
This one is free, without any ads and open source.
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.
@@ -10,6 +10,23 @@ comes in Material Design and can also generate barcodes.
Binary Eye uses the [ZXing-C++][zxing_cpp] ("Zebra Crossing") barcode
scanning library.
If you find this app useful and wish to support its continued development,
you can [buy me a coffee](https://www.buymeacoffee.com/markusfisch) or
send some Bitcoin decimals to `bc1q2guk2rpll587aymrfadkdtpq32448x5khk5j8z`.
<a href="https://www.buymeacoffee.com/markusfisch" target="_blank"><img
src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png"
alt="Buy Me A Coffee"
style="height: 60px !important;width: 217px !important;"/></a>&nbsp;<a
href="https://liberapay.com/markusfisch/" target="_blank"><img
src="https://liberapay.com/assets/widgets/donate.svg"
alt="Liberapay"
style="height: 60px !important"/></a>&nbsp;<a
href="https://ko-fi.com/markusfisch" target="_blank"><img
src="https://storage.ko-fi.com/cdn/brandasset/kofi_s_tag_dark.png"
alt="Ko-fi"
style="height: 60px !important"/></a>
## Screenshots
<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/screencap-scanning.png"
@@ -36,6 +53,7 @@ ZXing can read the following barcode formats:
* [CODE 93][code_93]
* [CODE 128][code_128]
* [DATA MATRIX][data_matrix]
* [DX FILM EDGE][dx_film_edge]
* [EAN 8][ean_8]
* [EAN 13][ean_13]
* [ITF][itf]
@@ -43,6 +61,7 @@ ZXing can read the following barcode formats:
* [PDF417][pdf417]
* [QR CODE][qr_code]
* [Micro QR Code][micro_qr_code]
* [rMQR Code][rmqr_code]
* [RSS 14][rss]
* [RSS EXPANDED][rss]
* [UPC A][upc_a]
@@ -69,9 +88,11 @@ ZXing can generate the following barcode formats:
### Deep Links
You can invoke Binary Eye with a web URI intent from anything that can
open URIs. There are two options:
open URIs.
1. [binaryeye://scan](binaryeye://scan)
#### Decoding
1. [binaryeye://scan](binaryeye://scan) (note: GitHub does not render this as clickable link)
2. [http(s)://markusfisch.de/BinaryEye](http://markusfisch.de/BinaryEye)
If you want to get the scanned contents, you can add a `ret` query
@@ -84,7 +105,17 @@ Supported symbols are:
* `RESULT` - scanned content
* `RESULT_BYTES` - raw result as a hex string
* `FORMAT` - barcode format
* `META` - the meta data, if available
#### Encoding
1. [binaryeye://encode](binaryeye://encode)
You can use the URL arguments `content` and `format` to automatically
preset this data. For example:
[binaryeye://encode?content=Test&format=QRCODE](binaryeye://encode?content=Test&format=QRCODE)
If you want the code to be generated immediately, add `execute=true`.
### SCAN Intent
@@ -121,10 +152,12 @@ 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")
class YourActivity : AppCompatActivity() {
private val resultLauncher = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) { result ->
if (result.resultCode == RESULT_OK) {
val content = result.data?.getStringExtra("SCAN_RESULT")
}
}
@@ -144,6 +177,7 @@ class YourActivity : Activity() {
[code_93]: https://en.wikipedia.org/wiki/Code_93
[code_128]: https://en.wikipedia.org/wiki/Code_128
[data_matrix]: https://en.wikipedia.org/wiki/Data_Matrix
[dx_film_edge]: https://en.wikipedia.org/wiki/DX_encoding
[ean_8]: https://en.wikipedia.org/wiki/EAN-8
[ean_13]: https://en.wikipedia.org/wiki/International_Article_Number
[itf]: https://en.wikipedia.org/wiki/Interleaved_2_of_5
@@ -151,6 +185,7 @@ class YourActivity : Activity() {
[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
[rmqr_code]: https://www.qrcode.com/en/codes/rmqr.html
[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
+31 -12
View File
@@ -2,15 +2,15 @@ apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
android {
namespace 'de.markusfisch.android.binaryeye'
compileSdkVersion sdk_version
namespace "de.markusfisch.android.binaryeye"
compileSdk sdk_version
defaultConfig {
minSdkVersion 9
targetSdkVersion sdk_version
minSdk 9
targetSdk sdk_version
versionCode 109
versionName '1.56.1'
versionCode 146
versionName '1.65.1'
}
signingConfigs {
@@ -42,6 +42,10 @@ android {
}
}
buildFeatures {
buildConfig true
}
bundle {
language {
// To make the app bundle contain all language resources
@@ -53,18 +57,33 @@ android {
enableSplit = false
}
}
// Required since Gradle 8.
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "1.8"
}
}
dependencies {
testImplementation 'junit:junit:4.13.2'
// Kotlin
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.1"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.9.0"
// Support
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"
implementation 'com.github.markusfisch:CameraView:1.9.1'
implementation 'com.github.markusfisch:ScalingImageView:1.4.0'
implementation 'com.github.markusfisch:zxing-cpp:v1.4.0.8'
// External
implementation 'com.github.markusfisch:CameraView:1.10.0'
implementation 'com.github.markusfisch:ScalingImageView:1.4.1'
implementation 'com.github.markusfisch:zxing-cpp:v2.3.0.3'
// Testing
testImplementation 'junit:junit:4.13.2'
}
-2
View File
@@ -3,5 +3,3 @@
-dontwarn android.support.v8.**
-keep class android.support.v7.widget.SearchView { *; }
-keep class de.markusfisch.android.zxingcpp.** { *; }
Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 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.4 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.2 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

+29 -6
View File
@@ -6,16 +6,21 @@
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-feature android:name="android.hardware.camera.autofocus"
android:required="false" />
<uses-sdk tools:overrideLibrary="android.support.v14.preference"/>
<queries>
<intent>
@@ -58,13 +63,20 @@
<intent-filter android:label="@string/compose_barcode">
<action android:name="android.intent.action.SEND"/>
<category android:name="android.intent.category.DEFAULT"/>
<data android:mimeType="text/plain"/>
<data android:mimeType="text/*"/>
</intent-filter>
<intent-filter>
<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:host="details" android:scheme="market"
android:pathSuffix="id=com.xiaomi.scanner"/>
</intent-filter>
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>
@@ -97,14 +109,25 @@
</intent-filter>
</activity>
<activity
android:name=".activity.MainActivity"
android:name=".activity.WelcomeActivity"
android:exported="true"/>
<activity
android:name=".activity.MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>
<data android:scheme="binaryeye" android:host="encode"/>
</intent-filter>
</activity>
<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">
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE"
tools:targetApi="n">
<intent-filter>
<action android:name="android.service.quicksettings.action.QS_TILE"/>
</intent-filter>
@@ -23,7 +23,7 @@ object OpenOrSearchAction : IAction {
url: String,
search: Boolean
) {
if (!context.openUrl(url) && search) {
if (!context.openUrl(url, silent = true) && search) {
openSearch(context, url)
}
}
@@ -46,8 +46,8 @@ object OpenOrSearchAction : IAction {
).toMutableList()
// Remove the "Always ask" entry. The arrays search_engines_*
// are used in the preferences too.
names.removeFirst()
urls.removeFirst()
names.removeAt(0)
urls.removeAt(0)
if (prefs.openWithUrl.isNotEmpty()) {
names.add(prefs.openWithUrl)
urls.add(prefs.openWithUrl)
@@ -1,6 +1,6 @@
package de.markusfisch.android.binaryeye.actions.vtype
import java.util.*
import java.util.Locale
object VTypeParser {
private const val BACKSLASH_R_LEGACY =
@@ -39,6 +39,6 @@ data class VTypeProperty(
} ?: this.info.firstOrNull()
companion object {
private val typeRegex = """^(?:TYPE=)(.+?)[:;]?.*$""".toRegex(RegexOption.IGNORE_CASE)
private val typeRegex = """^TYPE=([A-Z_]+)[:;]?.*$""".toRegex(RegexOption.IGNORE_CASE)
}
}
@@ -8,7 +8,7 @@ import android.provider.ContactsContract
import de.markusfisch.android.binaryeye.R
import de.markusfisch.android.binaryeye.actions.IntentAction
import de.markusfisch.android.binaryeye.actions.vtype.VTypeParser
import java.util.*
import java.util.Locale
object VCardAction : IntentAction() {
override val iconResId: Int
@@ -45,10 +45,13 @@ object VCardAction : IntentAction() {
val (extraPhoneType, extraPhone) = when (index) {
0 -> ContactsContract.Intents.Insert.PHONE_TYPE to
ContactsContract.Intents.Insert.PHONE
1 -> ContactsContract.Intents.Insert.SECONDARY_PHONE_TYPE to
ContactsContract.Intents.Insert.SECONDARY_PHONE
2 -> ContactsContract.Intents.Insert.TERTIARY_PHONE_TYPE to
ContactsContract.Intents.Insert.TERTIARY_PHONE_TYPE
else -> return@forEachIndexed
}
phoneProperty.firstTypeOrFirstInfo?.also {
@@ -65,10 +68,13 @@ object VCardAction : IntentAction() {
val (extraMailType, extraMail) = when (index) {
0 -> ContactsContract.Intents.Insert.EMAIL_TYPE to
ContactsContract.Intents.Insert.EMAIL
1 -> ContactsContract.Intents.Insert.SECONDARY_EMAIL_TYPE to
ContactsContract.Intents.Insert.SECONDARY_EMAIL
2 -> ContactsContract.Intents.Insert.TERTIARY_EMAIL_TYPE to
ContactsContract.Intents.Insert.TERTIARY_EMAIL
else -> return@forEachIndexed
}
mailProperty.firstTypeOrFirstInfo?.also {
@@ -184,7 +190,7 @@ object VCardAction : IntentAction() {
}
private val String.addressType: Int?
get() = when (this.toUpperCase(Locale.US)) {
get() = when (this.uppercase()) {
"HOME" -> ContactsContract.CommonDataKinds.StructuredPostal.TYPE_HOME
"WORK" -> ContactsContract.CommonDataKinds.StructuredPostal.TYPE_WORK
"OTHER" -> ContactsContract.CommonDataKinds.StructuredPostal.TYPE_OTHER
@@ -9,7 +9,8 @@ import de.markusfisch.android.binaryeye.R
import de.markusfisch.android.binaryeye.actions.IntentAction
import de.markusfisch.android.binaryeye.actions.vtype.VTypeParser
import java.text.SimpleDateFormat
import java.util.*
import java.util.Date
import java.util.Locale
object VEventAction : IntentAction() {
override val iconResId: Int
@@ -7,9 +7,7 @@ import de.markusfisch.android.binaryeye.content.openUrl
object WebAction : IAction {
private val colloquialRegex =
"""^(http[s]*://)*([a-z0-9]+[a-z0-9-]*[a-z0-9]+[.])+[a-z]{2,}([?#/]+[\w/=&;._-]*)*$""".toRegex(
RegexOption.IGNORE_CASE
)
"^(URL:[ ]*)*(http[s]*://)*[A-Za-z0-9-]{3,}\\.[A-Za-z]{2,}[^ \t\r\n]*$".toRegex()
override val iconResId: Int = R.drawable.ic_action_open
override val titleResId: Int = R.string.open_url
@@ -20,6 +18,9 @@ object WebAction : IAction {
override suspend fun execute(context: Context, data: ByteArray) {
var url = String(data).trim()
if (url.startsWith("URL:")) {
url = url.substring(4).trim()
}
if (!url.startsWith("http") && !url.startsWith("ftp")) {
url = "http://${url}"
}
@@ -22,10 +22,9 @@ object WifiAction : IAction {
context: Context,
data: ByteArray
) = withContext(Dispatchers.IO) {
val wifiConfig = WifiConnector.parse(
String(data)
) ?: return@withContext
val message = WifiConnector.addNetwork(context, wifiConfig)
val message = WifiConnector.parse(String(data))?.let {
WifiConnector.addNetwork(context, it)
} ?: R.string.wifi_config_failed
withContext(Dispatchers.Main) {
context.toast(message)
}
@@ -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,12 +44,21 @@ object WifiConnector {
passwordBlock?.apply {
invoke(parsedData.password)
}
return if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
// WifiConfiguration is deprecated in Android Q.
@Suppress("DEPRECATION")
WifiConfiguration().apply(parsedData)
} else {
WifiNetworkSuggestion.Builder().apply(parsedData)
return try {
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q) {
// WifiConfiguration is deprecated in Android Q but
// because it's only possible in R to query network
// suggestions, we still use the deprecated API on Q.
@Suppress("DEPRECATION")
WifiConfiguration().apply(parsedData)
} else {
WifiNetworkSuggestion.Builder().apply(parsedData)
}
} catch (e: IllegalArgumentException) {
// Some arguments may be out of range, e.g. SSIDs cannot
// be longer than 32 characters in some versions of Android.
// See: https://github.com/markusfisch/BinaryEye/issues/402
null
}
}
@@ -63,33 +66,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 +90,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 +119,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 +136,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,10 +172,11 @@ object WifiConnector {
data: SimpleDataAccessor
): WifiNetworkSuggestion.Builder? {
try {
when (data.securityType) {
when (data.securityType.uppercase()) {
"WPA", "WPA2" -> {
data.password?.let { setWpa2Passphrase(it) }
}
"WPA2-EAP" -> {
setWpa2EnterpriseConfig(WifiEnterpriseConfig().apply {
identity = data.identity
@@ -192,9 +186,11 @@ object WifiConnector {
data.phase2Method?.let { phase2Method = it }
})
}
"WPA3" -> {
data.password?.let { setWpa3Passphrase(it) }
}
"WPA3-EAP" -> {
setWpa3EnterpriseConfig(WifiEnterpriseConfig().apply {
identity = data.identity
@@ -240,8 +236,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
@@ -254,6 +250,7 @@ object WifiConnector {
allowedGroupCiphers.set(WifiConfiguration.GroupCipher.WEP40)
allowedGroupCiphers.set(WifiConfiguration.GroupCipher.WEP104)
}
"WPA" -> {
passwordWithQuotes?.let {
preSharedKey = it
@@ -270,6 +267,7 @@ object WifiConnector {
allowedGroupCiphers.set(WifiConfiguration.GroupCipher.TKIP)
allowedGroupCiphers.set(WifiConfiguration.GroupCipher.CCMP)
}
"WPA2-EAP" -> requireSdk(Build.VERSION_CODES.JELLY_BEAN_MR2) {
passwordWithQuotes?.let {
preSharedKey = it
@@ -303,11 +301,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,5 +1,6 @@
package de.markusfisch.android.binaryeye.activity
import android.annotation.SuppressLint
import android.app.Activity
import android.app.AlertDialog
import android.content.Context
@@ -8,22 +9,26 @@ import android.content.pm.PackageManager
import android.graphics.Matrix
import android.graphics.Rect
import android.hardware.Camera
import android.media.AudioManager
import android.media.ToneGenerator
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.Vibrator
import android.support.design.widget.FloatingActionButton
import android.support.v7.app.AppCompatActivity
import android.support.v7.widget.Toolbar
import android.view.Menu
import android.view.MenuItem
import android.view.MotionEvent
import android.view.View
import android.widget.EditText
import android.widget.SeekBar
import de.markusfisch.android.binaryeye.R
import de.markusfisch.android.binaryeye.adapter.prettifyFormatName
import de.markusfisch.android.binaryeye.app.*
import de.markusfisch.android.binaryeye.app.PERMISSION_CAMERA
import de.markusfisch.android.binaryeye.app.applyLocale
import de.markusfisch.android.binaryeye.app.db
import de.markusfisch.android.binaryeye.app.hasBluetoothPermission
import de.markusfisch.android.binaryeye.app.hasCameraPermission
import de.markusfisch.android.binaryeye.app.prefs
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.content.openUrl
@@ -33,35 +38,40 @@ import de.markusfisch.android.binaryeye.graphics.FrameMetrics
import de.markusfisch.android.binaryeye.graphics.mapPosition
import de.markusfisch.android.binaryeye.graphics.setFrameRoi
import de.markusfisch.android.binaryeye.graphics.setFrameToView
import de.markusfisch.android.binaryeye.media.releaseToneGenerators
import de.markusfisch.android.binaryeye.net.sendAsync
import de.markusfisch.android.binaryeye.net.urlEncode
import de.markusfisch.android.binaryeye.os.error
import de.markusfisch.android.binaryeye.os.getVibrator
import de.markusfisch.android.binaryeye.os.vibrate
import de.markusfisch.android.binaryeye.view.initSystemBars
import de.markusfisch.android.binaryeye.preference.Preferences
import de.markusfisch.android.binaryeye.view.errorFeedback
import de.markusfisch.android.binaryeye.view.initBars
import de.markusfisch.android.binaryeye.view.scanFeedback
import de.markusfisch.android.binaryeye.view.setPaddingFromWindowInsets
import de.markusfisch.android.binaryeye.widget.DetectorView
import de.markusfisch.android.binaryeye.widget.toast
import de.markusfisch.android.cameraview.widget.CameraView
import de.markusfisch.android.zxingcpp.ZxingCpp
import de.markusfisch.android.zxingcpp.ZxingCpp.Format
import de.markusfisch.android.zxingcpp.ZxingCpp.BarcodeFormat
import de.markusfisch.android.zxingcpp.ZxingCpp.Binarizer
import de.markusfisch.android.zxingcpp.ZxingCpp.ReaderOptions
import de.markusfisch.android.zxingcpp.ZxingCpp.Result
import java.nio.charset.Charset
import de.markusfisch.android.zxingcpp.ZxingCpp.TextMode
import java.io.FileInputStream
import java.util.Scanner
import kotlin.math.max
import kotlin.math.min
import kotlin.math.roundToInt
class CameraActivity : AppCompatActivity() {
private val frameRoi = Rect()
private val matrix = Matrix()
private lateinit var vibrator: Vibrator
private lateinit var cameraView: CameraView
private lateinit var detectorView: DetectorView
private lateinit var zoomBar: SeekBar
private lateinit var flashFab: FloatingActionButton
private var formats = setOf<String>()
private var formatsToRead = setOf<BarcodeFormat>()
private var frameMetrics = FrameMetrics()
private var decoding = true
private var returnResult = false
@@ -70,8 +80,10 @@ class CameraActivity : AppCompatActivity() {
private var frontFacing = false
private var bulkMode = prefs.bulkMode
private var restrictFormat: String? = null
private var searchTerm: String? = null
private var ignoreNext: String? = null
private var fallbackBuffer: IntArray? = null
private var requestCameraPermission = true
override fun onRequestPermissionsResult(
requestCode: Int,
@@ -82,8 +94,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)
}
}
}
@@ -118,10 +129,7 @@ class CameraActivity : AppCompatActivity() {
// custom locale.
setTitle(R.string.scan_code)
vibrator = getVibrator()
initSystemBars(this)
setSupportActionBar(findViewById(R.id.toolbar) as Toolbar)
initBars()
cameraView = findViewById(R.id.camera_view) as CameraView
detectorView = findViewById(R.id.detector_view) as DetectorView
@@ -133,7 +141,7 @@ class CameraActivity : AppCompatActivity() {
initDetectorView()
if (intent?.action == Intent.ACTION_SEND &&
intent.type == "text/plain"
intent.type?.startsWith("text/") == true
) {
handleSendText(intent)
}
@@ -143,35 +151,69 @@ class CameraActivity : AppCompatActivity() {
super.onDestroy()
fallbackBuffer = null
saveZoom()
detectorView.saveCropHandlePos()
detectorView.storeCropHandlePos(CAMERA_CROP_HANDLE)
releaseToneGenerators()
}
override fun onResume() {
super.onResume()
if (firstStart()) {
startActivity(Intent(this, WelcomeActivity::class.java))
return
}
System.gc()
updateHints()
updateHintsAndTitle()
if (prefs.bulkMode && bulkMode != prefs.bulkMode) {
bulkMode = prefs.bulkMode
invalidateOptionsMenu()
ignoreNext = null
}
setReturnTarget(intent)
if (hasCameraPermission()) {
// Avoid asking multiple times when the user has denied access
// for this session. Otherwise ActivityCompat.requestPermissions()
// will trigger onResume() again and again.
if (hasCameraPermission(requestCameraPermission)) {
openCamera()
}
requestCameraPermission = false
}
private fun updateHints() {
private fun Context.firstStart(): Boolean {
val welcomeShownName = "welcome_shown"
if (prefs.preferences.getBoolean(welcomeShownName, false)) {
android.util.Log.d("mfdbg", "mfdbg: already welcome shown")
return false
}
prefs.preferences.edit().putBoolean(welcomeShownName, true).apply()
val packageInfo = packageManager.getPackageInfo(packageName, 0)
val installedSince = System.currentTimeMillis() - packageInfo.firstInstallTime
android.util.Log.d("mfdbg", "mfdbg: installed since is $installedSince")
return installedSince < 86400000
}
private fun updateHintsAndTitle() {
val restriction = restrictFormat
formats = if (restriction != null) {
title = getString(
R.string.scan_format,
prettifyFormatName(restriction)
)
formatsToRead = if (restriction != null) {
setOf(restriction)
} else {
setTitle(R.string.scan_code)
prefs.barcodeFormats
}.toFormatSet()
updateTitle()
}
private fun updateTitle() {
if (searchTerm != null || restrictFormat != null) {
title = getString(
R.string.scan_format,
prettifyFormatName(
listOfNotNull(
restrictFormat,
searchTerm
).joinToString(",")
)
)
} else {
setTitle(R.string.scan_code)
}
}
@@ -180,6 +222,7 @@ class CameraActivity : AppCompatActivity() {
intent?.action == "com.google.zxing.client.android.SCAN" -> {
returnResult = true
}
intent?.dataString?.isReturnUrl() == true -> {
finishAfterShowingResult = true
returnUrlTemplate = intent.data?.getQueryParameter("ret")
@@ -216,6 +259,7 @@ class CameraActivity : AppCompatActivity() {
frontFacing = savedState.getBoolean(FRONT_FACING)
bulkMode = savedState.getBoolean(BULK_MODE)
restrictFormat = savedState.getString(RESTRICT_FORMAT)
searchTerm = savedState.getString(SEARCH_TERM)
}
override fun onSaveInstanceState(outState: Bundle) {
@@ -224,6 +268,7 @@ class CameraActivity : AppCompatActivity() {
outState.putBoolean(FRONT_FACING, frontFacing)
outState.putBoolean(BULK_MODE, bulkMode)
outState.putString(RESTRICT_FORMAT, restrictFormat)
outState.putString(SEARCH_TERM, searchTerm)
super.onSaveInstanceState(outState)
}
@@ -248,44 +293,57 @@ class CameraActivity : AppCompatActivity() {
createBarcode()
true
}
R.id.history -> {
startActivity(MainActivity.getHistoryIntent(this))
true
}
R.id.pick_file -> {
startActivityForResult(
Intent.createChooser(
Intent(Intent.ACTION_GET_CONTENT).apply {
type = "image/*"
},
getString(R.string.pick_file)
getString(R.string.pick_image_file)
),
PICK_FILE_RESULT_CODE
)
true
}
R.id.switch_camera -> {
switchCamera()
true
}
R.id.bulk_mode -> {
bulkMode = bulkMode xor true
item.isChecked = bulkMode
ignoreNext = null
true
}
R.id.restrict_format -> {
showRestrictionDialog()
true
}
R.id.find_code -> {
askForCode()
true
}
R.id.preferences -> {
startActivity(MainActivity.getPreferencesIntent(this))
true
}
R.id.info -> {
openReadme()
true
}
else -> super.onOptionsItemSelected(item)
}
}
@@ -315,12 +373,36 @@ class CameraActivity : AppCompatActivity() {
setTitle(R.string.restrict_format)
setItems(names.toTypedArray()) { _, which ->
restrictFormat = formats[which]
updateHints()
updateHintsAndTitle()
}
show()
}
}
// Dialogs do not have a parent view.
@SuppressLint("InflateParams")
private fun askForCode() {
val view = layoutInflater.inflate(R.layout.dialog_find_code, null)
val editText = view.findViewById<EditText>(R.id.term)
searchTerm.let {
editText.setText(it)
}
AlertDialog.Builder(this)
.setView(view)
.setPositiveButton(android.R.string.ok) { _, _ ->
searchTerm = editText.text.toString()
if (searchTerm?.isEmpty() == true) {
searchTerm = null
}
updateTitle()
}
.setNegativeButton(android.R.string.cancel) { _, _ ->
searchTerm = null
updateTitle()
}
.show()
}
private fun openReadme() {
val intent = Intent(
Intent.ACTION_VIEW,
@@ -331,9 +413,34 @@ class CameraActivity : AppCompatActivity() {
private fun handleSendText(intent: Intent) {
val text = intent.getStringExtra(Intent.EXTRA_TEXT)
if (text?.isEmpty() == false) {
startActivity(MainActivity.getEncodeIntent(this, text, true))
finish()
return
}
// Read text from file.
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
@Suppress("DEPRECATION")
intent.getParcelableExtra(Intent.EXTRA_STREAM) as Uri?
} else {
intent.getParcelableExtra(Intent.EXTRA_STREAM, Uri::class.java)
}?.let { textUri ->
val file = contentResolver.openFileDescriptor(textUri, "r")
if (file != null) {
val fs = FileInputStream(file.fileDescriptor)
val scn = Scanner(fs).useDelimiter("\\A")
if (scn.hasNext()) {
startActivity(
MainActivity.getEncodeIntent(
this, scn.next(), true
)
)
finish()
}
file.close()
}
}
}
@@ -354,6 +461,7 @@ class CameraActivity : AppCompatActivity() {
progress = zoomBar.progress
return true
}
MotionEvent.ACTION_MOVE -> {
if (prefs.zoomBySwiping) {
v ?: return false
@@ -367,6 +475,7 @@ class CameraActivity : AppCompatActivity() {
return true
}
}
MotionEvent.ACTION_UP -> {
// Stop calling focusTo() as soon as it returns false
// to avoid throwing and catching future exceptions.
@@ -414,7 +523,6 @@ class CameraActivity : AppCompatActivity() {
override fun onCameraError() {
this@CameraActivity.toast(R.string.camera_error)
finish()
}
override fun onCameraReady(camera: Camera) {
@@ -426,22 +534,45 @@ class CameraActivity : AppCompatActivity() {
updateFrameRoiAndMappingMatrix()
ignoreNext = null
decoding = true
// These settings can't change while the camera is open.
val options = ReaderOptions(
tryHarder = prefs.tryHarder,
tryRotate = prefs.autoRotate,
tryInvert = true,
tryDownscale = true,
returnCodabarStartEnd = true,
maxNumberOfSymbols = 1,
textMode = TextMode.PLAIN
)
var useLocalAverage = false
camera.setPreviewCallback { frameData, _ ->
if (decoding) {
useLocalAverage = useLocalAverage xor true
ZxingCpp.readByteArray(
frameData,
frameMetrics.width,
frameRoi.left, frameRoi.top,
frameRoi.width(), frameRoi.height(),
frameMetrics.orientation,
formats.joinToString(),
prefs.tryHarder,
prefs.autoRotate,
tryInvert = true,
tryDownscale = true
)?.let { result ->
if (result.text != ignoreNext) {
postResult(result.redact())
options.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
}
)?.let { results ->
val result = results.first()
val text = result.text
val term = searchTerm
if (text != ignoreNext &&
(term == null || text.contains(term))
) {
postResult(result)
decoding = false
}
}
@@ -511,7 +642,7 @@ class CameraActivity : AppCompatActivity() {
updateFrameRoiAndMappingMatrix()
}
detectorView.setPaddingFromWindowInsets()
detectorView.restoreCropHandlePos()
detectorView.restoreCropHandlePos(CAMERA_CROP_HANDLE)
}
private fun updateFrameRoiAndMappingMatrix() {
@@ -561,7 +692,7 @@ class CameraActivity : AppCompatActivity() {
detectorView.coordinates
)
)
vibrator.vibrate()
scanFeedback()
val returnUri = returnUrlTemplate?.let {
try {
completeUrl(it, result)
@@ -577,16 +708,13 @@ class CameraActivity : AppCompatActivity() {
setResult(Activity.RESULT_OK, getReturnIntent(result))
finish()
}
returnUri != null -> execShareIntent(
Intent(Intent.ACTION_VIEW, returnUri)
)
else -> {
showResult(
this@CameraActivity,
result,
vibrator,
bulkMode
)
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.
@@ -596,7 +724,13 @@ class CameraActivity : AppCompatActivity() {
}
}
if (bulkMode) {
ignoreNext = result.text
when (prefs.ignoreDuplicates()) {
Preferences.Companion.IgnoreDuplicates.Consecutive,
Preferences.Companion.IgnoreDuplicates.Any ->
ignoreNext = result.text
else -> Unit
}
if (prefs.showToastInBulkMode) {
toast(result.text)
}
@@ -608,51 +742,28 @@ class CameraActivity : AppCompatActivity() {
}
companion object {
const val CAMERA_CROP_HANDLE = "camera_crop_handle"
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"
private const val SEARCH_TERM = "search_term"
}
}
fun Result.redact() = if (
format == Format.PDF_417.name &&
text.isNotEmpty()
) {
Result(
format,
contentType,
String(
text.toByteArray(Charset.forName("ISO-8859-1")),
Charset.forName("UTF-8")
),
position,
orientation,
rawBytes,
ecLevel,
symbologyIdentifier,
sequenceSize,
sequenceIndex,
sequenceId,
readerInit,
lineCount,
versionNumber,
gtin
)
} else {
this
}
fun Set<String>.toFormatSet(): Set<BarcodeFormat> = map {
BarcodeFormat.valueOf(it)
}.toSet()
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 = result.toScan()
if (prefs.useHistory) {
@@ -660,8 +771,8 @@ fun showResult(
}
if (prefs.sendScanActive && prefs.sendScanUrl.isNotEmpty()) {
if (prefs.sendScanType == "4") {
activity.openUrl(
prefs.sendScanUrl + scan.content.urlEncode()
openUrl(
prefs.sendScanUrl + scan.text.urlEncode()
)
return
}
@@ -670,28 +781,49 @@ fun showResult(
prefs.sendScanType
) { code, body ->
if (code == null || code < 200 || code > 299) {
vibrator.error()
if (!activity.isSilent()) {
beepBeepBeep()
}
errorFeedback()
}
if (body != null && body.isNotEmpty()) {
activity.toast(body)
if (!body.isNullOrEmpty()) {
toast(body)
} else if (code == null || code > 299) {
activity.toast(R.string.background_request_failed)
toast(R.string.background_request_failed)
}
}
}
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)
)
}
}
private fun getReturnIntent(result: Result) = Intent().apply {
putExtra("SCAN_RESULT", result.text)
putExtra("SCAN_RESULT_FORMAT", result.format)
putExtra("SCAN_RESULT_FORMAT", result.format.toString())
putExtra("SCAN_RESULT_ORIENTATION", result.orientation)
putExtra("SCAN_RESULT_ERROR_CORRECTION_LEVEL", result.ecLevel)
if (result.rawBytes.isNotEmpty()) {
@@ -709,25 +841,7 @@ private fun completeUrl(urlTemplate: String, result: Result) = Uri.parse(
urlTemplate
.replace("{RESULT}", result.text.urlEncode())
.replace("{RESULT_BYTES}", result.rawBytes.toHexString())
.replace(
"{FORMAT}", result.format.urlEncode()
)
.replace("{FORMAT}", result.format.name.urlEncode())
// And support {CODE} from the old ZXing app, too.
.replace("{CODE}", result.text.urlEncode())
)
private fun Context.isSilent(): Boolean {
val am = getSystemService(Context.AUDIO_SERVICE) as AudioManager
return when (am.ringerMode) {
AudioManager.RINGER_MODE_SILENT,
AudioManager.RINGER_MODE_VIBRATE -> true
else -> false
}
}
private fun beepBeepBeep() {
ToneGenerator(AudioManager.STREAM_ALARM, 100).startTone(
ToneGenerator.TONE_CDMA_CONFIRM,
1000
)
}
@@ -3,21 +3,25 @@ package de.markusfisch.android.binaryeye.activity
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.support.v4.app.Fragment
import android.support.v7.app.AppCompatActivity
import android.support.v7.widget.Toolbar
import de.markusfisch.android.binaryeye.R
import de.markusfisch.android.binaryeye.app.*
import de.markusfisch.android.binaryeye.app.PERMISSION_LOCATION
import de.markusfisch.android.binaryeye.app.PERMISSION_WRITE
import de.markusfisch.android.binaryeye.app.applyLocale
import de.markusfisch.android.binaryeye.app.permissionGrantedCallback
import de.markusfisch.android.binaryeye.app.prefs
import de.markusfisch.android.binaryeye.app.setFragment
import de.markusfisch.android.binaryeye.database.Scan
import de.markusfisch.android.binaryeye.fragment.DecodeFragment
import de.markusfisch.android.binaryeye.fragment.EncodeFragment
import de.markusfisch.android.binaryeye.fragment.HistoryFragment
import de.markusfisch.android.binaryeye.fragment.PreferencesFragment
import de.markusfisch.android.binaryeye.view.colorSystemAndToolBars
import de.markusfisch.android.binaryeye.view.initSystemBars
import de.markusfisch.android.binaryeye.view.recordToolbarHeight
import de.markusfisch.android.binaryeye.view.initBars
class MainActivity : AppCompatActivity() {
override fun onRequestPermissionsResult(
@@ -54,10 +58,7 @@ class MainActivity : AppCompatActivity() {
super.onCreate(state)
setContentView(R.layout.activity_main)
initSystemBars(this)
val toolbar = findViewById(R.id.toolbar) as Toolbar
recordToolbarHeight(toolbar)
setSupportActionBar(toolbar)
initBars()
supportActionBar?.setDisplayHomeAsUpEnabled(true)
supportFragmentManager.addOnBackStackChangedListener {
@@ -80,10 +81,24 @@ class MainActivity : AppCompatActivity() {
return when {
intent.hasExtra(PREFERENCES) -> PreferencesFragment()
intent.hasExtra(HISTORY) -> HistoryFragment()
intent.hasExtra(ENCODE) -> EncodeFragment.newInstance()
intent.hasExtra(DECODED) -> DecodeFragment.newInstance(
intent.getParcelableExtra(DECODED)!!
intent.dataString?.startsWith("binaryeye://encode") == true -> {
val uri = Uri.parse(intent.dataString)
EncodeFragment.newInstance(
uri.getQueryParameter("content"),
uri.getQueryParameter("format"),
!uri.getQueryParameter("execute").isNullOrEmpty()
)
}
intent.hasExtra(ENCODE) -> EncodeFragment.newInstance(
intent.getStringExtra(ENCODE)
)
intent.hasExtra(DECODED) -> intent.getScanExtra(DECODED)?.let {
DecodeFragment.newInstance(it)
} ?: DecodeFragment()
else -> PreferencesFragment()
}
}
@@ -128,3 +143,12 @@ class MainActivity : AppCompatActivity() {
}
}
}
private fun Intent.getScanExtra(name: String): Scan? = if (
Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU
) {
@Suppress("DEPRECATION")
getParcelableExtra(name)
} else {
getParcelableExtra(name, Scan::class.java)
}
@@ -7,11 +7,9 @@ import android.graphics.Matrix
import android.graphics.Rect
import android.graphics.RectF
import android.net.Uri
import android.os.Build
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.view.Menu
import android.view.MenuItem
import de.markusfisch.android.binaryeye.R
@@ -21,18 +19,23 @@ 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.mapPosition
import de.markusfisch.android.binaryeye.os.getVibrator
import de.markusfisch.android.binaryeye.os.vibrate
import de.markusfisch.android.binaryeye.media.releaseToneGenerators
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.initBars
import de.markusfisch.android.binaryeye.view.scanFeedback
import de.markusfisch.android.binaryeye.view.setPaddingFromWindowInsets
import de.markusfisch.android.binaryeye.widget.CropImageView
import de.markusfisch.android.binaryeye.widget.DetectorView
import de.markusfisch.android.binaryeye.widget.toast
import de.markusfisch.android.zxingcpp.ZxingCpp
import de.markusfisch.android.zxingcpp.ZxingCpp.Binarizer
import de.markusfisch.android.zxingcpp.ZxingCpp.ReaderOptions
import de.markusfisch.android.zxingcpp.ZxingCpp.Result
import kotlinx.coroutines.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlin.math.max
import kotlin.math.min
import kotlin.math.roundToInt
@@ -41,8 +44,16 @@ class PickActivity : AppCompatActivity() {
private val matrix = Matrix()
private val parentJob = Job()
private val scope = CoroutineScope(Dispatchers.IO + parentJob)
private val options = ReaderOptions(
tryHarder = true,
tryRotate = true,
tryInvert = true,
tryDownscale = true,
returnCodabarStartEnd = true,
maxNumberOfSymbols = 1,
formats = prefs.barcodeFormats.toFormatSet()
)
private lateinit var vibrator: Vibrator
private lateinit var cropImageView: CropImageView
private lateinit var detectorView: DetectorView
private lateinit var freeRotationItem: MenuItem
@@ -62,12 +73,7 @@ class PickActivity : AppCompatActivity() {
// locale.
setTitle(R.string.pick_code_to_scan)
vibrator = getVibrator()
initSystemBars(this)
val toolbar = findViewById(R.id.toolbar) as Toolbar
recordToolbarHeight(toolbar)
setSupportActionBar(toolbar)
initBars()
supportFragmentManager.addOnBackStackChangedListener {
colorSystemAndToolBars(this@PickActivity)
@@ -80,20 +86,28 @@ class PickActivity : AppCompatActivity() {
return
}
cropImageView = findViewById(R.id.image) as CropImageView
cropImageView.restrictTranslation = false
cropImageView.freeRotation = prefs.freeRotation
cropImageView.setImageBitmap(bitmap)
cropImageView.onScan = {
scanWithinBounds(bitmap)
cropImageView = (findViewById(R.id.image) as CropImageView).apply {
restrictTranslation = false
freeRotation = prefs.freeRotation
setImageBitmap(bitmap)
onScan = {
scanWithinBounds(bitmap)
}
runAfterLayout = {
minWidth = min(
minWidth / 2f,
max(bitmap.width, bitmap.height).toFloat()
)
}
}
detectorView = findViewById(R.id.detector_view) as DetectorView
detectorView.onRoiChanged = {
scanWithinBounds(bitmap)
detectorView = (findViewById(R.id.detector_view) as DetectorView).apply {
onRoiChanged = {
scanWithinBounds(bitmap)
}
setPaddingFromWindowInsets()
restoreCropHandlePos(PICKER_CROP_HANDLE)
}
detectorView.setPaddingFromWindowInsets()
detectorView.restoreCropHandlePos()
findViewById(R.id.scan).setOnClickListener {
showResult()
@@ -115,52 +129,47 @@ class PickActivity : AppCompatActivity() {
}
private fun scanWithinBounds(bitmap: Bitmap) {
val viewRoi = if (detectorView.roi.width() < 1) {
cropImageView.getBoundsRect()
} else {
detectorView.roi
}
val mappedRect = cropImageView.mappedRect
val cropped = bitmap.crop(
getNormalizedRoi(mappedRect, viewRoi),
cropImageView.imageRotation,
cropImageView.pivotX,
cropImageView.pivotY
) ?: return
val croppedInView = Rect(
max(viewRoi.left, mappedRect.left.roundToInt()),
max(viewRoi.top, mappedRect.top.roundToInt()),
min(viewRoi.right, mappedRect.right.roundToInt()),
min(viewRoi.bottom, mappedRect.bottom.roundToInt())
val viewRoi = Rect(
if (detectorView.roi.width() < 1) {
cropImageView.getBoundsRect()
} else {
detectorView.roi
}
)
matrix.apply {
setScale(
croppedInView.width().toFloat() / cropped.width,
croppedInView.height().toFloat() / cropped.height
)
postTranslate(
croppedInView.left.toFloat(),
croppedInView.top.toFloat()
)
}
val mappedRect = RectF(cropImageView.mappedRect)
val imageRotation = cropImageView.imageRotation
val pivotX = cropImageView.pivotX
val pivotY = cropImageView.pivotY
scope.launch {
ZxingCpp.readBitmap(
cropped,
0, 0,
cropped.width, cropped.height,
0,
prefs.barcodeFormats.joinToString(),
tryHarder = true,
tryRotate = true,
tryInvert = true,
tryDownscale = true
)?.let {
val cropped = bitmap.crop(
getNormalizedRoi(mappedRect, viewRoi),
imageRotation,
pivotX,
pivotY
) ?: return@launch
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()
)
}
cropped.decode()?.first()?.let {
withContext(Dispatchers.Main) {
if (isFinishing) {
return@withContext
}
result = it
vibrator.vibrate()
scanFeedback()
detectorView.update(
matrix.mapPosition(
it.position,
@@ -172,10 +181,31 @@ 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,
options.apply {
binarizer = Binarizer.LOCAL_AVERAGE
}
) ?: ZxingCpp.readBitmap(
this,
0, 0,
width, height,
0,
options.apply {
binarizer = Binarizer.GLOBAL_HISTOGRAM
}
)
override fun onDestroy() {
super.onDestroy()
detectorView.saveCropHandlePos()
detectorView.storeCropHandlePos(PICKER_CROP_HANDLE)
parentJob.cancel()
releaseToneGenerators()
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
@@ -192,12 +222,14 @@ 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)
}
}
@@ -213,13 +245,11 @@ class PickActivity : AppCompatActivity() {
}
private fun rotateClockwise() {
cropImageView.imageRotation = cropImageView.imageRotation + 90 % 360
cropImageView.imageRotation = (cropImageView.imageRotation + 90) % 360
}
private fun loadSentImage(intent: Intent): Bitmap? {
val uri = intent.getParcelableExtra<Parcelable>(
Intent.EXTRA_STREAM
) as? Uri ?: return null
val uri = intent.getUriExtra() ?: return null
return contentResolver.loadImageUri(uri)
}
@@ -231,12 +261,16 @@ class PickActivity : AppCompatActivity() {
private fun showResult() {
val r = result
if (r != null) {
showResult(this, r.redact(), vibrator)
showResult(r)
finish()
} else {
applicationContext.toast(R.string.no_barcode_found)
}
}
companion object {
private const val PICKER_CROP_HANDLE = "picker_crop_handle"
}
}
private fun getNormalizedRoi(imageRect: RectF, roi: Rect): RectF {
@@ -249,3 +283,12 @@ private fun getNormalizedRoi(imageRect: RectF, roi: Rect): RectF {
(roi.bottom - imageRect.top) / h
)
}
private fun Intent.getUriExtra(): Uri? = if (
Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU
) {
@Suppress("DEPRECATION")
getParcelableExtra(Intent.EXTRA_STREAM)
} else {
getParcelableExtra(Intent.EXTRA_STREAM, Uri::class.java)
}
@@ -0,0 +1,32 @@
package de.markusfisch.android.binaryeye.activity
import android.os.Bundle
import android.support.v7.app.AppCompatActivity
import de.markusfisch.android.binaryeye.R
import de.markusfisch.android.binaryeye.app.prefs
class WelcomeActivity : AppCompatActivity() {
override fun onCreate(state: Bundle?) {
super.onCreate(state)
setContentView(R.layout.activity_welcome)
findViewById(R.id.expert).setOnClickListener {
finish()
}
findViewById(R.id.simple).setOnClickListener {
prefs.apply {
showCropHandle = false
zoomBySwiping = false
beep = true
copyImmediately = true
openImmediately = true
showMetaData = false
showHexDump = false
defaultSearchUrl = "https://www.google.com/search?q="
openWithUrl = defaultSearchUrl
expandEscapeSequences = false
brightenScreen = true
}
finish()
}
}
}
@@ -13,14 +13,14 @@ import de.markusfisch.android.binaryeye.database.Database
import java.text.DateFormat
import java.text.ParseException
import java.text.SimpleDateFormat
import java.util.*
import java.util.Locale
class ScansAdapter(context: Context, cursor: Cursor) :
CursorAdapter(context, cursor, false) {
private val idIndex = cursor.getColumnIndex(Database.SCANS_ID)
private val timeIndex = cursor.getColumnIndex(Database.SCANS_DATETIME)
private val nameIndex = cursor.getColumnIndex(Database.SCANS_NAME)
private val contentIndex = cursor.getColumnIndex(Database.SCANS_CONTENT)
private val textIndex = cursor.getColumnIndex(Database.SCANS_TEXT)
private val formatIndex = cursor.getColumnIndex(Database.SCANS_FORMAT)
private val selections = mutableMapOf<Long, Int>()
private val selectedColor = ContextCompat.getColor(
@@ -74,7 +74,7 @@ class ScansAdapter(context: Context, cursor: Cursor) :
fun getContent(
position: Int
) = (getItem(position) as Cursor?)?.getString(contentIndex)
) = (getItem(position) as Cursor?)?.getString(textIndex)
override fun newView(
context: Context,
@@ -92,17 +92,19 @@ class ScansAdapter(context: Context, cursor: Cursor) :
getViewHolder(view).apply {
timeView.text = formatDateTime(cursor.getString(timeIndex))
val name = cursor.getString(nameIndex)
val content = cursor.getString(contentIndex)
val text = cursor.getString(textIndex)
var icon = 0
contentView.text = when {
name?.isNotEmpty() == true -> {
icon = R.drawable.ic_label
name
}
content?.isEmpty() == true -> context.getString(
text.isNullOrEmpty() -> context.getString(
R.string.binary_data
)
else -> content
else -> text
}
contentView.setCompoundDrawablesWithIntrinsicBounds(
icon, 0, 0, 0
@@ -2,7 +2,7 @@ package de.markusfisch.android.binaryeye.app
import android.content.Context
import android.os.Build
import java.util.*
import java.util.Locale
fun Context.applyLocale(localeName: String) {
if (localeName.isEmpty()) {
@@ -3,15 +3,16 @@ 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 Activity.hasCameraPermission() = hasPermission(
fun Activity.hasCameraPermission(request: Boolean = true) = hasPermission(
Manifest.permission.CAMERA,
PERMISSION_CAMERA
if (request) PERMISSION_CAMERA else 0
)
const val PERMISSION_WRITE = 2
@@ -32,6 +33,18 @@ fun Activity.hasLocationPermission(callback: () -> Any): Boolean {
)
}
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
@@ -40,11 +53,13 @@ private fun Activity.hasPermission(
permission
) != PackageManager.PERMISSION_GRANTED
) {
ActivityCompat.requestPermissions(
this,
arrayOf(permission),
requestCode
)
if (requestCode > 0) {
ActivityCompat.requestPermissions(
this,
arrayOf(permission),
requestCode
)
}
false
} else {
true
@@ -0,0 +1,123 @@
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.UUID
fun Scan.sendBluetoothAsync(
host: String,
callback: (Boolean, Boolean) -> Unit
) {
CoroutineScope(Dispatchers.IO).launch(Dispatchers.IO) {
val connected = if (isConnected) {
true
} else {
connect(host, true)
}
val sent = if (connected) {
send(text, host, true)
} 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, onceMore: Boolean): 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) {
if (onceMore)
connect(deviceName, false)
else
close()
false
}
private fun send(message: String, host: String, onceMore: Boolean): Boolean = try {
writer?.apply {
write(message)
write("\n")
flush()
}
true
} catch (e: Exception) {
close()
if (connect(host, false))
send(message, host, false)
else
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
}
@@ -0,0 +1,197 @@
package de.markusfisch.android.binaryeye.content
import android.graphics.Bitmap
import de.markusfisch.android.binaryeye.database.Scan
import de.markusfisch.android.binaryeye.graphics.COLOR_BLACK
import de.markusfisch.android.binaryeye.graphics.COLOR_WHITE
import de.markusfisch.android.zxingcpp.ZxingCpp
import de.markusfisch.android.zxingcpp.ZxingCpp.BarcodeFormat
import de.markusfisch.android.zxingcpp.ZxingCpp.BitMatrix
import de.markusfisch.android.zxingcpp.ZxingCpp.toBitmap
import de.markusfisch.android.zxingcpp.ZxingCpp.toSvg
import de.markusfisch.android.zxingcpp.ZxingCpp.toText
import java.util.Arrays
enum class BarcodeColors {
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
}
}
fun Scan.toBarcode(): Barcode<*> {
val content = text.ifEmpty {
raw
}
return if (symbol != null) {
BitMatrixBarcode(
symbol,
content,
format
)
} else {
ContentBarcode(
content,
format,
errorCorrectionLevel.toErrorCorrectionInt()
)
}
}
fun String?.toErrorCorrectionInt() = when (this) {
"L" -> 0
"M" -> 4
"Q" -> 6
"H" -> 8
else -> -1
}
abstract class Barcode<T>(
val content: T,
val format: BarcodeFormat,
val colors: BarcodeColors
) {
private var _bitmap: Bitmap? = null
fun bitmap(): Bitmap {
val b = _bitmap ?: bitmap(512)
_bitmap = b
return b
}
fun bitmap(size: Int): Bitmap {
return toBitmap(size)
}
protected abstract fun toBitmap(size: Int): Bitmap
private var _svg: String? = null
fun svg(): String {
val s = _svg ?: toSvg()
_svg = s
return s
}
protected abstract fun toSvg(): String
private var _text: String? = null
fun text(): String {
val t = _text ?: toText()
_text = t
return t
}
protected abstract fun toText(): String
}
class ContentBarcode<T>(
content: T,
format: BarcodeFormat,
val ecLevel: Int = -1,
val margin: Int = -1,
colors: BarcodeColors = BarcodeColors.BLACK_ON_WHITE
) : Barcode<T>(content, format, colors) {
override fun toBitmap(size: Int) = ZxingCpp.encodeAsBitmap(
content, format, size, format.height(size), margin, ecLevel,
setColor = colors.foregroundColor(),
unsetColor = colors.backgroundColor()
)
override fun toSvg() = ZxingCpp.encodeAsSvg(
content, format, margin, ecLevel
)
override fun toText() = ZxingCpp.encodeAsText(
content, format, margin, ecLevel,
inverted = colors == BarcodeColors.BLACK_ON_WHITE
)
}
class BitMatrixBarcode<T>(
val bitMatrix: BitMatrix,
content: T,
format: BarcodeFormat,
colors: BarcodeColors = BarcodeColors.BLACK_ON_WHITE
) : Barcode<T>(content, format, colors) {
override fun toBitmap(size: Int) = bitMatrix.inflate(size).toBitmap(
setColor = colors.foregroundColor(),
unsetColor = colors.backgroundColor()
)
override fun toSvg() = bitMatrix.inflate().toSvg()
override fun toText() = bitMatrix.inflate().toText(
inverted = colors == BarcodeColors.BLACK_ON_WHITE
)
}
private fun BitMatrix.inflate(
size: Int = 0,
quietZone: Boolean = true
): BitMatrix {
val extraModules = if (quietZone) 2 else 0
val codeWidth = width + extraModules
val codeHeight = height + extraModules
var scale = size / codeWidth
if (scale < 1) {
if (size > 0 && !quietZone) {
return this
}
scale = 1
}
val outWidth = codeWidth * scale
val outHeight = codeHeight * scale
val scaled = BitMatrix(
outWidth,
outHeight,
ByteArray(outWidth * outHeight)
)
Arrays.fill(scaled.data, 1)
val padding = extraModules / 2
val left = padding * scale
val top = padding * scale
val right = outWidth - left
val bottom = outHeight - top
for (y in top until bottom) {
var dst = y * outWidth + left
val offset = (y - top) / scale * width
for (x in left until right) {
scaled.data[dst++] = data[
offset + (x - left) / scale
]
}
}
return scaled
}
private fun BarcodeFormat.height(size: Int) = when (this) {
// 1D barcodes don't need as much vertical space.
BarcodeFormat.CODABAR,
BarcodeFormat.CODE_39,
BarcodeFormat.CODE_93,
BarcodeFormat.CODE_128,
BarcodeFormat.DATA_BAR,
BarcodeFormat.DATA_BAR_EXPANDED,
BarcodeFormat.DX_FILM_EDGE,
BarcodeFormat.EAN_8,
BarcodeFormat.EAN_13,
BarcodeFormat.ITF,
BarcodeFormat.UPC_A,
BarcodeFormat.UPC_E -> size / 3
else -> size
}
@@ -10,6 +10,8 @@ import de.markusfisch.android.binaryeye.BuildConfig
import de.markusfisch.android.binaryeye.R
import de.markusfisch.android.binaryeye.widget.toast
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
fun Context.execShareIntent(intent: Intent): Boolean = if (
!startIntent(intent)
@@ -35,11 +37,14 @@ fun Context.startIntent(intent: Intent): Boolean = try {
false
}
fun Context.openUrl(url: String): Boolean = openUri(url.parseAndNormalizeUri())
fun Context.openUri(uri: Uri): Boolean = execShareIntent(
Intent(Intent.ACTION_VIEW, uri)
)
fun Context.openUrl(url: String, silent: Boolean = false): Boolean {
val intent = Intent(Intent.ACTION_VIEW, url.parseAndNormalizeUri())
return if (silent) {
startIntent(intent)
} else {
execShareIntent(intent)
}
}
fun String.parseAndNormalizeUri(): Uri = Uri.parse(this).let {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
@@ -56,6 +61,44 @@ fun Context.shareText(text: String, mimeType: String = "text/plain") {
})
}
private var lastShareFile: File? = null
fun wipeLastShareFile() {
lastShareFile?.let {
it.delete()
lastShareFile = null
}
}
fun <T> Context.shareAsFile(content: T, fileName: String) {
val (bytes, mimeType) = when (content) {
is ByteArray -> Pair(
content,
"text/plain"
)
is String -> Pair(
content.toByteArray(),
"application/octet-stream"
)
else -> throw IllegalArgumentException(
"Unsupported content text for shareAsFile()"
)
}
wipeLastShareFile()
val file = File(externalCacheDir, fileName)
try {
FileOutputStream(file).use {
it.write(bytes)
}
} catch (e: IOException) {
toast(R.string.error_saving_file)
return
}
lastShareFile = file
shareFile(file, mimeType)
}
fun Context.shareFile(file: File, mimeType: String) {
getUriForFile(file)?.let {
shareUri(it, mimeType)
@@ -32,9 +32,10 @@ private fun exportCsv(
val columns = arrayOf(
Database.SCANS_DATETIME,
Database.SCANS_FORMAT,
Database.SCANS_CONTENT,
Database.SCANS_NAME,
Database.SCANS_TEXT,
Database.SCANS_ERROR_CORRECTION_LEVEL,
Database.SCANS_VERSION_NUMBER,
Database.SCANS_VERSION,
Database.SCANS_SEQUENCE_SIZE,
Database.SCANS_SEQUENCE_INDEX,
Database.SCANS_SEQUENCE_ID,
@@ -46,7 +47,7 @@ private fun exportCsv(
val indices = columns.map {
cursor.getColumnIndex(it)
}
val contentIndex = cursor.getColumnIndex(Database.SCANS_CONTENT)
val contentIndex = cursor.getColumnIndex(Database.SCANS_TEXT)
val rawIndex = cursor.getColumnIndex(Database.SCANS_RAW)
outputStream.write(
columns.joinToString(
@@ -1,6 +1,7 @@
package de.markusfisch.android.binaryeye.database
import android.database.Cursor
import de.markusfisch.android.zxingcpp.ZxingCpp
// Overwrite Kotlin's ".use" function for Cursor because Cursor cannot
// be cast to Closeable below API level 16. This should be removed when
@@ -30,3 +31,18 @@ fun Cursor.getBlob(name: String): ByteArray? {
val idx = getColumnIndex(name)
return if (idx < 0) null else getBlob(idx)
}
fun Cursor.getBitMatrix(
widthName: String,
heightName: String,
dataName: String
): ZxingCpp.BitMatrix? {
val width = getInt(widthName)
val height = getInt(heightName)
val data = getBlob(dataName)
return if (width > 0 && height > 0 && data != null) {
ZxingCpp.BitMatrix(width, height, data)
} else {
null
}
}
@@ -6,6 +6,8 @@ import android.database.Cursor
import android.database.sqlite.SQLiteDatabase
import android.database.sqlite.SQLiteOpenHelper
import de.markusfisch.android.binaryeye.app.prefs
import de.markusfisch.android.binaryeye.preference.Preferences
import de.markusfisch.android.zxingcpp.ZxingCpp.BarcodeFormat
class Database {
private lateinit var db: SQLiteDatabase
@@ -14,12 +16,12 @@ class Database {
db = OpenHelper(context).writableDatabase
}
fun getScans(query: String? = null): Cursor? = db.rawQuery(
fun getScans(query: String? = null): Cursor = db.rawQuery(
"""SELECT
$SCANS_ID,
$SCANS_DATETIME,
$SCANS_NAME,
$SCANS_CONTENT,
$SCANS_TEXT,
$SCANS_FORMAT
FROM $SCANS
${getWhereClause(query)}
@@ -27,16 +29,16 @@ class Database {
""".trimMargin(), getWhereArguments(query)
)
fun getScansDetailed(query: String? = null): Cursor? = db.rawQuery(
fun getScansDetailed(query: String? = null): Cursor = db.rawQuery(
"""SELECT
$SCANS_ID,
$SCANS_DATETIME,
$SCANS_NAME,
$SCANS_CONTENT,
$SCANS_TEXT,
$SCANS_RAW,
$SCANS_FORMAT,
$SCANS_ERROR_CORRECTION_LEVEL,
$SCANS_VERSION_NUMBER,
$SCANS_VERSION,
$SCANS_SEQUENCE_SIZE,
$SCANS_SEQUENCE_INDEX,
$SCANS_SEQUENCE_ID,
@@ -54,7 +56,7 @@ class Database {
query: String?,
prefix: String = "WHERE"
) = if (query?.isNotEmpty() == true) {
"""$prefix $SCANS_CONTENT LIKE ?
"""$prefix $SCANS_TEXT LIKE ?
OR $SCANS_NAME LIKE ?"""
} else {
""
@@ -69,16 +71,43 @@ class Database {
null
}
fun getScansDetailed(ids: LongArray): Cursor = db.rawQuery(
"""SELECT
$SCANS_ID,
$SCANS_DATETIME,
$SCANS_NAME,
$SCANS_TEXT,
$SCANS_RAW,
$SCANS_FORMAT,
$SCANS_ERROR_CORRECTION_LEVEL,
$SCANS_VERSION,
$SCANS_SEQUENCE_SIZE,
$SCANS_SEQUENCE_INDEX,
$SCANS_SEQUENCE_ID,
$SCANS_GTIN_COUNTRY,
$SCANS_GTIN_ADD_ON,
$SCANS_GTIN_PRICE,
$SCANS_GTIN_ISSUE_NUMBER
FROM $SCANS
WHERE $SCANS_ID IN (${ids.joinToString(",")})
ORDER BY $SCANS_DATETIME DESC
""".trimMargin(), null
)
fun getScan(id: Long): Scan? = db.rawQuery(
"""SELECT
$SCANS_ID,
$SCANS_DATETIME,
$SCANS_NAME,
$SCANS_CONTENT,
$SCANS_TEXT,
$SCANS_RAW,
$SCANS_FORMAT,
$SCANS_ERROR_CORRECTION_LEVEL,
$SCANS_VERSION_NUMBER,
$SCANS_VERSION,
$SCANS_DATA_MASK,
$SCANS_SYMBOL_WIDTH,
$SCANS_SYMBOL_HEIGHT,
$SCANS_SYMBOL_DATA,
$SCANS_SEQUENCE_SIZE,
$SCANS_SEQUENCE_INDEX,
$SCANS_SEQUENCE_ID,
@@ -89,14 +118,20 @@ class Database {
FROM $SCANS
WHERE $SCANS_ID = ?
""".trimMargin(), arrayOf("$id")
)?.use {
).use {
if (it.moveToFirst()) {
Scan(
it.getString(SCANS_CONTENT),
it.getString(SCANS_TEXT),
it.getBlob(SCANS_RAW),
it.getString(SCANS_FORMAT),
BarcodeFormat.valueOf(it.getString(SCANS_FORMAT)),
it.getString(SCANS_ERROR_CORRECTION_LEVEL),
it.getInt(SCANS_VERSION_NUMBER),
it.getString(SCANS_VERSION),
it.getInt(SCANS_DATA_MASK),
it.getBitMatrix(
SCANS_SYMBOL_WIDTH,
SCANS_SYMBOL_HEIGHT,
SCANS_SYMBOL_DATA
),
it.getInt(SCANS_SEQUENCE_SIZE),
it.getInt(SCANS_SEQUENCE_INDEX),
it.getString(SCANS_SEQUENCE_ID),
@@ -105,77 +140,113 @@ class Database {
it.getString(SCANS_GTIN_PRICE),
it.getString(SCANS_GTIN_ISSUE_NUMBER),
it.getString(SCANS_DATETIME),
it.getLong(SCANS_ID)
it.getLong(SCANS_ID),
it.getString(SCANS_NAME),
)
} else {
null
}
}
fun insertScan(scan: Scan): Long = db.insert(
SCANS,
null,
ContentValues().apply {
put(SCANS_DATETIME, scan.dateTime)
val isRaw = scan.raw != null
if (isRaw) {
put(SCANS_CONTENT, "")
put(SCANS_RAW, scan.raw)
} else {
put(SCANS_CONTENT, scan.content)
}
put(SCANS_FORMAT, scan.format)
scan.errorCorrectionLevel?.let {
put(SCANS_ERROR_CORRECTION_LEVEL, it)
}
put(SCANS_VERSION_NUMBER, scan.versionNumber)
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) {
fun insertScan(scan: Scan): Long {
when (prefs.ignoreDuplicates()) {
Preferences.Companion.IgnoreDuplicates.Consecutive -> {
val id = getIdOfLastScan(
get(SCANS_CONTENT) as String,
if (isRaw) get(SCANS_RAW) as ByteArray else null,
scan.text,
scan.raw,
scan.format
)
if (id > 0L) {
return id
}
}
Preferences.Companion.IgnoreDuplicates.Any -> {
val id = getIdOfScanByContent(scan.text, scan.format)
if (id > 0L) {
return id
}
}
else -> Unit
}
)
return db.insert(
SCANS,
null,
ContentValues().apply {
put(SCANS_DATETIME, scan.dateTime)
put(SCANS_TEXT, scan.text)
if (scan.raw != null) {
put(SCANS_RAW, scan.raw)
}
put(SCANS_FORMAT, scan.format.name)
scan.errorCorrectionLevel?.let {
put(SCANS_ERROR_CORRECTION_LEVEL, it)
}
put(SCANS_VERSION, scan.version)
put(SCANS_DATA_MASK, scan.dataMask)
if (scan.symbol != null) {
put(SCANS_SYMBOL_WIDTH, scan.symbol.width)
put(SCANS_SYMBOL_HEIGHT, scan.symbol.height)
put(SCANS_SYMBOL_DATA, scan.symbol.data)
}
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) }
}
)
}
private fun getIdOfLastScan(
content: String,
raw: ByteArray?,
format: String
format: BarcodeFormat
): Long = db.rawQuery(
"""SELECT
$SCANS_ID,
$SCANS_CONTENT,
$SCANS_TEXT,
$SCANS_RAW,
$SCANS_FORMAT
FROM $SCANS
ORDER BY $SCANS_ID DESC
LIMIT 1
""".trimMargin(), null
)?.use {
).use {
if (it.count > 0 &&
it.moveToFirst() &&
it.getString(SCANS_CONTENT) == content &&
it.getString(SCANS_TEXT) == content &&
(raw == null || it.getBlob(SCANS_RAW)
?.contentEquals(raw) == true) &&
it.getString(SCANS_FORMAT) == format
it.getString(SCANS_FORMAT) == format.name
) {
it.getLong(SCANS_ID)
} else {
0L
}
} ?: 0L
}
private fun getIdOfScanByContent(
content: String,
format: BarcodeFormat
): Long = db.rawQuery(
"""SELECT
$SCANS_ID
FROM $SCANS
WHERE $SCANS_TEXT = ?
AND $SCANS_FORMAT = ?
LIMIT 1
""".trimMargin(), arrayOf(content, format.name)
).use {
if (it.moveToFirst()) {
it.getLong(SCANS_ID)
} else {
0L
}
}
fun removeScan(id: Long) {
db.delete(SCANS, "$SCANS_ID = ?", arrayOf("$id"))
@@ -192,9 +263,9 @@ class Database {
}
private class OpenHelper(context: Context) :
SQLiteOpenHelper(context, FILE_NAME, null, 5) {
SQLiteOpenHelper(context, FILE_NAME, null, 8) {
override fun onCreate(db: SQLiteDatabase) {
createScans(db)
db.createScans()
}
override fun onUpgrade(
@@ -203,16 +274,25 @@ 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) {
migrateToZxingCpp(db)
db.migrateToZxingCpp()
}
if (oldVersion < 6) {
db.migrateToVersionString()
}
if (oldVersion < 7) {
db.addDataMaskColumn()
}
if (oldVersion < 8) {
db.addSymbolColumns()
}
}
}
@@ -223,11 +303,16 @@ class Database {
const val SCANS_ID = "_id"
const val SCANS_DATETIME = "_datetime"
const val SCANS_NAME = "name"
const val SCANS_CONTENT = "content"
const val SCANS_TEXT = "content"
const val SCANS_RAW = "raw"
const val SCANS_FORMAT = "format"
const val SCANS_ERROR_CORRECTION_LEVEL = "error_correction_level"
const val SCANS_VERSION_NUMBER = "version_number"
const val SCANS_VERSION = "version"
const val SCANS_DATA_MASK = "data_mask"
const val SCANS_SYMBOL_WIDTH = "symbol_width"
const val SCANS_SYMBOL_HEIGHT = "symbol_height"
const val SCANS_SYMBOL_DATA = "symbol_data"
const val SCANS_ISSUE_NUMBER = "issue_number"
const val SCANS_ORIENTATION = "orientation"
const val SCANS_OTHER_META_DATA = "other_meta_data"
@@ -243,18 +328,22 @@ class Database {
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".trimMargin())
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,
$SCANS_NAME TEXT,
$SCANS_CONTENT TEXT NOT NULL,
$SCANS_TEXT TEXT NOT NULL,
$SCANS_RAW BLOB,
$SCANS_FORMAT TEXT NOT NULL,
$SCANS_ERROR_CORRECTION_LEVEL TEXT,
$SCANS_VERSION_NUMBER INTEGER,
$SCANS_VERSION TEXT,
$SCANS_DATA_MASK INTEGER,
$SCANS_SYMBOL_WIDTH INTEGER,
$SCANS_SYMBOL_HEIGHT INTEGER,
$SCANS_SYMBOL_DATA BLOB,
$SCANS_SEQUENCE_SIZE INTEGER,
$SCANS_SEQUENCE_INDEX INTEGER,
$SCANS_SEQUENCE_ID TEXT,
@@ -266,44 +355,56 @@ class Database {
)
}
private fun addRawColumn(db: SQLiteDatabase) {
db.execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_RAW BLOB".trimMargin())
private fun SQLiteDatabase.addRawColumn() {
execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_RAW BLOB".trimMargin())
}
private fun addMetaDataColumns(db: SQLiteDatabase) {
db.apply {
execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_ERROR_CORRECTION_LEVEL TEXT".trimMargin())
execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_ISSUE_NUMBER INT".trimMargin())
execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_ORIENTATION INT".trimMargin())
execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_OTHER_META_DATA TEXT".trimMargin())
execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_PDF417_EXTRA_METADATA TEXT".trimMargin())
execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_POSSIBLE_COUNTRY TEXT".trimMargin())
execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_SUGGESTED_PRICE TEXT".trimMargin())
execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_UPC_EAN_EXTENSION TEXT".trimMargin())
}
private fun 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".trimMargin())
private fun SQLiteDatabase.addNameColumn() {
execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_NAME TEXT")
}
private fun migrateToZxingCpp(db: SQLiteDatabase) {
db.apply {
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.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 SQLiteDatabase.addDataMaskColumn() {
execSQL("ALTER TABLE $SCANS ADD $SCANS_DATA_MASK INTEGER")
execSQL("UPDATE $SCANS SET $SCANS_DATA_MASK = -1")
}
private fun SQLiteDatabase.addSymbolColumns() {
execSQL("ALTER TABLE $SCANS ADD $SCANS_SYMBOL_WIDTH INTEGER")
execSQL("ALTER TABLE $SCANS ADD $SCANS_SYMBOL_HEIGHT INTEGER")
execSQL("ALTER TABLE $SCANS ADD $SCANS_SYMBOL_DATA BLOB")
}
}
}
@@ -24,9 +24,10 @@ fun Cursor.exportJson(): String? {
val columns = arrayOf(
Database.SCANS_DATETIME,
Database.SCANS_FORMAT,
Database.SCANS_CONTENT,
Database.SCANS_NAME,
Database.SCANS_TEXT,
Database.SCANS_ERROR_CORRECTION_LEVEL,
Database.SCANS_VERSION_NUMBER,
Database.SCANS_VERSION,
Database.SCANS_SEQUENCE_SIZE,
Database.SCANS_SEQUENCE_INDEX,
Database.SCANS_SEQUENCE_ID,
@@ -38,7 +39,7 @@ fun Cursor.exportJson(): String? {
val indices = columns.map {
Pair(getColumnIndex(it), it)
}
val contentIndex = getColumnIndex(Database.SCANS_CONTENT)
val contentIndex = getColumnIndex(Database.SCANS_TEXT)
val rawIndex = getColumnIndex(Database.SCANS_RAW)
val root = JSONArray()
do {
@@ -1,21 +1,23 @@
package de.markusfisch.android.binaryeye.database
import android.graphics.Bitmap
import android.os.Build
import android.os.Parcel
import android.os.Parcelable
import android.text.format.DateFormat
import de.markusfisch.android.zxingcpp.ZxingCpp
import de.markusfisch.android.zxingcpp.ZxingCpp.BarcodeFormat
import de.markusfisch.android.zxingcpp.ZxingCpp.BitMatrix
import de.markusfisch.android.zxingcpp.ZxingCpp.ContentType
import de.markusfisch.android.zxingcpp.ZxingCpp.Format
import de.markusfisch.android.zxingcpp.ZxingCpp.Result
import java.util.Locale
data class Scan(
val content: String,
val text: String,
val raw: ByteArray?,
val format: String,
val format: BarcodeFormat,
val errorCorrectionLevel: String? = null,
val versionNumber: Int = 0,
val version: String? = null,
val dataMask: Int = -1,
val symbol: BitMatrix? = null,
val sequenceSize: Int = -1,
val sequenceIndex: Int = -1,
val sequenceId: String = "",
@@ -24,7 +26,8 @@ data class Scan(
val price: String? = null,
val issueNumber: String? = null,
val dateTime: String = getDateTime(),
var id: Long = 0L
var id: Long = 0L,
val label: String? = null
) : Parcelable {
// Needs to be overwritten manually, as ByteArray is an array and
// this isn't handled well by Kotlin.
@@ -40,12 +43,13 @@ data class Scan(
return id == other.id &&
dateTime == other.dateTime &&
content == other.content &&
text == other.text &&
((raw == null && other.raw == null) ||
(raw != null && other.raw != null && raw.contentEquals(other.raw))) &&
format == other.format &&
errorCorrectionLevel == other.errorCorrectionLevel &&
versionNumber == other.versionNumber &&
version == other.version &&
dataMask == other.dataMask &&
sequenceSize == other.sequenceSize &&
sequenceIndex == other.sequenceIndex &&
sequenceId == other.sequenceId &&
@@ -60,11 +64,12 @@ data class Scan(
override fun hashCode(): Int {
var result = id.hashCode()
result = 31 * result + dateTime.hashCode()
result = 31 * result + content.hashCode()
result = 31 * result + text.hashCode()
result = 31 * result + (raw?.contentHashCode() ?: 0)
result = 31 * result + format.hashCode()
result = 31 * result + (errorCorrectionLevel?.hashCode() ?: 0)
result = 31 * result + versionNumber
result = 31 * result + version.hashCode()
result = 31 * result + dataMask
result = 31 * result + sequenceSize
result = 31 * result + sequenceIndex
result = 31 * result + sequenceId.hashCode()
@@ -76,11 +81,13 @@ data class Scan(
}
private constructor(parcel: Parcel) : this(
content = parcel.readString() ?: "",
text = parcel.readString() ?: "",
raw = parcel.readSizedByteArray(),
format = parcel.readString() ?: "",
format = BarcodeFormat.valueOf(parcel.readString() ?: ""),
errorCorrectionLevel = parcel.readString(),
versionNumber = parcel.readInt(),
version = parcel.readString(),
dataMask = parcel.readInt(),
symbol = parcel.readBitMatrix(),
sequenceSize = parcel.readInt(),
sequenceIndex = parcel.readInt(),
sequenceId = parcel.readString() ?: "",
@@ -89,16 +96,19 @@ data class Scan(
price = parcel.readString(),
issueNumber = parcel.readString(),
dateTime = parcel.readString() ?: "",
id = parcel.readLong()
id = parcel.readLong(),
label = parcel.readString()
)
override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.apply {
writeString(content)
writeString(text)
writeSizedByteArray(raw)
writeString(format)
writeString(format.name)
writeString(errorCorrectionLevel)
writeInt(versionNumber)
writeString(version)
writeInt(dataMask)
writeBitMatrix(symbol)
writeInt(sequenceSize)
writeInt(sequenceIndex)
writeString(sequenceId)
@@ -108,6 +118,7 @@ data class Scan(
writeString(issueNumber)
writeString(dateTime)
writeLong(id)
writeString(label)
}
}
@@ -123,26 +134,20 @@ data class Scan(
}
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,
when (contentType) {
ContentType.GS1,
ContentType.ISO15434,
ContentType.TEXT -> text
else -> "" // Show as binary.
},
rawBytes,
format,
ecLevel,
versionNumber,
version,
dataMask,
symbol,
sequenceSize,
sequenceIndex,
sequenceId,
@@ -153,41 +158,6 @@ fun Result.toScan(): Scan {
)
}
data class Recreation(
val format: Format,
val ecLevel: Int,
val size: Int,
val margin: Int
) {
fun encode(content: String): Bitmap? = try {
ZxingCpp.encodeAsBitmap(
content, format, size, size, margin, ecLevel
)
} catch (_: RuntimeException) {
null
}
}
fun Scan.toRecreation(
size: Int = 128,
margin: Int = -1
) = if (content.isEmpty()) {
null
} else {
Recreation(
Format.valueOf(format),
when (errorCorrectionLevel) {
"L" -> 0
"M" -> 1
"Q" -> 2
"H" -> 3
else -> -1
},
size,
margin
)
}
private fun getDateTime(
time: Long = System.currentTimeMillis()
) = DateFormat.format(
@@ -199,7 +169,11 @@ private fun getDateTime(
"yyyy-MM-dd HH:mm:ss"
},
time
).toString()
).toString() + String.format(
Locale.getDefault(),
":%03d",
time % 1000
)
private fun Parcel.writeSizedByteArray(array: ByteArray?) {
val size = array?.size ?: 0
@@ -219,3 +193,21 @@ private fun Parcel.readSizedByteArray(): ByteArray? {
null
}
}
private fun Parcel.writeBitMatrix(bm: BitMatrix?) {
writeInt(bm?.width ?: 0)
bm?.let {
writeInt(bm.height)
writeSizedByteArray(bm.data)
}
}
private fun Parcel.readBitMatrix(): BitMatrix? {
val width = readInt()
if (width < 1) {
return null
}
val height = readInt()
val data = readSizedByteArray() ?: return null
return BitMatrix(width, height, data)
}
@@ -4,44 +4,70 @@ import android.annotation.SuppressLint
import android.app.AlertDialog
import android.content.Context
import android.graphics.Bitmap
import android.graphics.Matrix
import android.os.Build
import android.os.Bundle
import android.support.v4.app.Fragment
import android.view.*
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.widget.EditText
import android.widget.ImageView
import android.widget.SeekBar
import android.widget.TextView
import de.markusfisch.android.binaryeye.R
import de.markusfisch.android.binaryeye.app.db
import de.markusfisch.android.binaryeye.app.hasWritePermission
import de.markusfisch.android.binaryeye.app.prefs
import de.markusfisch.android.binaryeye.content.Barcode
import de.markusfisch.android.binaryeye.content.BarcodeColors
import de.markusfisch.android.binaryeye.content.BitMatrixBarcode
import de.markusfisch.android.binaryeye.content.ContentBarcode
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.database.toScan
import de.markusfisch.android.binaryeye.io.addSuffixIfNotGiven
import de.markusfisch.android.binaryeye.io.toSaveResult
import de.markusfisch.android.binaryeye.io.writeExternalFile
import de.markusfisch.android.binaryeye.os.getScreenBrightness
import de.markusfisch.android.binaryeye.os.setScreenBrightness
import de.markusfisch.android.binaryeye.view.doOnApplyWindowInsets
import de.markusfisch.android.binaryeye.view.setPaddingFromWindowInsets
import de.markusfisch.android.binaryeye.widget.ConfinedScalingImageView
import de.markusfisch.android.binaryeye.widget.toast
import de.markusfisch.android.zxingcpp.ZxingCpp
import de.markusfisch.android.zxingcpp.ZxingCpp.Format
import kotlin.math.min
import kotlinx.coroutines.*
import de.markusfisch.android.zxingcpp.ZxingCpp.BarcodeFormat
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import java.io.OutputStream
import java.util.*
import java.util.Locale
import kotlin.math.max
import kotlin.math.min
class BarcodeFragment : Fragment() {
private enum class FileType {
PNG, SVG, TXT
PNG, JPG, SVG, TXT
}
private val parentJob = Job()
private val scope = CoroutineScope(Dispatchers.IO + parentJob)
private lateinit var barcode: Barcode
private lateinit var imageView: ConfinedScalingImageView
private lateinit var barcode: Barcode<*>
private lateinit var addToHistoryItem: MenuItem
private lateinit var brightenScreenItem: MenuItem
private var currentBrightness = -1f
override fun onCreate(state: Bundle?) {
super.onCreate(state)
@@ -62,6 +88,9 @@ class BarcodeFragment : Fragment() {
false
)
// Make `imageView` available for `onPause()`.
imageView = view.findViewById(R.id.barcode)
val bitmap: Bitmap
try {
barcode = arguments?.toBarcode() ?: throw IllegalArgumentException(
@@ -71,7 +100,7 @@ class BarcodeFragment : Fragment() {
bitmap = barcode.bitmap()
} catch (e: Exception) {
var message = e.message
if (message == null || message.isEmpty()) {
if (message.isNullOrEmpty()) {
message = getString(R.string.error_encoding_barcode)
}
message?.let {
@@ -81,15 +110,20 @@ class BarcodeFragment : Fragment() {
return null
}
val imageView = view.findViewById<ConfinedScalingImageView>(
R.id.barcode
)
imageView.setImageBitmap(bitmap)
imageView.post {
// Make sure to invoke this after ScalingImageView.onLayout().
imageView.runAfterLayout = {
val scale = prefs.previewScale
if (scale > 0) {
imageView.setScale(
scale,
bitmap.width / 2f,
bitmap.height / 2f
)
}
imageView.minWidth = min(
imageView.minWidth / 2f,
barcode.size.toFloat()
max(bitmap.width, bitmap.height).toFloat()
)
}
@@ -101,27 +135,67 @@ class BarcodeFragment : Fragment() {
}
}
(view.findViewById(R.id.inset_layout) as View).setPaddingFromWindowInsets()
imageView.doOnApplyWindowInsets { v, insets ->
(v as ConfinedScalingImageView).insets.set(insets)
view.findViewById<View>(R.id.inset_layout).setPaddingFromWindowInsets()
imageView.doOnApplyWindowInsets { _, insets ->
imageView.insets.set(insets)
}
return view
}
private fun Bundle.toBarcode() = Barcode(
getString(CONTENT) ?: throw IllegalArgumentException(
"content cannot be null"
),
Format.valueOf(
private fun Bundle.toBarcode(): Barcode<*> {
val content =
getString(CONTENT_TEXT) ?: getByteArray(CONTENT_RAW) ?: throw IllegalArgumentException(
"content cannot be null"
)
val barcodeFormat = BarcodeFormat.valueOf(
getString(FORMAT) ?: throw IllegalArgumentException(
"format cannot be null"
)
),
getInt(SIZE),
getInt(EC_LEVEL),
Colors.values()[getInt(COLORS)]
)
)
val colors = BarcodeColors.entries[getInt(COLORS)]
val data = getByteArray(BIT_MATRIX_DATA)
return if (data != null) {
BitMatrixBarcode(
ZxingCpp.BitMatrix(
getInt(BIT_MATRIX_WIDTH),
getInt(BIT_MATRIX_HEIGHT),
data
),
content,
barcodeFormat,
colors
)
} else {
ContentBarcode(
content,
barcodeFormat,
getInt(EC_LEVEL),
getInt(MARGIN),
colors
)
}
}
override fun onResume() {
super.onResume()
if (prefs.brightenScreen) {
// Post to make sure brightenScreenItem is initialized.
view?.post {
brightenScreen()
}
}
}
override fun onPause() {
super.onPause()
if (currentBrightness > -1f) {
restoreScreenBrightness()
}
if (imageView.isInBounds()) {
prefs.previewScale = imageView.getScale()
}
}
override fun onDestroyView() {
super.onDestroyView()
@@ -130,10 +204,22 @@ class BarcodeFragment : Fragment() {
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.fragment_barcode, menu)
addToHistoryItem = menu.findItem(R.id.add_to_history)
brightenScreenItem = menu.findItem(R.id.brighten_screen)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.add_to_history -> {
readAndAddToHistory(
barcode.bitmap(),
barcode.format
)
addToHistoryItem.isVisible = false
context.toast(R.string.added_to_history)
true
}
R.id.copy_to_clipboard -> {
context.apply {
copyToClipboard(barcode.text())
@@ -141,12 +227,32 @@ class BarcodeFragment : Fragment() {
}
true
}
R.id.export_to_file -> {
context.pickFileType(R.string.export_as) {
askForFileNameAndSave(it)
context.pickFileType(R.string.export_as) { fileType ->
when (fileType) {
FileType.PNG,
FileType.JPG -> askForSize { size ->
askForFileNameAndSave(fileType, size)
}
else -> askForFileNameAndSave(fileType, 0)
}
}
true
}
R.id.brighten_screen -> {
prefs.brightenScreen = if (currentBrightness > -1f) {
restoreScreenBrightness()
false
} else {
brightenScreen()
true
}
true
}
else -> super.onOptionsItemSelected(item)
}
}
@@ -155,7 +261,7 @@ class BarcodeFragment : Fragment() {
title: Int,
action: (FileType) -> Unit
) {
val fileTypes = FileType.values()
val fileTypes = FileType.entries.toTypedArray()
AlertDialog.Builder(this)
.setTitle(title)
.setItems(fileTypes.map { it.name }.toTypedArray()) { _, which ->
@@ -166,11 +272,13 @@ class BarcodeFragment : Fragment() {
// Dialogs do not have a parent view.
@SuppressLint("InflateParams")
private fun askForFileNameAndSave(fileType: FileType) {
private fun askForFileNameAndSave(fileType: FileType, bitmapSize: Int) {
val ac = activity ?: return
// Write permission is only required before Android Q.
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q &&
!ac.hasWritePermission { askForFileNameAndSave(fileType) }
!ac.hasWritePermission {
askForFileNameAndSave(fileType, bitmapSize)
}
) {
return
}
@@ -188,19 +296,28 @@ class BarcodeFragment : Fragment() {
addSuffixIfNotGiven(fileName, ".png"),
MIME_PNG
) {
barcode.bitmap().saveAsPng(it)
barcode.bitmap(bitmapSize).saveAsPng(it)
}
FileType.JPG -> saveAs(
addSuffixIfNotGiven(fileName, ".jpg"),
MIME_JPG
) {
barcode.bitmap(bitmapSize).saveAsJpg(it)
}
FileType.SVG -> saveAs(
addSuffixIfNotGiven(fileName, ".svg"),
MIME_SVG
) { outputStream ->
outputStream.write(barcode.svg().toByteArray())
) {
it.write(barcode.svg().toByteArray())
}
FileType.TXT -> saveAs(
addSuffixIfNotGiven(fileName, ".txt"),
MIME_TXT
) { outputStream ->
outputStream.write(barcode.text().toByteArray())
) {
it.write(barcode.text().toByteArray())
}
}
}
@@ -209,6 +326,52 @@ class BarcodeFragment : Fragment() {
.show()
}
// Dialogs do not have a parent view.
@SuppressLint("InflateParams")
private fun askForSize(write: (size: Int) -> Unit) {
val ac = activity ?: return
val view = ac.layoutInflater.inflate(R.layout.dialog_size, null)
val sizeView = view.findViewById<TextView>(R.id.size_display)
val sizeBarView = view.findViewById<SeekBar>(R.id.size_bar)
sizeBarView.initSizeBar(sizeView)
AlertDialog.Builder(ac)
.setView(view)
.setPositiveButton(android.R.string.ok) { _, _ ->
write(getSize(sizeBarView.progress))
}
.setNegativeButton(android.R.string.cancel) { _, _ ->
}
.show()
}
private fun SeekBar.initSizeBar(sizeView: TextView) {
sizeView.updateSize(progress)
setOnSeekBarChangeListener(
object : SeekBar.OnSeekBarChangeListener {
override fun onProgressChanged(
seekBar: SeekBar,
progressValue: Int,
fromUser: Boolean
) {
sizeView.updateSize(progressValue)
}
override fun onStartTrackingTouch(seekBar: SeekBar) {}
override fun onStopTrackingTouch(seekBar: SeekBar) {}
}
)
}
private fun TextView.updateSize(power: Int) {
val size = getSize(power)
text = if (size > 0) {
getString(R.string.size_width_by_height, size, size)
} else {
getString(R.string.size_no_magnification)
}
}
private fun saveAs(
fileName: String,
mimeType: String,
@@ -225,21 +388,34 @@ class BarcodeFragment : Fragment() {
private fun Context.shareAs(fileType: FileType) {
when (fileType) {
FileType.PNG -> share(barcode.bitmap())
FileType.PNG -> askForSize {
share(barcode.bitmap(it), MIME_PNG, "png")
}
FileType.JPG -> askForSize {
share(barcode.bitmap(it), MIME_JPG, "jpg")
}
FileType.SVG -> shareText(barcode.svg(), MIME_SVG)
FileType.TXT -> shareText(barcode.text())
}
}
private fun Context.share(bitmap: Bitmap) {
private fun Context.share(bitmap: Bitmap, mimeType: String, ext: String) {
scope.launch(Dispatchers.IO) {
val file = File(
externalCacheDir,
"shared_barcode.png"
"shared_barcode.$ext"
)
val success = try {
FileOutputStream(file).use {
bitmap.saveAsPng(it)
when (mimeType) {
MIME_PNG -> bitmap.saveAsPng(it)
MIME_JPG -> bitmap.saveAsJpg(it)
else -> throw IllegalArgumentException(
"Invalid mime type"
)
}
}
true
} catch (e: IOException) {
@@ -247,7 +423,7 @@ class BarcodeFragment : Fragment() {
}
withContext(Dispatchers.Main) {
if (success) {
shareFile(file, "image/png")
shareFile(file, mimeType)
} else {
toast(R.string.error_saving_file)
}
@@ -255,98 +431,166 @@ class BarcodeFragment : Fragment() {
}
}
private fun brightenScreen() {
activity?.let {
currentBrightness = it.getScreenBrightness()
it.setScreenBrightness(1f)
brightenScreenItem.isChecked = true
}
}
private fun restoreScreenBrightness() {
if (currentBrightness > -1f) {
activity?.setScreenBrightness(currentBrightness)
currentBrightness = -1f
brightenScreenItem.isChecked = false
}
}
companion object {
private const val CONTENT = "content"
private const val BIT_MATRIX_WIDTH = "bit_matrix_width"
private const val BIT_MATRIX_HEIGHT = "bit_matrix_height"
private const val BIT_MATRIX_DATA = "bit_matrix_data"
private const val CONTENT_TEXT = "content_text"
private const val CONTENT_RAW = "content_raw"
private const val FORMAT = "format"
private const val SIZE = "size"
private const val EC_LEVEL = "ec_level"
private const val MARGIN = "margin"
private const val COLORS = "colors"
private const val MIME_PNG = "image/png"
private const val MIME_JPG = "image/jpeg"
private const val MIME_SVG = "image/svg+xmg"
private const val MIME_TXT = "text/plain"
fun newInstance(
content: String,
format: Format,
size: Int,
ecLevel: Int = -1,
barcode: Barcode<*>,
colors: Int = 0
): Fragment {
val args = Bundle()
args.putString(CONTENT, content)
args.putString(FORMAT, format.name)
args.putInt(SIZE, size)
args.putInt(EC_LEVEL, ecLevel)
args.putInt(COLORS, colors)
when (barcode) {
is ContentBarcode -> args.putEcLevelMargin(
barcode.ecLevel,
barcode.margin
)
is BitMatrixBarcode -> {
args.putInt(BIT_MATRIX_WIDTH, barcode.bitMatrix.width)
args.putInt(BIT_MATRIX_HEIGHT, barcode.bitMatrix.height)
args.putByteArray(BIT_MATRIX_DATA, barcode.bitMatrix.data)
}
else -> throw IllegalArgumentException("unknown barcode")
}
args.putContentFormatColors(
barcode.content,
barcode.format,
colors
)
val fragment = BarcodeFragment()
fragment.arguments = args
return fragment
}
fun <T> newInstance(
content: T,
format: BarcodeFormat,
ecLevel: Int = -1,
margin: Int = -1,
colors: Int = 0
): Fragment {
val args = Bundle()
args.putContentFormatColors(content, format, colors)
args.putEcLevelMargin(ecLevel, margin)
val fragment = BarcodeFragment()
fragment.arguments = args
return fragment
}
private fun <T> Bundle.putContentFormatColors(
content: T,
format: BarcodeFormat,
colors: Int
) {
when (content) {
is String -> {
putString(CONTENT_TEXT, content)
}
is ByteArray -> {
putByteArray(CONTENT_RAW, content)
}
else -> {
throw IllegalArgumentException(
"content must be a String of a ByteArray"
)
}
}
putString(FORMAT, format.name)
putInt(COLORS, colors)
}
private fun Bundle.putEcLevelMargin(
ecLevel: Int,
margin: Int
) {
putInt(EC_LEVEL, ecLevel)
putInt(MARGIN, margin)
}
}
}
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 fun ImageView.setScale(scale: Float, x: Float, y: Float) {
imageMatrix = Matrix().apply {
setTranslate(-x, -y)
postScale(scale, scale)
postTranslate(x, y)
}
}
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 ImageView.getScale(): Float {
val values = FloatArray(9)
imageMatrix.getValues(values)
return values[Matrix.MSCALE_X]
}
private fun Bitmap.saveAsPng(outputStream: OutputStream, quality: Int = 90) =
compress(Bitmap.CompressFormat.PNG, quality, outputStream)
private fun Bitmap.saveAsJpg(outputStream: OutputStream, quality: Int = 90) =
compress(Bitmap.CompressFormat.JPEG, 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())
private fun encodeFileName(name: String): String = fileNameCharacters
.replace(name, "_")
.take(16)
.trim('_')
.lowercase(Locale.getDefault())
private fun getSize(step: Int) = when (step) {
0 -> 0
else -> 128 shl (step - 1)
}
private fun readAndAddToHistory(
bitmap: Bitmap,
format: BarcodeFormat
) {
ZxingCpp.readBitmap(
bitmap,
0, 0,
bitmap.width, bitmap.height,
0,
ZxingCpp.ReaderOptions(
tryHarder = true,
tryRotate = true,
tryInvert = true,
tryDownscale = true,
maxNumberOfSymbols = 1,
formats = setOf(format)
)
)?.first {
db.insertScan(it.toScan())
true
}
}
@@ -6,30 +6,55 @@ import android.support.design.widget.FloatingActionButton
import android.support.v4.app.Fragment
import android.text.Editable
import android.text.Html
import android.text.Spannable
import android.text.SpannableString
import android.text.TextWatcher
import android.text.method.LinkMovementMethod
import android.view.*
import android.widget.*
import android.text.style.TypefaceSpan
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.widget.EditText
import android.widget.ImageView
import android.widget.TableLayout
import android.widget.TableRow
import android.widget.TextView
import de.markusfisch.android.binaryeye.R
import de.markusfisch.android.binaryeye.actions.ActionRegistry
import de.markusfisch.android.binaryeye.actions.IAction
import de.markusfisch.android.binaryeye.actions.wifi.WifiAction
import de.markusfisch.android.binaryeye.actions.wifi.WifiConnector
import de.markusfisch.android.binaryeye.activity.MainActivity
import de.markusfisch.android.binaryeye.adapter.prettifyFormatName
import de.markusfisch.android.binaryeye.app.*
import de.markusfisch.android.binaryeye.app.addFragment
import de.markusfisch.android.binaryeye.app.db
import de.markusfisch.android.binaryeye.app.hasLocationPermission
import de.markusfisch.android.binaryeye.app.hasWritePermission
import de.markusfisch.android.binaryeye.app.prefs
import de.markusfisch.android.binaryeye.content.ContentBarcode
import de.markusfisch.android.binaryeye.content.copyToClipboard
import de.markusfisch.android.binaryeye.content.shareAsFile
import de.markusfisch.android.binaryeye.content.shareText
import de.markusfisch.android.binaryeye.content.toBarcode
import de.markusfisch.android.binaryeye.content.toErrorCorrectionInt
import de.markusfisch.android.binaryeye.content.toHexString
import de.markusfisch.android.binaryeye.database.Recreation
import de.markusfisch.android.binaryeye.content.wipeLastShareFile
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.hideSoftKeyboard
import de.markusfisch.android.binaryeye.view.setPaddingFromWindowInsets
import de.markusfisch.android.binaryeye.widget.toast
import kotlinx.coroutines.*
import de.markusfisch.android.zxingcpp.ZxingCpp
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.security.MessageDigest
import kotlin.math.roundToInt
class DecodeFragment : Fragment() {
@@ -40,7 +65,9 @@ class DecodeFragment : Fragment() {
private lateinit var hexView: TextView
private lateinit var stampView: TextView
private lateinit var recreationView: ImageView
private lateinit var labelView: EditText
private lateinit var fab: FloatingActionButton
private lateinit var scan: Scan
private lateinit var format: String
private val parentJob = Job()
@@ -54,8 +81,8 @@ class DecodeFragment : Fragment() {
private var action = ActionRegistry.DEFAULT_ACTION
private var isBinary = false
private var originalBytes: ByteArray = ByteArray(0)
private var id = 0L
private var recreation: Recreation? = null
private var label: String? = null
private var recreationSize = 0
override fun onCreate(state: Bundle?) {
super.onCreate(state)
@@ -75,21 +102,20 @@ class DecodeFragment : Fragment() {
false
)
closeAutomatically = prefs.closeAutomatically &&
activity?.intent?.hasExtra(MainActivity.DECODED) == true
val justScanned = activity?.intent?.hasExtra(MainActivity.DECODED) == true
closeAutomatically = prefs.closeAutomatically && justScanned
val scan = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
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
isBinary = scan.text.isEmpty()
originalBytes = scan.raw ?: scan.text.toByteArray()
format = scan.format.name
recreationSize = (200f * dp).roundToInt()
contentView = view.findViewById(R.id.content)
formatView = view.findViewById(R.id.format)
@@ -98,35 +124,52 @@ class DecodeFragment : Fragment() {
hexView = view.findViewById(R.id.hex)
stampView = view.findViewById(R.id.stamp)
recreationView = view.findViewById(R.id.recreation)
labelView = view.findViewById(R.id.label)
fab = view.findViewById(R.id.open)
initContentAndFab(originalContent)
initContentAndFab(justScanned)
if (prefs.showMetaData) {
metaView.fillMetaView(scan)
} else {
metaView.visibility = View.GONE
}
if (prefs.showRecreation) {
recreation = scan.toRecreation(
(200f * dp).roundToInt()
)
if (id > 0) {
scan.label?.let {
labelView.setText(it)
label = it
}
} else {
labelView.visibility = View.GONE
}
updateViewsAndFab(originalContent, originalBytes)
updateViewsAndFab(scan.text, originalBytes)
(view.findViewById(R.id.inset_layout) as View).setPaddingFromWindowInsets()
(view.findViewById(R.id.scroll_view) as View).setPaddingFromWindowInsets()
view.findViewById<View>(R.id.inset_layout).setPaddingFromWindowInsets()
view.findViewById<View>(R.id.scroll_view).setPaddingFromWindowInsets()
return view
}
override fun onPause() {
super.onPause()
val id = scan.id
if (id > 0) {
val newLabel = labelView.text.toString().trim()
if (newLabel != (label ?: "")) {
db.renameScan(id, newLabel)
label = newLabel
}
}
}
override fun onDestroy() {
super.onDestroy()
parentJob.cancel()
wipeLastShareFile()
}
private fun initContentAndFab(originalContent: String) {
private fun initContentAndFab(justScanned: Boolean) {
if (isBinary) {
contentView.setText(String(originalBytes).foldNonAlNum())
contentView.isEnabled = false
@@ -135,7 +178,7 @@ class DecodeFragment : Fragment() {
askForFileNameAndSave(originalBytes)
}
} else {
contentView.setText(originalContent)
contentView.setText(scan.text)
contentView.addTextChangedListener(object : TextWatcher {
override fun afterTextChanged(s: Editable?) {
updateViewsAndFab(s?.toString() ?: "")
@@ -156,10 +199,11 @@ class DecodeFragment : Fragment() {
) = Unit
})
fab.setOnClickListener {
executeAction(content)
executeAction(this.content)
}
if (prefs.openImmediately) {
executeAction(content)
if (justScanned && prefs.openImmediately) {
executeAction(this.content)
backOrFinish()
}
}
}
@@ -176,12 +220,9 @@ class DecodeFragment : Fragment() {
action = ActionRegistry.getAction(bytes)
}
if (prevAction !== action) {
updateFab(action)
fab.setImageResource(action.iconResId)
}
}
private fun updateFab(action: IAction) {
fab.setImageResource(action.iconResId)
// Run this for the initial action too.
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
fab.setOnLongClickListener { v ->
v.context.toast(action.titleResId)
@@ -203,25 +244,65 @@ class DecodeFragment : Fragment() {
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
)
val unmodified: Boolean
val content = if (isBinary) {
if (bytes.isEmpty()) {
clearRecreation()
return@showIf
}
unmodified = bytes.contentEquals(originalBytes)
bytes
} else {
if (text.isEmpty()) {
clearRecreation()
return@showIf
}
unmodified = text == scan.text
text
}
val barcode = if (unmodified) {
scan.toBarcode()
} else {
ContentBarcode(
content,
scan.format,
scan.errorCorrectionLevel.toErrorCorrectionInt()
)
}
try {
v.setImageBitmap(barcode.bitmap(recreationSize))
v.setOnClickListener {
v.context.hideSoftKeyboard(contentView)
v.post {
fragmentManager?.addFragment(
BarcodeFragment.newInstance(barcode)
)
}
}
} catch (e: RuntimeException) {
clearRecreation()
}
}
dataView.fillDataView(text)
dataView.fillDataView(text, bytes)
stampView.setTrackingLink(bytes, format)
}
private fun TableLayout.fillDataView(text: String) {
val items = LinkedHashMap<Int, String?>()
private fun clearRecreation() {
recreationView.apply {
setImageBitmap(null)
setOnClickListener(null)
}
}
private fun TableLayout.fillDataView(text: String, bytes: ByteArray) {
val items = LinkedHashMap<Int, CharSequence?>()
when (prefs.showChecksum) {
"CRC4" -> items[R.string.crc4] = String.format("%X", crc4(bytes))
"MD5" -> items[R.string.md5] = bytes.md5().toHexString().fold()
"SHA1" -> items[R.string.sha1] = bytes.sha1().toHexString().fold()
"SHA256" -> items[R.string.sha256] = bytes.sha256().toHexString().fold()
else -> Unit
}
if (action is WifiAction) {
WifiConnector.parseMap(text)?.let { wifiData ->
items.putAll(
@@ -243,7 +324,7 @@ class DecodeFragment : Fragment() {
}
private fun TableLayout.fillMetaView(scan: Scan) {
val items = linkedMapOf(
val items = linkedMapOf<Int, CharSequence?>(
R.string.error_correction_level to scan.errorCorrectionLevel,
R.string.sequence_size to scan.sequenceSize.positiveToString(),
R.string.sequence_index to scan.sequenceIndex.positiveToString(),
@@ -253,15 +334,15 @@ class DecodeFragment : Fragment() {
R.string.gtin_price to scan.price,
R.string.gtin_issue_number to scan.issueNumber,
)
if (scan.versionNumber > 0) {
val versionString = if (scan.format == "QR_CODE") {
if (!scan.version.isNullOrBlank()) {
val versionString = if (scan.format.name == "QR_CODE") {
getString(
R.string.qr_version_and_modules,
scan.versionNumber,
scan.versionNumber * 4 + 17
scan.version,
(scan.version.toIntOrNull() ?: 0) * 4 + 17
)
} else {
scan.versionNumber.toString()
scan.version
}
items.putAll(
linkedMapOf(
@@ -269,11 +350,20 @@ class DecodeFragment : Fragment() {
)
)
}
if (scan.format == ZxingCpp.BarcodeFormat.QR_CODE &&
scan.dataMask > -1
) {
items.putAll(
linkedMapOf(
R.string.qr_data_mask to scan.dataMask.toString()
)
)
}
fill(items)
}
private fun TableLayout.fill(
items: LinkedHashMap<Int, String?>
items: LinkedHashMap<Int, CharSequence?>
) {
removeAllViews()
visibility = if (items.isEmpty()) View.GONE else {
@@ -299,10 +389,6 @@ class DecodeFragment : Fragment() {
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.fragment_decode, menu)
if (isBinary) {
menu.findItem(R.id.copy_to_clipboard).isVisible = false
menu.findItem(R.id.create).isVisible = false
}
if (id > 0L) {
menu.findItem(R.id.remove).isVisible = true
}
@@ -318,11 +404,13 @@ class DecodeFragment : Fragment() {
maybeBackOrFinish()
true
}
R.id.copy_to_clipboard -> {
copyToClipboard(textOrHex())
maybeBackOrFinish()
true
}
R.id.share -> {
context?.apply {
shareText(textOrHex())
@@ -330,17 +418,36 @@ class DecodeFragment : Fragment() {
}
true
}
R.id.share_file -> {
context?.apply {
val (data, name) = if (isBinary) {
Pair(originalBytes, "barcode_content.bin")
} else {
Pair(content, "barcode_content.txt")
}
shareAsFile(data, name)
maybeBackOrFinish()
}
true
}
R.id.create -> {
fragmentManager?.addFragment(
EncodeFragment.newInstance(content, format)
EncodeFragment.newInstance(
if (isBinary) originalBytes else content,
format
)
)
true
}
R.id.remove -> {
db.removeScan(id)
db.removeScan(scan.id)
backOrFinish()
true
}
else -> super.onOptionsItemSelected(item)
}
}
@@ -387,17 +494,18 @@ class DecodeFragment : Fragment() {
private fun executeAction(content: String) {
val ac = activity ?: return
if (content.isNotEmpty()) {
if (action is WifiAction &&
Build.VERSION.SDK_INT < Build.VERSION_CODES.Q &&
!ac.hasLocationPermission { executeAction(content) }
) {
return
}
scope.launch {
action.execute(ac, content.toByteArray())
maybeBackOrFinish()
}
if (content.isEmpty()) {
return
}
if (action is WifiAction &&
Build.VERSION.SDK_INT < Build.VERSION_CODES.Q &&
!ac.hasLocationPermission { executeAction(content) }
) {
return
}
scope.launch {
action.execute(ac, content.toByteArray())
maybeBackOrFinish()
}
}
@@ -451,7 +559,7 @@ private inline fun <T : View> T.showIf(
}
private val nonAlNum = "[^a-zA-Z0-9]".toRegex()
private val multipleDots = "[…]+".toRegex()
private val multipleDots = "+".toRegex()
private fun String.foldNonAlNum() = replace(nonAlNum, "")
.replace(multipleDots, "")
@@ -547,7 +655,7 @@ private fun generateDpTrackingLink(bytes: ByteArray, format: String): String? {
"%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>"
return "<a href=\"https://www.deutschepost.de/de/s/sendungsverfolgung.html?piececode=$trackingNumber\">Deutsche Post: $trackingNumber</a>"
}
// CRC-4 with polynomial x^4 + x + 1.
@@ -582,3 +690,35 @@ private fun String.fromHtml() = if (
@Suppress("DEPRECATION")
Html.fromHtml(this)
}
private fun ByteArray.md5(): ByteArray = MessageDigest.getInstance("MD5").run {
update(this@md5)
digest()
}
private fun ByteArray.sha1(): ByteArray = MessageDigest.getInstance("SHA-1").run {
update(this@sha1)
digest()
}
private fun ByteArray.sha256(): ByteArray = MessageDigest.getInstance("SHA-256").run {
update(this@sha256)
digest()
}
private fun String.fold(): CharSequence {
val l = length
return if (l > 10) {
val half = l / 2
val s = substring(0, half) + "\n" + substring(half)
SpannableString(s).apply {
setSpan(
TypefaceSpan("monospace"),
0, s.length,
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
)
}
} else {
this
}
}
@@ -1,20 +1,35 @@
package de.markusfisch.android.binaryeye.fragment
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.support.v4.app.Fragment
import android.support.v7.widget.SwitchCompat
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.widget.*
import android.widget.AdapterView
import android.widget.ArrayAdapter
import android.widget.CheckBox
import android.widget.EditText
import android.widget.Spinner
import android.widget.TextView
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.app.setFragment
import de.markusfisch.android.binaryeye.text.unescape
import de.markusfisch.android.binaryeye.view.hideSoftKeyboard
import de.markusfisch.android.binaryeye.view.setPaddingFromWindowInsets
import de.markusfisch.android.binaryeye.widget.toast
import de.markusfisch.android.zxingcpp.ZxingCpp.Format
import de.markusfisch.android.zxingcpp.ZxingCpp.BarcodeFormat
import java.io.InputStream
import kotlin.math.min
class EncodeFragment : Fragment() {
private lateinit var formatView: Spinner
@@ -22,24 +37,54 @@ class EncodeFragment : Fragment() {
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 addQuietZoneSwitch: SwitchCompat
private lateinit var contentView: EditText
private lateinit var unescapeCheckBox: CheckBox
private val writers = 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
private val formats = 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 var bytes: ByteArray? = null
override fun onActivityResult(
requestCode: Int,
resultCode: Int,
resultData: Intent?
) {
when (requestCode) {
PICK_FILE_RESULT_CODE -> {
if (resultCode == Activity.RESULT_OK &&
resultData != null &&
resultData.data != null
) {
val ac = activity ?: return
ac.hideSoftKeyboard(contentView)
val uri = resultData.data ?: return
bytes = ac.contentResolver?.openInputStream(uri)?.use {
it.readBytesMax(4096)
} ?: return
setEncodeByteArray()
}
}
}
}
override fun onCreate(state: Bundle?) {
super.onCreate(state)
setHasOptionsMenu(true)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
@@ -58,7 +103,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
@@ -71,16 +116,36 @@ class EncodeFragment : Fragment() {
position: Int,
id: Long
) {
val ecVisibility = if (
writers[position] == Format.QR_CODE
) View.VISIBLE else View.GONE
ecLabel.visibility = ecVisibility
ecSpinner.visibility = ecVisibility
val colorsVisibility = if (
writers[position].canBeInverted()
) View.VISIBLE else View.GONE
colorsLabel.visibility = colorsVisibility
colorsSpinner.visibility = colorsVisibility
val format = formats[position]
val arrayId = when (format) {
BarcodeFormat.AZTEC -> R.array.aztec_error_correction_levels
BarcodeFormat.QR_CODE -> R.array.qr_error_correction_levels
BarcodeFormat.PDF_417 -> R.array.pdf417_error_correction_levels
else -> 0
}
if (arrayId > 0) {
ecSpinner.setEntries(arrayId)
val idx = format.unpackEcLevel(
prefs.indexOfLastSelectedEcLevel
)
if (idx < ecSpinner.adapter.count) {
ecSpinner.setSelection(idx)
}
true
} else {
false
}.setVisibilityFor(ecLabel, ecSpinner)
format.canBeInverted().setVisibilityFor(
colorsLabel, colorsSpinner
)
when (format) {
BarcodeFormat.AZTEC,
BarcodeFormat.DATA_MATRIX,
BarcodeFormat.QR_CODE,
BarcodeFormat.PDF_417 -> true
else -> false
}.setVisibilityFor(addQuietZoneSwitch)
}
override fun onNothingSelected(parentView: AdapterView<*>?) {}
@@ -88,25 +153,48 @@ class EncodeFragment : Fragment() {
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()
addQuietZoneSwitch = view.findViewById(R.id.add_quiet_zone)
addQuietZoneSwitch.isChecked = prefs.addQuietZone
contentView = view.findViewById(R.id.content)
unescapeCheckBox = view.findViewById(R.id.unescape)
unescapeCheckBox.isChecked = prefs.expandEscapeSequences
var complete = false
val args = arguments
args?.getString(CONTENT)?.let {
args?.getString(CONTENT_TEXT)?.let {
contentView.setText(it)
complete = it.isNotEmpty()
}
args?.getByteArray(CONTENT_RAW)?.let {
bytes = it
complete = it.isNotEmpty()
setEncodeByteArray()
}
val barcodeFormat = args?.getString(FORMAT)
if (barcodeFormat != null) {
formatView.setSelection(
writers.indexOf(barcodeFormat.toFormat())
formats.indexOf(barcodeFormat.toFormat())
)
} else if (state == null) {
formatView.post {
@@ -118,73 +206,139 @@ class EncodeFragment : Fragment() {
it.context.encode()
}
(view.findViewById(R.id.inset_layout) as View).setPaddingFromWindowInsets()
(view.findViewById(R.id.scroll_view) as View).setPaddingFromWindowInsets()
view.findViewById<View>(R.id.inset_layout).setPaddingFromWindowInsets()
view.findViewById<View>(R.id.scroll_view).setPaddingFromWindowInsets()
if (complete &&
args?.getBoolean(EXECUTE, false) == true
) {
view.post {
val content = view.context.getContent() ?: return@post
fragmentManager?.setFragment(newEncodeFragment(content))
}
}
return view
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.fragment_encode, menu)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.pick_file -> {
startActivityForResult(
Intent.createChooser(
Intent(Intent.ACTION_GET_CONTENT).apply {
type = "*/*"
},
getString(R.string.pick_file)
),
PICK_FILE_RESULT_CODE
)
true
}
else -> super.onOptionsItemSelected(item)
}
}
override fun onPause() {
super.onPause()
prefs.indexOfLastSelectedFormat = formatView.selectedItemPosition
prefs.expandEscapeSequences = unescapeCheckBox.isChecked
prefs.addQuietZone = addQuietZoneSwitch.isChecked
}
private fun Context.encode() {
val content = contentView.text.toString()
if (content.isEmpty()) {
toast(R.string.error_no_content)
return
}
hideSoftKeyboard(contentView)
val writer = writers[formatView.selectedItemPosition]
fragmentManager?.addFragment(
BarcodeFragment.newInstance(
content,
writer,
getSize(sizeBarView.progress),
ecSpinner.selectedItemPosition,
if (writer.canBeInverted()) {
colorsSpinner.selectedItemPosition
} else 0
)
val content = getContent() ?: return
contentView.post {
fragmentManager?.addFragment(newEncodeFragment(content))
}
}
private fun newEncodeFragment(content: Any): Fragment {
val format = formats[formatView.selectedItemPosition]
return BarcodeFragment.newInstance(
content,
format,
format.getErrorCorrectionLevel(ecSpinner.selectedItemPosition),
when (format) {
BarcodeFormat.AZTEC,
BarcodeFormat.DATA_MATRIX,
BarcodeFormat.QR_CODE,
BarcodeFormat.PDF_417 -> if (
addQuietZoneSwitch.isChecked
) 1 else 0
else -> -1
},
if (format.canBeInverted()) {
colorsSpinner.selectedItemPosition
} else 0
)
}
private fun initSizeBar() {
updateSize(sizeBarView.progress)
sizeBarView.setOnSeekBarChangeListener(
object : SeekBar.OnSeekBarChangeListener {
override fun onProgressChanged(
seekBar: SeekBar,
progressValue: Int,
fromUser: Boolean
) {
updateSize(progressValue)
}
override fun onStartTrackingTouch(seekBar: SeekBar) {}
override fun onStopTrackingTouch(seekBar: SeekBar) {}
})
private fun Context.getContent(): Any? {
if (bytes != null) {
return bytes
}
var text = contentView.text.toString()
if (text.isEmpty()) {
toast(R.string.error_no_content)
return null
}
if (!unescapeCheckBox.isChecked) {
return text
}
try {
text = text.unescape()
} catch (e: IllegalArgumentException) {
toast(e.message ?: "Invalid escape sequence")
return null
}
// Return a ByteArray if there were escape sequences for
// non-printable characters.
return if (text.any { it.code < 32 && it.code !in setOf(9, 10, 13) }) {
// Do not expand UTF-8 sequences in binary data.
text.toByteArray(Charsets.ISO_8859_1)
} else {
text
}
}
private fun updateSize(power: Int) {
val size = getSize(power)
sizeView.text = getString(R.string.width_by_height, size, size)
private fun setEncodeByteArray() {
contentView.text = null
contentView.hint = getString(R.string.binary_data)
contentView.isEnabled = false
unescapeCheckBox.isEnabled = false
}
private fun getSize(power: Int) = 128 * (power + 1)
companion object {
private const val CONTENT = "content"
private const val CONTENT_TEXT = "content_text"
private const val CONTENT_RAW = "content_raw"
private const val FORMAT = "format"
private const val EXECUTE = "execute"
private const val PICK_FILE_RESULT_CODE = 1
fun newInstance(
content: String? = null,
format: String? = null
fun <T> newInstance(
content: T? = null,
format: String? = null,
execute: Boolean = false
): Fragment {
val args = Bundle()
content?.let { args.putString(CONTENT, content) }
content?.let {
when (content) {
is String -> args.putString(CONTENT_TEXT, content)
is ByteArray -> args.putByteArray(CONTENT_RAW, content)
else -> throw IllegalArgumentException(
"content must be a String or a ByteArray"
)
}
args.putBoolean(EXECUTE, execute)
}
format?.let { args.putString(FORMAT, it) }
val fragment = EncodeFragment()
fragment.arguments = args
@@ -193,15 +347,71 @@ class EncodeFragment : Fragment() {
}
}
private fun Format.canBeInverted() = when (this) {
Format.AZTEC,
Format.DATA_MATRIX,
Format.QR_CODE -> true
private fun Boolean.setVisibilityFor(vararg views: View) {
val visibility = if (this) View.VISIBLE else View.GONE
for (view in views) {
view.visibility = visibility
}
}
private fun BarcodeFormat.packEcLevel(packed: Int, level: Int): Int {
val s = ecLevelShift()
return (level shl s) or (packed and (15 shl s).inv())
}
private fun BarcodeFormat.unpackEcLevel(packed: Int) =
(packed shr ecLevelShift()) and 15
private fun BarcodeFormat.ecLevelShift() = when (this) {
BarcodeFormat.AZTEC -> 0
BarcodeFormat.QR_CODE -> 4
BarcodeFormat.PDF_417 -> 8
else -> throw IllegalArgumentException("$this does not have error levels")
}
private fun BarcodeFormat.canBeInverted() = when (this) {
BarcodeFormat.AZTEC,
BarcodeFormat.DATA_MATRIX,
BarcodeFormat.QR_CODE -> true
else -> false
}
private fun String.toFormat(default: Format = Format.QR_CODE): Format = try {
Format.valueOf(this)
private fun String.toFormat(default: BarcodeFormat = BarcodeFormat.QR_CODE): BarcodeFormat = try {
BarcodeFormat.valueOf(this)
} catch (_: IllegalArgumentException) {
default
}
private fun BarcodeFormat.getErrorCorrectionLevel(position: Int) = when (this) {
BarcodeFormat.AZTEC -> position
BarcodeFormat.QR_CODE -> (position + 1) * 2
BarcodeFormat.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 InputStream.readBytesMax(max: Int): ByteArray {
var offset = 0
var remaining = min(available(), max)
val result = ByteArray(remaining)
while (remaining > 0) {
val read = read(result, offset, remaining)
if (read < 0) {
break
}
remaining -= read
offset += read
}
return result
}
@@ -15,20 +15,43 @@ import android.support.v7.app.AppCompatActivity
import android.support.v7.view.ActionMode
import android.support.v7.widget.SearchView
import android.support.v7.widget.SwitchCompat
import android.view.*
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.widget.EditText
import android.widget.ListView
import de.markusfisch.android.binaryeye.R
import de.markusfisch.android.binaryeye.adapter.ScansAdapter
import de.markusfisch.android.binaryeye.app.*
import de.markusfisch.android.binaryeye.app.addFragment
import de.markusfisch.android.binaryeye.app.alertDialog
import de.markusfisch.android.binaryeye.app.db
import de.markusfisch.android.binaryeye.app.hasWritePermission
import de.markusfisch.android.binaryeye.app.prefs
import de.markusfisch.android.binaryeye.content.copyToClipboard
import de.markusfisch.android.binaryeye.content.shareAsFile
import de.markusfisch.android.binaryeye.content.shareText
import de.markusfisch.android.binaryeye.database.*
import de.markusfisch.android.binaryeye.content.wipeLastShareFile
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.use
import de.markusfisch.android.binaryeye.io.askForFileName
import de.markusfisch.android.binaryeye.io.toSaveResult
import de.markusfisch.android.binaryeye.view.*
import de.markusfisch.android.binaryeye.view.lockStatusBarColor
import de.markusfisch.android.binaryeye.view.setPaddingFromWindowInsets
import de.markusfisch.android.binaryeye.view.systemBarListViewScrollListener
import de.markusfisch.android.binaryeye.view.unlockStatusBarColor
import de.markusfisch.android.binaryeye.view.useVisibility
import de.markusfisch.android.binaryeye.widget.toast
import kotlinx.coroutines.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class HistoryFragment : Fragment() {
private lateinit var useHistorySwitch: SwitchCompat
@@ -47,9 +70,13 @@ class HistoryFragment : Fragment() {
R.menu.fragment_history_edit,
menu
)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP &&
// SDK35+ doesn't draw a status bar background anymore.
Build.VERSION.SDK_INT < Build.VERSION_CODES.VANILLA_ICE_CREAM
) {
lockStatusBarColor()
val ac = activity ?: return false
@Suppress("DEPRECATION")
ac.window.statusBarColor = ContextCompat.getColor(
ac,
R.color.accent_dark
@@ -79,7 +106,8 @@ class HistoryFragment : Fragment() {
closeActionMode()
true
}
R.id.edit_scan -> {
R.id.edit_label -> {
scansAdapter?.forSelection { id, position ->
ac.askForName(
id,
@@ -90,6 +118,7 @@ class HistoryFragment : Fragment() {
closeActionMode()
true
}
R.id.remove_scan -> {
scansAdapter?.getSelectedIds()?.let {
if (it.isNotEmpty()) {
@@ -99,6 +128,7 @@ class HistoryFragment : Fragment() {
closeActionMode()
true
}
else -> false
}
}
@@ -114,6 +144,7 @@ class HistoryFragment : Fragment() {
private var filter: String? = null
private var clearListMenuItem: MenuItem? = null
private var exportHistoryMenuItem: MenuItem? = null
private var shareAsFileMenuItem: MenuItem? = null
override fun onCreate(state: Bundle?) {
super.onCreate(state)
@@ -136,12 +167,16 @@ class HistoryFragment : Fragment() {
useHistorySwitch = view.findViewById(
R.id.use_history
) as SwitchCompat
)
initHistorySwitch(useHistorySwitch)
listView = view.findViewById(R.id.scans)
listView.setOnItemClickListener { _, _, _, id ->
showScan(id)
listView.setOnItemClickListener { _, v, position, id ->
if (actionMode != null) {
scansAdapter?.select(v, id, position)
} else {
showScan(id)
}
}
listView.setOnItemLongClickListener { _, v, position, id ->
scansAdapter?.select(v, id, position)
@@ -156,12 +191,12 @@ class HistoryFragment : Fragment() {
fab = view.findViewById(R.id.share)
fab.setOnClickListener { v ->
v.context.pickListSeparatorAndShare()
v.context.pickListSeparatorAndShare(false)
}
progressView = view.findViewById(R.id.progress_view)
(view.findViewById(R.id.inset_layout) as View).setPaddingFromWindowInsets()
view.findViewById<View>(R.id.inset_layout).setPaddingFromWindowInsets()
listView.setPaddingFromWindowInsets()
update()
@@ -173,6 +208,7 @@ class HistoryFragment : Fragment() {
super.onDestroy()
scansAdapter?.changeCursor(null)
parentJob.cancel()
wipeLastShareFile()
}
override fun onPause() {
@@ -182,10 +218,19 @@ class HistoryFragment : Fragment() {
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.fragment_history, menu)
initSearchView(menu.findItem(R.id.search))
menu.setGroupVisible(R.id.scans_available, scansAdapter?.count != 0)
clearListMenuItem = menu.findItem(R.id.clear)
exportHistoryMenuItem = menu.findItem(R.id.export_history)
val searchMenuItem = menu.findItem(R.id.search)
initSearchView(searchMenuItem)
val visible = scansAdapter?.count != 0
searchMenuItem?.isVisible = visible
exportHistoryMenuItem = menu.findItem(R.id.export_history)?.apply {
isVisible = visible
}
shareAsFileMenuItem = menu.findItem(R.id.share_as_file)?.apply {
isVisible = visible
}
clearListMenuItem = menu.findItem(R.id.clear)?.apply {
isVisible = visible
}
}
private fun initSearchView(item: MenuItem?) {
@@ -221,10 +266,17 @@ class HistoryFragment : Fragment() {
context.askToRemoveScans()
true
}
R.id.export_history -> {
askToExportToFile()
true
}
R.id.share_as_file -> {
context.pickListSeparatorAndShare(true)
true
}
else -> super.onOptionsItemSelected(item)
}
}
@@ -249,7 +301,7 @@ class HistoryFragment : Fragment() {
val cursor = db.getScans(filter)
withContext(Dispatchers.Main) {
val ac = activity ?: return@withContext
val hasScans = cursor != null && cursor.count > 0
val hasScans = cursor.count > 0
if (filter == null) {
if (!hasScans) {
listView.emptyView = useHistorySwitch
@@ -262,7 +314,7 @@ class HistoryFragment : Fragment() {
} else {
View.GONE
}
cursor?.let { cursor ->
cursor.let { cursor ->
// Close previous cursor.
scansAdapter?.also { it.changeCursor(null) }
scansAdapter = ScansAdapter(ac, cursor)
@@ -278,6 +330,7 @@ class HistoryFragment : Fragment() {
private fun enableMenuItems(enabled: Boolean) {
clearListMenuItem?.isEnabled = enabled
exportHistoryMenuItem?.isEnabled = enabled
shareAsFileMenuItem?.isEnabled = enabled
}
private fun closeActionMode() {
@@ -393,12 +446,12 @@ class HistoryFragment : Fragment() {
} ?: return@useVisibility
val message = when (delimiter) {
"db" -> ac.exportDatabase(name)
else -> db.getScansDetailed(filter)?.use {
else -> db.getScansDetailed(filter).use {
when (delimiter) {
"json" -> ac.exportJson(name, it)
else -> ac.exportCsv(name, it, delimiter)
}
} ?: false
}
}.toSaveResult()
withContext(Dispatchers.Main) {
ac.toast(message)
@@ -407,32 +460,44 @@ class HistoryFragment : Fragment() {
}
}
private fun Context.pickListSeparatorAndShare() {
private fun Context.pickListSeparatorAndShare(asFile: Boolean) {
val separators = resources.getStringArray(
R.array.list_separators_values
)
AlertDialog.Builder(this)
.setTitle(R.string.pick_list_separator)
.setItems(R.array.list_separators_names) { _, which ->
shareScans(separators[which])
shareScans(separators[which], asFile)
}
.show()
}
private fun shareScans(format: String) = scope.launch {
private fun shareScans(format: String, asFile: Boolean) = scope.launch {
progressView.useVisibility {
var text: String? = null
db.getScansDetailed(filter)?.use { cursor ->
val selectedIds = scansAdapter?.getSelectedIds()
if (selectedIds?.isNotEmpty() == true) {
db.getScansDetailed(selectedIds.toLongArray())
} else {
db.getScansDetailed(filter)
}.use { cursor ->
val details = format.split(":")
text = when (details[0]) {
"text" -> cursor.exportText(details[1])
"csv" -> cursor.exportCsv(details[1])
else -> cursor.exportJson()
when (details[0]) {
"text" -> Pair(cursor.exportText(details[1]), "txt")
"csv" -> Pair(cursor.exportCsv(details[1]), "csv")
else -> Pair(cursor.exportJson(), "json")
}
}
text?.let {
withContext(Dispatchers.Main) {
context.shareText(it)
}.let { (text, ext) ->
text?.let {
withContext(Dispatchers.Main) {
if (asFile) {
context.shareAsFile(
it,
String.format("scans.%s", ext)
)
} else {
context.shareText(it)
}
}
}
}
}
@@ -441,7 +506,7 @@ class HistoryFragment : Fragment() {
private fun Cursor.exportText(separator: String): String {
val sb = StringBuilder()
val contentIndex = getColumnIndex(Database.SCANS_CONTENT)
val contentIndex = getColumnIndex(Database.SCANS_TEXT)
if (contentIndex > -1 && moveToFirst()) {
do {
val content = getString(contentIndex)
@@ -0,0 +1,94 @@
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.LayoutInflater
import android.view.View
import android.view.ViewGroup
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<View>(R.id.inset_layout).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,9 +16,12 @@ 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.bluetooth.setBluetoothHosts
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
@@ -27,33 +30,70 @@ import de.markusfisch.android.binaryeye.widget.toast
class PreferencesFragment : PreferenceFragmentCompat() {
private val changeListener = object : OnSharedPreferenceChangeListener {
override fun onSharedPreferenceChanged(
sharedPreferences: SharedPreferences,
key: String
sharedPreferences: SharedPreferences?,
key: String?
) {
key ?: return
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 {
context.askToClearNetworkSuggestions()
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
}
}
}
@@ -72,8 +112,8 @@ class PreferencesFragment : PreferenceFragmentCompat() {
@RequiresApi(Build.VERSION_CODES.Q)
private fun Context.clearNetworkSuggestions() {
toast(
if (
removeNetworkSuggestions(this) == WifiManager.STATUS_NETWORK_SUGGESTIONS_SUCCESS
if (removeAllNetworkSuggestions() ==
WifiManager.STATUS_NETWORK_SUGGESTIONS_SUCCESS
) {
R.string.clear_network_suggestions_success
} else {
@@ -84,6 +124,7 @@ class PreferencesFragment : PreferenceFragmentCompat() {
override fun onResume() {
super.onResume()
activity?.setTitle(R.string.preferences)
listView.setPaddingFromWindowInsets()
listView.removeOnScrollListener(systemBarRecyclerViewScrollListener)
listView.addOnScrollListener(systemBarRecyclerViewScrollListener)
@@ -105,6 +146,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)
}
@@ -122,9 +169,11 @@ class PreferencesFragment : PreferenceFragmentCompat() {
is UrlPreference -> {
preference.setSummary(preference.getUrl())
}
is ListPreference -> {
preference.setSummary(preference.entry)
}
is MultiSelectListPreference -> {
preference.setSummary(
preference.values.joinToString(", ") {
@@ -132,6 +181,7 @@ class PreferencesFragment : PreferenceFragmentCompat() {
}
)
}
is PreferenceGroup -> {
setSummaries(preference)
}
@@ -152,3 +202,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())
@@ -9,6 +9,7 @@ import de.markusfisch.android.binaryeye.app.prefs
import de.markusfisch.android.binaryeye.database.Scan
import de.markusfisch.android.binaryeye.net.sendAsync
import de.markusfisch.android.binaryeye.preference.UrlPreference
import de.markusfisch.android.zxingcpp.ZxingCpp.BarcodeFormat
class UrlDialogFragment : PreferenceDialogFragmentCompat() {
private var urlView: TextView? = null
@@ -50,7 +51,7 @@ class UrlDialogFragment : PreferenceDialogFragmentCompat() {
Scan(
"test",
null,
"none"
BarcodeFormat.NONE
).sendAsync(url, prefs.sendScanType) { code, body ->
textView.text = when {
code != null -> "$code"
@@ -55,17 +55,17 @@ fun Bitmap.crop(
pivotX: Float,
pivotY: Float
) = try {
val erected = erect(rotation, pivotX, pivotY)
val w = erected.width
val h = erected.height
val b = rotate(rotation, pivotX, pivotY)
val w = b.width
val h = b.height
val x = max(0, (rect.left * w).roundToInt())
val y = max(0, (rect.top * h).roundToInt())
Bitmap.createBitmap(
erected,
b,
x,
y,
min(w - x, (rect.right * w).roundToInt() - x),
min(h - y, (rect.bottom * h).roundToInt() - y)
min(w, (rect.right * w).roundToInt()) - x,
min(h, (rect.bottom * h).roundToInt()) - y
)
} catch (e: OutOfMemoryError) {
null
@@ -73,13 +73,11 @@ fun Bitmap.crop(
null
}
private fun Bitmap.erect(
private fun Bitmap.rotate(
rotation: Float,
pivotX: Float,
pivotY: Float
): Bitmap = if (
rotation % 360f != 0f
) {
): Bitmap = if (rotation % 360f != 0f) {
Bitmap.createBitmap(
this,
0,
@@ -54,6 +54,7 @@ fun Matrix.setFrameToView(
uprightWidth = frameMetrics.height
uprightHeight = frameMetrics.width
}
else -> {
uprightWidth = frameMetrics.width
uprightHeight = frameMetrics.height
@@ -5,11 +5,12 @@ import android.graphics.Canvas
fun Bitmap.fixTransparency(): Bitmap {
val result = analyzeTransparency()
return if (result.hasTransparentPixels) {
val cfg = config
return if (cfg != null && result.hasTransparentPixels) {
val copy = Bitmap.createBitmap(
width,
height,
config
cfg
)
Canvas(copy).apply {
drawColor(result.backgroundColor)
@@ -61,7 +61,6 @@ private fun Context.openExternalOutputStream(
fileName: String,
mimeType: String
): OutputStream = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
@Suppress("DEPRECATION")
val file = File(
Environment.getExternalStoragePublicDirectory(
Environment.DIRECTORY_DOWNLOADS
@@ -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, 1000)
}
fun releaseToneGenerators() {
confirmToneGenerator?.release()
confirmToneGenerator = null
errorToneGenerator?.release()
errorToneGenerator = null
}
@@ -33,6 +33,7 @@ private fun Scan.send(url: String, type: String): Response {
"1" -> request(
url + "?" + asUrlArguments()
)
"2" -> request(url) { con ->
con.requestMethod = "POST"
con.setRequestProperty("Content-Type", "application/x-www-form-urlencoded")
@@ -41,6 +42,7 @@ private fun Scan.send(url: String, type: String): Response {
close()
}
}
"3" -> request(url) { con ->
con.requestMethod = "POST"
con.setRequestProperty("Content-Type", "application/json")
@@ -49,8 +51,9 @@ private fun Scan.send(url: String, type: String): Response {
close()
}
}
else -> request(
url + content.urlEncode()
url + text.urlEncode()
)
}
}
@@ -64,11 +67,11 @@ private fun Scan.asJson() = JSONObject().apply {
}
private fun Scan.getMap(): Map<String, String> = mapOf(
"content" to content,
"content" to text,
"raw" to raw?.toHexString(),
"format" to format,
"format" to format.name,
"errorCorrectionLevel" to errorCorrectionLevel,
"versionNumber" to versionNumber.toString(),
"version" to version,
"sequenceSize" to sequenceSize.toString(),
"sequenceIndex" to sequenceIndex.toString(),
"sequenceId" to sequenceId,
@@ -0,0 +1,22 @@
package de.markusfisch.android.binaryeye.os
import android.app.Activity
import android.view.WindowManager
fun Activity.getScreenBrightness(): Float {
val layoutParams = window.attributes
return if (layoutParams.screenBrightness == WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_NONE) {
android.provider.Settings.System.getInt(
contentResolver,
android.provider.Settings.System.SCREEN_BRIGHTNESS
) / 255.0f
} else {
layoutParams.screenBrightness
}
}
fun Activity.setScreenBrightness(brightness: Float) {
val layoutParams = window.attributes
layoutParams.screenBrightness = brightness
window.attributes = layoutParams
}
@@ -4,9 +4,8 @@ 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 DURATION_IN_MS = 100L
private const val DURATION_IN_MS = 250L
private val beforeO = Build.VERSION.SDK_INT < Build.VERSION_CODES.O
private val errorPatternTimings = longArrayOf(
@@ -38,9 +37,7 @@ fun Context.getVibrator(): Vibrator = getSystemService(
) as Vibrator
fun Vibrator.vibrate() {
if (!prefs.vibrate) {
return
} else if (beforeO) {
if (beforeO) {
@Suppress("DEPRECATION")
vibrate(DURATION_IN_MS)
} else {
@@ -54,9 +51,7 @@ fun Vibrator.vibrate() {
}
fun Vibrator.error() {
if (!prefs.vibrate) {
return
} else if (beforeO) {
if (beforeO) {
@Suppress("DEPRECATION")
vibrate(errorPatternTimings, -1)
} else {
@@ -0,0 +1,41 @@
package de.markusfisch.android.binaryeye.preference
import android.content.SharedPreferences
import org.json.JSONObject
data class CropHandle(
val x: Int = -2, // -2 means set default roi.
val y: Int = -2,
val orientation: Int = 0
)
fun SharedPreferences.storeCropHandle(
name: String,
cropHandle: CropHandle
) {
edit().putString(
name,
JSONObject().apply {
put("x", cropHandle.x)
put("y", cropHandle.y)
put("orientation", cropHandle.orientation)
}.toString()
).apply()
}
fun SharedPreferences.restoreCropHandle(
name: String,
default: CropHandle = CropHandle()
): CropHandle {
val s = getString(name, null) ?: return default
return try {
val json = JSONObject(s)
CropHandle(
json.getInt("x"),
json.getInt("y"),
json.getInt("orientation")
)
} catch (e: Exception) {
return default
}
}
@@ -1,54 +1,45 @@
package de.markusfisch.android.binaryeye.preference
import android.annotation.SuppressLint
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
import de.markusfisch.android.binaryeye.activity.CameraActivity
import de.markusfisch.android.zxingcpp.ZxingCpp.BarcodeFormat
class Preferences {
lateinit var preferences: SharedPreferences
var barcodeFormats = setOf(
Format.AZTEC.name,
Format.CODABAR.name,
Format.CODE_39.name,
Format.CODE_93.name,
Format.CODE_128.name,
Format.DATA_BAR.name,
Format.DATA_BAR_EXPANDED.name,
Format.DATA_MATRIX.name,
Format.EAN_8.name,
Format.EAN_13.name,
Format.ITF.name,
Format.MAXICODE.name,
Format.PDF_417.name,
Format.QR_CODE.name,
Format.MICRO_QR_CODE.name,
Format.UPC_A.name,
Format.UPC_E.name,
BarcodeFormat.AZTEC.name,
BarcodeFormat.CODABAR.name,
BarcodeFormat.CODE_39.name,
BarcodeFormat.CODE_93.name,
BarcodeFormat.CODE_128.name,
BarcodeFormat.DATA_BAR.name,
BarcodeFormat.DATA_BAR_EXPANDED.name,
BarcodeFormat.DATA_MATRIX.name,
BarcodeFormat.DX_FILM_EDGE.name,
BarcodeFormat.EAN_8.name,
BarcodeFormat.EAN_13.name,
BarcodeFormat.ITF.name,
BarcodeFormat.MAXICODE.name,
BarcodeFormat.PDF_417.name,
BarcodeFormat.QR_CODE.name,
BarcodeFormat.MICRO_QR_CODE.name,
BarcodeFormat.RMQR_CODE.name,
BarcodeFormat.UPC_A.name,
BarcodeFormat.UPC_E.name,
)
@RequiresApi(Build.VERSION_CODES.HONEYCOMB)
set(value) {
apply(BARCODE_FORMATS, value)
field = value
}
var cropHandleX = -2 // -2 means set default roi.
set(value) {
apply(CROP_HANDLE_X, value)
field = value
}
var cropHandleY = -2
set(value) {
apply(CROP_HANDLE_Y, value)
field = value
}
var cropHandleOrientation = 0
set(value) {
apply(CROP_HANDLE_ORIENTATION, value)
field = value
}
var showCropHandle = true
set(value) {
apply(SHOW_CROP_HANDLE, value)
@@ -61,37 +52,27 @@ class Preferences {
}
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
@@ -99,14 +80,24 @@ 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)
field = value
}
var ignoreConsecutiveDuplicates = true
var ignoreDuplicatesName = "ignore_consecutive_duplicates"
set(value) {
apply(IGNORE_CONSECUTIVE_DUPLICATES, value)
apply(IGNORE_DUPLICATES_NAME, value)
field = value
}
var copyImmediately = false
@@ -129,6 +120,11 @@ class Preferences {
apply(SHOW_HEX_DUMP, value)
field = value
}
var showChecksum = ""
set(value) {
apply(SHOW_CHECKSUM, value)
field = value
}
var showRecreation = true
set(value) {
apply(SHOW_RECREATION, value)
@@ -149,7 +145,7 @@ class Preferences {
apply(OPEN_WITH_URL, value)
field = value
}
var sendScanActive = true
var sendScanActive = false
set(value) {
apply(SEND_SCAN_ACTIVE, value)
field = value
@@ -164,9 +160,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
@@ -174,11 +182,36 @@ class Preferences {
apply(INDEX_OF_LAST_SELECTED_FORMAT, value)
field = value
}
var freeRotation = true
var indexOfLastSelectedEcLevel: Int = 0
set(value) {
apply(INDEX_OF_LAST_SELECTED_EC_LEVEL, value)
field = value
}
var freeRotation = false
set(value) {
apply(FREE_ROTATION, value)
field = value
}
var expandEscapeSequences = true
set(value) {
apply(EXPAND_ESCAPE_SEQUENCES, value)
field = value
}
var addQuietZone = true
set(value) {
apply(ADD_QUIET_ZONE, value)
field = value
}
var brightenScreen = false
set(value) {
apply(BRIGHTEN_SCREEN, value)
field = value
}
var previewScale = 0f
set(value) {
apply(PREVIEW_SCALE, value)
field = value
}
fun init(context: Context) {
preferences = PreferenceManager.getDefaultSharedPreferences(context)
@@ -188,15 +221,16 @@ class Preferences {
fun update() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
preferences.getStringSet(BARCODE_FORMATS, barcodeFormats)?.let {
barcodeFormats = it
barcodeFormats = addFormatsOnUpdate(
it,
BarcodeFormat.RMQR_CODE,
BarcodeFormat.DX_FILM_EDGE
)
}
}
cropHandleX = preferences.getInt(CROP_HANDLE_X, cropHandleX)
cropHandleY = preferences.getInt(CROP_HANDLE_Y, cropHandleY)
cropHandleOrientation = preferences.getInt(
CROP_HANDLE_ORIENTATION,
cropHandleOrientation
)
migrateCropHandle()
showCropHandle = preferences.getBoolean(
SHOW_CROP_HANDLE,
showCropHandle
@@ -214,11 +248,22 @@ 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,
ignoreConsecutiveDuplicates
)
// Map old setting to new one if it wasn't on default value.
val ignoreConsecutiveDuplicates = "ignore_consecutive_duplicates"
if (!preferences.getBoolean(ignoreConsecutiveDuplicates, true)) {
preferences.edit().remove(ignoreConsecutiveDuplicates).apply()
ignoreDuplicatesName = "accept_duplicates"
}
preferences.getString(IGNORE_DUPLICATES_NAME, ignoreDuplicatesName)?.also {
ignoreDuplicatesName = it
}
copyImmediately = preferences.getBoolean(
COPY_IMMEDIATELY,
copyImmediately
@@ -229,6 +274,9 @@ class Preferences {
)
showMetaData = preferences.getBoolean(SHOW_META_DATA, showMetaData)
showHexDump = preferences.getBoolean(SHOW_HEX_DUMP, showHexDump)
preferences.getString(SHOW_CHECKSUM, showChecksum)?.also {
showChecksum = it
}
showRecreation = preferences.getBoolean(SHOW_RECREATION, showRecreation)
closeAutomatically = preferences.getBoolean(
CLOSE_AUTOMATICALLY,
@@ -250,6 +298,16 @@ class Preferences {
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
}
@@ -257,23 +315,82 @@ class Preferences {
INDEX_OF_LAST_SELECTED_FORMAT,
indexOfLastSelectedFormat
)
indexOfLastSelectedEcLevel = preferences.getInt(
INDEX_OF_LAST_SELECTED_EC_LEVEL,
indexOfLastSelectedEcLevel
)
freeRotation = preferences.getBoolean(FREE_ROTATION, freeRotation)
expandEscapeSequences = preferences.getBoolean(
EXPAND_ESCAPE_SEQUENCES,
expandEscapeSequences
)
addQuietZone = preferences.getBoolean(ADD_QUIET_ZONE, addQuietZone)
brightenScreen = preferences.getBoolean(
BRIGHTEN_SCREEN,
brightenScreen
)
previewScale = preferences.getFloat(
PREVIEW_SCALE,
previewScale
)
}
private fun addFormatsOnUpdate(
restored: Set<String>,
vararg formats: BarcodeFormat
) = restored.toMutableSet().apply {
for (format in formats) {
val name = "${format.name}_added"
if (!preferences.getBoolean(name, false)) {
preferences.edit().putBoolean(name, true).apply()
add(format.name)
}
}
}
private fun migrateCropHandle() {
val cropHandleXName = "crop_handle_x"
val def = CropHandle()
if (preferences.getInt(cropHandleXName, def.x) == def.x) {
return;
}
storeCropHandle(
CameraActivity.CAMERA_CROP_HANDLE,
CropHandle(
preferences.getInt(cropHandleXName, def.x),
preferences.getInt("crop_handle_y", def.y),
preferences.getInt("crop_handle_orientation", def.orientation)
)
)
preferences.edit().putInt(cropHandleXName, def.x).apply()
}
fun restoreCropHandle(
name: String
) = preferences.restoreCropHandle(name)
fun storeCropHandle(name: String, cropHandle: CropHandle) {
preferences.storeCropHandle(name, cropHandle)
}
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
}
fun ignoreDuplicates() = when (ignoreDuplicatesName) {
"ignore_consecutive_duplicates" -> IgnoreDuplicates.Consecutive
"ignore_any_duplicates" -> IgnoreDuplicates.Any
else -> IgnoreDuplicates.Never
}
private fun put(label: String, value: Boolean) =
preferences.edit().putBoolean(label, value)
private fun put(label: String, value: String) =
preferences.edit().putString(label, value)
private fun commit(label: String, value: Boolean) {
put(label, value).commit()
}
private fun commit(label: String, value: String) {
put(label, value).commit()
}
private fun apply(label: String, value: Boolean) {
put(label, value).apply()
}
@@ -286,16 +403,26 @@ class Preferences {
preferences.edit().putInt(label, value).apply()
}
private fun apply(label: String, value: Float) {
preferences.edit().putFloat(label, value).apply()
}
@SuppressLint("ApplySharedPref")
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 {
enum class IgnoreDuplicates {
Consecutive, Any, Never
}
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"
@@ -304,12 +431,15 @@ class Preferences {
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 IGNORE_DUPLICATES_NAME = "ignore_duplicates_name"
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_CHECKSUM = "show_checksum"
private const val SHOW_RECREATION = "show_recreation"
private const val CLOSE_AUTOMATICALLY = "close_automatically"
private const val DEFAULT_SEARCH_URL = "default_search_url"
@@ -317,8 +447,15 @@ class Preferences {
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"
private const val ADD_QUIET_ZONE = "add_quiet_zone"
private const val BRIGHTEN_SCREEN = "brighten_screen"
private const val PREVIEW_SCALE = "preview_scale"
}
}
@@ -1,5 +1,7 @@
package de.markusfisch.android.binaryeye.service
import android.annotation.SuppressLint
import android.app.PendingIntent
import android.content.Intent
import android.os.Build
import android.service.quicksettings.TileService
@@ -12,6 +14,21 @@ class ScanTileService : TileService() {
super.onClick()
val intent = Intent(applicationContext, CameraActivity::class.java)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
startActivityAndCollapse(intent)
// TileService#startActivityAndCollapse(Intent) is only ever run
// below UpsideDownCake, but Lint doesn't understand this yet.
@SuppressLint("StartActivityAndCollapseDeprecated")
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
@Suppress("DEPRECATION")
startActivityAndCollapse(intent)
} else {
startActivityAndCollapse(
PendingIntent.getActivity(
this,
0,
intent,
PendingIntent.FLAG_IMMUTABLE
)
)
}
}
}
@@ -0,0 +1,82 @@
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,38 @@
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
}
}
@@ -8,6 +8,7 @@ import android.support.v4.content.ContextCompat
import android.support.v7.app.AppCompatActivity
import android.support.v7.widget.LinearLayoutManager
import android.support.v7.widget.RecyclerView
import android.support.v7.widget.Toolbar
import android.view.View
import android.view.WindowManager
import android.widget.AbsListView
@@ -57,10 +58,9 @@ private fun lastChildOutOfView(listView: AbsListView): Boolean {
return child != null && child.bottom >= listView.height
}
fun initSystemBars(activity: AppCompatActivity) {
fun AppCompatActivity.initBars() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
// Keeps the soft keyboard from repositioning the layout.
val window = activity.window
window.setSoftInputMode(
WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE
)
@@ -69,7 +69,16 @@ fun initSystemBars(activity: AppCompatActivity) {
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
}
colorSystemAndToolBars(activity)
colorSystemAndToolBars(this)
setPaddingFromWindowInsets(
findViewById(R.id.main),
(findViewById(R.id.toolbar) as Toolbar).apply {
setSupportActionBar(this)
},
findViewById(R.id.navbar)?.apply {
setBackgroundColor(translucentPrimaryColor)
}
)
}
private var statusBarColorLocked = false
@@ -97,14 +106,27 @@ fun colorSystemAndToolBars(
val topColor = if (scrolled) translucentPrimaryColor else 0
val activity = getAppCompatActivity(context) ?: return
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
val window = activity.window
if (!statusBarColorLocked) {
window.statusBarColor = topColor
}
window.navigationBarColor = if (scrolled || scrollable) {
translucentPrimaryColor
// System bars no longer have a background from SDK35+.
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.VANILLA_ICE_CREAM) {
val window = activity.window
if (!statusBarColorLocked) {
@Suppress("DEPRECATION")
window.statusBarColor = topColor
}
@Suppress("DEPRECATION")
window.navigationBarColor = if (scrolled || scrollable) {
translucentPrimaryColor
} else {
0
}
} else {
0
activity.findViewById(R.id.navbar)?.apply {
visibility = if (scrolled || scrollable) {
View.VISIBLE
} else {
View.GONE
}
}
}
}
activity.supportActionBar?.setBackgroundDrawable(
@@ -7,10 +7,37 @@ import android.support.v4.view.ViewCompat
import android.support.v4.view.WindowInsetsCompat
import android.support.v7.widget.Toolbar
import android.view.View
import android.view.WindowInsets
private var toolbarHeight = 0
fun recordToolbarHeight(toolbar: Toolbar) {
fun setPaddingFromWindowInsets(mainLayout: View, toolbar: Toolbar, navbar: View?) {
toolbarHeight = toolbar.layoutParams.height
// SDK 35+ is edge-to-edge by default, so the toolbar needs to extend
// below the status bar.
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.VANILLA_ICE_CREAM) {
return
}
mainLayout.setOnApplyWindowInsetsListener { _, insets ->
val systemBarInsets = insets.getInsets(
WindowInsets.Type.systemBars()
)
val statusBarTop = systemBarInsets.top
if (toolbar.paddingTop == 0 && statusBarTop > 0) {
toolbar.setPadding(0, statusBarTop, 0, 0)
toolbar.layoutParams.height += statusBarTop
}
val navBarBottom = systemBarInsets.bottom
if (navBarBottom > 0) {
mainLayout.setPadding(0, 0, 0, navBarBottom);
navbar?.apply {
layoutParams.height = navBarBottom
}
}
insets
}
}
fun View.setPaddingFromWindowInsets() {
@@ -10,6 +10,8 @@ import de.markusfisch.android.scalingimageview.widget.ScalingImageView
open class ConfinedScalingImageView : ScalingImageView {
val insets = Rect()
var runAfterLayout: (() -> Unit)? = null
constructor(context: Context, attrs: AttributeSet, defStyle: Int) :
super(context, attrs, defStyle)
@@ -37,5 +39,8 @@ open class ConfinedScalingImageView : ScalingImageView {
)
}
centerRemap()
runAfterLayout?.invoke()
}
fun isInBounds(): Boolean = inBounds()
}
@@ -2,7 +2,12 @@ package de.markusfisch.android.binaryeye.widget
import android.annotation.SuppressLint
import android.content.Context
import android.graphics.*
import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.Path
import android.graphics.Point
import android.graphics.Rect
import android.graphics.Region
import android.os.Build
import android.os.Parcel
import android.os.Parcelable
@@ -14,6 +19,7 @@ import de.markusfisch.android.binaryeye.R
import de.markusfisch.android.binaryeye.app.prefs
import de.markusfisch.android.binaryeye.graphics.getBitmapFromDrawable
import de.markusfisch.android.binaryeye.graphics.getDashedBorderPaint
import de.markusfisch.android.binaryeye.preference.CropHandle
import kotlin.math.abs
import kotlin.math.max
import kotlin.math.min
@@ -78,11 +84,16 @@ class DetectorView : View {
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) :
super(context, attrs, defStyleAttr)
fun saveCropHandlePos() {
fun storeCropHandlePos(name: String) {
val pos = getCropHandlePos()
prefs.cropHandleX = pos.x
prefs.cropHandleY = pos.y
prefs.cropHandleOrientation = currentOrientation
prefs.storeCropHandle(
name,
CropHandle(
pos.x,
pos.y,
currentOrientation
)
)
}
private fun getCropHandlePos() = if (handleActive) {
@@ -91,11 +102,12 @@ class DetectorView : View {
Point(-1, -1)
}
fun restoreCropHandlePos() {
fun restoreCropHandlePos(name: String) {
val cropHandle = prefs.restoreCropHandle(name)
setCropHandlePos(
prefs.cropHandleX,
prefs.cropHandleY,
prefs.cropHandleOrientation
cropHandle.x,
cropHandle.y,
cropHandle.orientation
)
}
@@ -162,6 +174,7 @@ class DetectorView : View {
false
}
}
MotionEvent.ACTION_MOVE -> {
if (handleGrabbed) {
handlePos.set(x, y)
@@ -176,6 +189,7 @@ class DetectorView : View {
false
}
}
MotionEvent.ACTION_CANCEL -> {
if (handleGrabbed) {
snap(x, y)
@@ -183,6 +197,7 @@ class DetectorView : View {
}
false
}
MotionEvent.ACTION_UP -> {
if (handleGrabbed) {
if (!handleActive) {
@@ -199,6 +214,7 @@ class DetectorView : View {
}
false
}
else -> super.onTouchEvent(event)
}
}
@@ -3,19 +3,29 @@ package de.markusfisch.android.binaryeye.widget
import android.content.Context
import android.widget.Toast
fun Context.toast(message: Int) = Toast.makeText(
applicationContext,
message,
Toast.LENGTH_LONG
).show()
private var lastToast: Toast? = null
fun Context.toast(message: String) = Toast.makeText(
applicationContext,
message.ellipsize(128),
Toast.LENGTH_LONG
).show()
fun <T> Context.toast(message: T) {
lastToast?.cancel()
lastToast = when (message) {
is String -> Toast.makeText(
applicationContext,
message.ellipsize(),
Toast.LENGTH_LONG
)
private fun String.ellipsize(max: Int) = if (length < max) {
is Int -> Toast.makeText(
applicationContext,
message,
Toast.LENGTH_LONG
)
else -> return
}
lastToast?.show()
}
private fun String.ellipsize(max: Int = 128) = if (length < max) {
this
} else {
"${take(max)}"
@@ -0,0 +1,4 @@
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/accent"/>
<foreground android:drawable="@drawable/ic_shortcut_history_foreground"/>
</adaptive-icon>
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.

After

Width:  |  Height:  |  Size: 835 B

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.7 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.

After

Width:  |  Height:  |  Size: 471 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.8 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.

After

Width:  |  Height:  |  Size: 590 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.

After

Width:  |  Height:  |  Size: 1.1 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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 16 KiB

@@ -0,0 +1,16 @@
<selector
xmlns:android="http://schemas.android.com/apk/res/android"
android:constantSize="true">
<item android:state_pressed="false">
<shape android:shape="rectangle">
<corners android:radius="2dp"/>
<solid android:color="@color/selected_row"/>
</shape>
</item>
<item android:state_pressed="true">
<shape android:shape="rectangle">
<corners android:radius="2dp"/>
<solid android:color="@color/pressed_row"/>
</shape>
</item>
</selector>
@@ -0,0 +1,3 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#FFFFFF" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="#fff" android:pathData="M20 8.69V4h-4.69L12 0.69 8.69 4H4v4.69L0.69 12 4 15.31V20h4.69L12 23.31 15.31 20H20v-4.69L23.31 12 20 8.69zM12 18c-3.31 0-6-2.69-6-6s2.69-6 6-6 6 2.69 6 6-2.69 6-6 6zm0-10c-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4-1.79-4-4-4z"/>
</vector>

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