Compare commits

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

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

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

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

* Added OpenOrSearchAction
* Added WebAction
* Added DEFAULT_ACTION

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

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

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

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

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

Fix indenting according to Kotlin Coding Conventions.

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

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

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

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

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

Now, instead of crashing, the app provides a fallback for
YUV conversion and image rotation. Of course, it's much
slower than the RenderScript implementation but still
better than crashing the app.
2019-05-14 19:14:59 +02:00
Markus Fisch c993cd53f1 Add italian translation 2019-05-12 21:23:32 +02:00
Markus Fisch 934983f668 Add shadow to Google Play icon 2019-05-09 23:02:13 +02:00
Markus Fisch ccf425faad Add new Google Play icon
According to new icon specification.
2019-05-09 22:34:45 +02:00
Markus Fisch a244763109 Move signing configuration into app/build.gradle
And add a rule to build an app bundle.
2019-05-09 20:38:36 +02:00
Markus Fisch 7ab712e040 Advance version number to 1.7.1 2019-05-08 11:09:25 +02:00
Markus Fisch cb7219b290 Update to RenderScript API 24 to build 64bit libs
Building against RenderScript API 18 doesn't produce
librs.rotator.so for arm64-v8a and x86_64 architectures.
2019-05-08 10:58:55 +02:00
Markus Fisch 7f4ba50ee4 Update gradle, build tools and remove Error Prone
I liked Error Prone. But the old version is incompatible
with Gradle 5 now and using a newer one requires an amount
of work I'm not willing to invest.
2019-05-07 17:05:50 +02:00
Markus Fisch b1f00f0592 Remove unused member variable 2019-05-07 10:51:54 +02:00
Markus Fisch 80f8f908d2 Advance version number to 1.7.0 2019-05-01 13:44:51 +02:00
Markus Fisch 506308ece9 Remove now unused remove_scan string resource 2019-05-01 13:04:08 +02:00
Markus Fisch baecded9d5 Pick list separator on sharing history listing
Depending on the use case, it may be better to have
a dividing line or not.
2019-05-01 12:31:54 +02:00
Markus Fisch ceefee397f Remove extra decoding thread
Unnecessary since CameraView 1.8.2 because Camera.PreviewCallback
is called from a separate thread already.
2019-05-01 12:11:18 +02:00
Markus Fisch e45804be31 Update URL to F-Droid badge 2019-04-30 10:08:54 +02:00
Balázs ÚrandMarkus Fisch edef1acbfb Update Hungarian translation (#28) 2019-04-24 22:08:34 +02:00
Markus Fisch 742939182c Advance version number to 1.6.0 2019-04-24 17:52:46 +02:00
Markus Fisch a21bfcdea6 Share history listing as plain text
Feature request.
2019-04-24 17:36:05 +02:00
Markus Fisch bd281e449b Add missing barcode_info plural for french 2019-04-24 16:49:49 +02:00
Markus Fisch 0e1464a15f Add separator line between history items
Makes it easier to separate list items.
2019-04-24 16:48:47 +02:00
Markus Fisch 8a6441ddf3 Add content description for share FABs
Improve accessibility.
2019-04-24 16:47:48 +02:00
Markus Fisch 94874c0a9e Add content description for flash FAB
Improve accessibility.
2019-04-24 16:46:38 +02:00
Markus Fisch b9a6410959 Advance version number to 1.5.1 2019-04-12 20:46:26 +02:00
Markus Fisch 512f5e00dd Show content bytes instead of chars in hex dump 2019-04-12 20:42:21 +02:00
Markus Fisch 91c2364958 Advance version number to 1.5.0 2019-04-10 21:53:24 +02:00
Markus Fisch 1e3f1c553c Show a hex dump of decoded contents
And update character count when contents are edited too.
2019-04-10 21:48:33 +02:00
Markus Fisch 80c65a48a0 Make when() return the fragment
Instead of assigning a help variable.
2019-04-10 10:30:46 +02:00
Markus Fisch 4c04ffb7db Move string literal to the right side
Left-over from Java "".equals() pattern.
2019-04-10 10:30:16 +02:00
Markus Fisch cc3949fa71 Fix evaluating sent text from share menu 2019-04-10 10:29:55 +02:00
Markus Fisch 904a0ecbdd Update CameraView library to latest version 2019-04-08 20:56:29 +02:00
Balázs ÚrandMarkus Fisch 9155dd026d Update Hungarian translation 2019-03-26 09:53:39 +01:00
grenagitandMarkus Fisch 7286a06985 Add French translation 2019-03-22 15:25:34 +01:00
Markus Fisch a71552a406 Advance version number to 1.4.3 2019-03-21 10:27:09 +01:00
Markus Fisch 5857e8c394 Remove toStartOf/toLeftOf from time view
Circular dependency because "format" already specifies
toEndOf/toRightOf.
2019-03-21 10:20:49 +01:00
Markus Fisch 429af04714 Advance version number to 1.4.2 2019-03-18 22:22:11 +01:00
Markus Fisch f134987572 Remove unused system bar methods 2019-03-18 22:17:51 +01:00
Markus Fisch d8a9043f94 Put findViewById() on one line
Because Android Studio can't stand the line break.
2019-03-18 22:15:07 +01:00
Markus Fisch 3782fa8360 Make sure time and format won't overlap in row 2019-03-18 22:14:40 +01:00
Markus Fisch 023e32f796 Use a plural resource for barcode length 2019-03-18 22:09:41 +01:00
Markus Fisch 82f3abdc86 Use string resource for width by height string 2019-03-18 22:01:40 +01:00
Markus Fisch 1345d31a6d Use property access syntax where possible
And clean up other syntax deviations.
2019-03-18 21:57:05 +01:00
Markus Fisch c56fa39c15 Show barcode type and content length
After a barcode was decoded.
2019-03-18 20:31:08 +01:00
Markus Fisch 7c8997ae9a Update Error Prone version 2019-03-18 20:21:23 +01:00
Markus Fisch 584343c4e9 Add main sources to Gradle source sets
Otherwise Android Studio won't find them.
2019-03-18 20:19:31 +01:00
Markus Fisch 5754f8eb7c Advance version number to 1.4.1 2019-03-17 21:03:29 +01:00
Markus Fisch 15a076026c Only show torch mode FAB if camera supports it 2019-03-17 20:56:31 +01:00
Markus Fisch 1c90451cb9 Catch RuntimeException when enabling torch mode
In case the chosen camera doesn't support torch mode.
2019-03-17 20:41:06 +01:00
Markus Fisch a4939f2f95 Advance version number to 1.4.0 2019-03-17 18:50:48 +01:00
Markus Fisch b19d2b1894 Add scanning from a shared image 2019-03-17 18:45:00 +01:00
Markus Fisch 242421a765 Share generated barcodes 2019-03-07 17:33:36 +01:00
Markus Fisch 4cf12b71ac Apply auto format 2019-03-07 15:43:30 +01:00
Markus Fisch b8cf3a6fe9 Save scanned codes and show a history listing
If the user chooses to keep a history.
There's a switch to enable/disable this feature.
By default, the app won't save anything.
2019-03-07 12:34:42 +01:00
Markus Fisch 333e163624 Update tools version 2019-03-05 12:44:59 +01:00
Markus Fisch dfe1518676 Update CameraView library to latest version 2019-03-04 18:06:18 +01:00
Markus Fisch 8097c685e8 Use Thread.isInterrupted() instead of private flag
And clean up joining threads.
2019-02-28 20:16:33 +01:00
Markus Fisch 810754d833 Fix Hungarian translation for switch camera 2019-02-26 21:58:35 +01:00
Balázs ÚrandMarkus Fisch a0377cb53a Add F-Droid badge 2019-02-26 10:01:46 +01:00
Markus Fisch 6ddcc31a8a Add hungarian string for switch camera 2019-02-26 10:00:29 +01:00
Balázs ÚrandMarkus Fisch b61fc06688 Added Hungarian translation 2019-02-26 09:58:23 +01:00
Markus Fisch f29f9e134f Add menu item to switch camera 2019-02-26 09:23:49 +01:00
Markus Fisch a2b264708d Update tools and Kotlin version 2019-02-24 15:13:07 +01:00
Heimen StoffelsandMarkus Fisch 2ae859511f Added Dutch translation 2019-02-24 14:35:47 +01:00
Markus Fisch b554e7f47c Advance version number to 1.3.8 2019-02-19 22:04:38 +01:00
Markus Fisch 1085b63d25 Correct a spelling mistake in README 2019-02-14 19:55:16 +01:00
Markus Fisch ac900ec855 Remove pre-built libraries and use gradle instead
Because F-Droid won't accept apps that contain pre-built libraries
that aren't fetched from a white-listed Maven repository, see
https://github.com/markusfisch/BinaryEye/issues/1
2019-02-14 19:46:26 +01:00
Markus Fisch 8f68e66047 Update Kotlin version 2019-01-21 10:04:07 +01:00
Markus Fisch faf300db3e Advance version number to 1.3.7 2019-01-18 18:40:36 +01:00
Markus Fisch 2fbdd03225 Normalize URL scheme for resolveActivity()
Because Intent.resolveActivity() fails to resolve URLs
with uppercase schemes. Uri.normalizeScheme() will make
the scheme lowercase.
2019-01-18 18:28:08 +01:00
Markus Fisch 9f825f9d5d Update build tools and gradle wrapper 2019-01-18 18:27:50 +01:00
Markus Fisch 5ff1904148 Update build tools and Kotlin versions 2018-11-21 17:33:14 +01:00
Markus Fisch 16e80ff80e Update camera view library
setTapToFocus() is now available in the library.
2018-10-26 21:31:24 +02:00
Markus Fisch 1dcd1845a7 Update build tools and gradle wrapper 2018-10-09 10:00:33 +02:00
Markus Fisch 955f507023 Update tools version 2018-10-04 22:28:40 +02:00
Markus Fisch 47a3224b27 Advance version number to 1.3.6 2018-10-04 21:12:26 +02:00
Markus Fisch d2bf09d321 Update camera view library 2018-10-04 21:00:56 +02:00
Markus Fisch deee39b367 Use property access syntax to get scene modes 2018-10-04 20:57:59 +02:00
Markus Fisch 4829433725 Revert "Pick camera mode with kotlin's first syntax"
`first` throws if there is no such element so it can't
be used to pick an item that can be there.

This reverts commit 9efd43e496.
2018-10-04 14:54:26 +02:00
Felizia BernutzandMarkus Fisch 9efd43e496 Pick camera mode with kotlin's first syntax
Makes the code a little bit shorter and more kotlin like.
2018-10-04 09:51:41 +02:00
Felizia BernutzandMarkus Fisch 5bd904ba4f Handle no supported scene modes (#5)
Handle nullable result of getSupportedSceneModes()

Only search for SCENE_MODE_BARCODE if there are any supported scene
modes. This extra check prevents triggering onCameraError() when there
are no supported scene modes, which would result in an unusable
application.
2018-09-22 10:23:49 +02:00
Markus Fisch 3244f0775f Advance version number to 1.3.5 2018-08-08 18:48:06 +02:00
Markus Fisch f4d625a048 Fix some minor style issues 2018-08-08 18:29:34 +02:00
Markus Fisch c838486ca1 Apply Kotlin Coding Conventions
Functions that don't fit on a single line should use
the following syntax:

https://kotlinlang.org/docs/reference/coding-conventions.html#function-formatting
2018-07-29 23:37:02 +02:00
Markus Fisch d4a9ea1a4b Fix directory name of german resources
Because `values-de-DE` results in:
> Error: Invalid resource directory name
2018-07-29 23:35:55 +02:00
Markus Fisch 4123905853 Add german translation for encode button
Remove `app_name` because that's in `donottranslate.xml`.

And convert spaces to tabs.
2018-07-29 23:29:53 +02:00
Markus FischandGitHub 720811bf3b Add German translation
Merge pull request #4 from ialokim/german
2018-07-29 23:27:50 +02:00
Markus FischandGitHub 375fb4aa46 Fix Share intent handling
Merge pull request #3 from ialokim/encode-indent
2018-07-29 23:26:42 +02:00
Ialokim aa4b79d13d added German translation 2018-07-29 19:43:53 +02:00
Ialokim 1e9c303646 handle Intent.SEND correctly, directly opening EncodeFragment 2018-07-29 19:20:37 +02:00
Markus Fisch d3ab28f2b3 Update Kotlin version 2018-07-29 15:44:54 +02:00
Markus Fisch 7b67778a29 Return scan result for ZXing' SCAN intents
Of course, handling ZXing' SCAN intents makes only
sense when the app is returning the scanned data.

And that's why responding to IMAGE_CAPTURE does not
make sense at all. Removed that.
2018-07-29 15:40:47 +02:00
Markus Fisch d2c8ef719b Respond to IMAGE_CAPTURE and ZXing' SCAN intents
So a user can pick Binary Eye in the share menu.
2018-07-29 13:32:24 +02:00
Markus Fisch 7bed192300 Update build tools and target SDK 2018-07-05 21:32:03 +02:00
Markus Fisch 331f65f3a4 Advance version number to 1.3.4 2018-06-02 13:17:14 +02:00
Markus Fisch e17c732b6a Decrease RenderScript Target API to 18
Because it's recommended to "set this value to the lowest
API level able to provide all the functionality" (see
https://developer.android.com/guide/topics/renderscript/compute).

Targeting (RenderScript!) API 24 seems to make Google Play
not transfer the support libraries to devices running Nougat
or higher. If you're installing the (release) APK manually
everything works just fine. Install the APK with Google Play
and the app will crash.
2018-06-02 13:05:10 +02:00
Markus Fisch 2e60b2b997 Add hint to EditText for decoded content
Just to make lint happy.
2018-06-02 12:38:12 +02:00
Markus Fisch 70d9b8b99a Fix launcher foreground for adaptive icons 2018-06-02 12:34:02 +02:00
Markus Fisch d0cc7e0a68 Advance version number to 1.3.3 2018-06-01 14:05:37 +02:00
Markus Fisch 264e7ad098 Downsample to 3/4 only 2018-05-31 20:16:00 +02:00
Markus Fisch 8c33e9bd57 Add automated test for YUV processor 2018-05-29 21:00:26 +02:00
Markus Fisch a8fd37e4e4 Run setFocusArea() for ACTION_UP events only 2018-05-29 20:58:32 +02:00
Markus Fisch 17f11ea2de Improved RenderScript processing 2018-05-29 20:58:14 +02:00
Markus Fisch f6a582e4f7 Include aar of CameraView library
Because you simply cannot rely on JitPack.
2018-05-29 19:47:28 +02:00
Markus Fisch 6e645a567a Use XML drawable for launcher background
Much smaller.
2018-05-27 21:35:22 +02:00
Markus Fisch e87dc35372 Downsample camera frame to soften artifacts
Because ZXing has problems recognizing barcodes when there
are screen artifacts in the image. Makes it hard to scan a
QR code from a screen.
2018-05-27 21:29:35 +02:00
Markus Fisch 75f4f3eadc Use property access syntax 2018-05-27 21:23:39 +02:00
Markus Fisch 64e1d722d5 Route raw YUV data to ZXing
And don't convert it into a Bitmap just to read the
pixels back from it.
2018-05-20 15:31:40 +02:00
Markus Fisch 05a206384c Add a comment explaining that !! operator 2018-05-20 14:48:21 +02:00
Markus Fisch 04790e2b57 Rename YuvToGray to Preprocessor
This class does more than just converting from
YUV to gray so this is a better name.
2018-05-20 13:45:38 +02:00
Markus Fisch aa3c4a17e9 Rename function files to use camel humps
According to the Kotlin Coding Conventions
(https://kotlinlang.org/docs/reference/coding-conventions.html)
files that contain only top-level declarations should also
use camel humps with an uppercase first letter.
2018-05-20 13:33:03 +02:00
Markus Fisch 5968ae01eb Add adaptive icon for Android 8+ 2018-05-14 21:06:01 +02:00
Markus Fisch 57500b7700 Decrease default zoom to 10% 2018-05-09 22:50:47 +02:00
Markus Fisch 4e17a21e5f Update camera library and use orientation listener
To handle landscape to landscape rotations.
2018-05-07 20:12:51 +02:00
Markus Fisch 605dc51d50 Update Kotlin version 2018-05-06 14:45:43 +02:00
Markus Fisch b17d29ba28 Increase size of logo on splash screen 2018-04-30 11:01:54 +02:00
Markus Fisch 9afc7fcc62 Update Kotlin and tools version 2018-04-28 11:54:11 +02:00
Markus Fisch d5543d0c39 Update gradle plugin and tools version 2018-04-15 15:37:25 +02:00
Markus Fisch 96fea1f343 Start decoding only when preview has started 2018-03-03 18:33:32 +01:00
Markus Fisch bc4d9f7148 Update to latest CameraView 2018-03-03 18:32:56 +01:00
Markus Fisch 4abacf030b Include CameraView library over jitpack
Also update to CameraView 1.4.0 and use 'implementation'
instead of 'compile' because Gradle 4 says so.
2018-03-01 14:34:45 +01:00
Markus Fisch 279c988464 Apply Kotlin Coding Conventions
From:
https://kotlinlang.org/docs/reference/coding-conventions.html
2018-02-27 14:22:41 +01:00
Markus Fisch a6449b50e9 Update build tools and Kotlin version 2018-02-27 10:44:16 +01:00
Markus Fisch b4e6449dac Decrease default magnification to 20% 2018-02-27 10:38:54 +01:00
Markus Fisch c3dfadd9e6 Move Java static class into Kotlin function file
All methods of SystemBarMetrics can be static so it's
better to get rid of that empty container class.
2017-12-18 21:37:49 +01:00
Markus Fisch bad5225024 Clean up supported screen settings
According to the docs, `resizeable` is deprecated and
`smallScreens`, `normalScreens`, `anyDensity` are true
by default.
2017-12-16 13:31:56 +01:00
Markus Fisch aa9b4927b0 Update to latest CameraView 2017-12-10 18:06:45 +01:00
Markus Fisch 69d12910cd Remove Travis CI integration
Apparently, at least for Android builds, Travis CI just doesn't
cut the mustard.

After updating the SDK version to 27, Travis CI *always* fails
to build the project because the android-27 license is not accepted.
I've done everything https://docs.travis-ci.com/user/languages/android/
told me to but still, it won't build.

So I've got enough of Travis CI and that's it. If that thing can't
build a standard Android project, it's simply not useful to me.
2017-12-09 12:44:57 +01:00
147 changed files with 7417 additions and 1392 deletions
+1 -1
View File
@@ -1,8 +1,8 @@
*.DS_Store
*.swp
.gradle
.idea
build
local.properties
gradle.properties
infer-out
*.iml
-18
View File
@@ -1,18 +0,0 @@
language: android
android:
components:
- platform-tools
- build-tools-26.0.2
- android-27
- extra-android-m2repository
addons:
sonarcloud:
organization: "markusfisch-github"
token:
secure: "mTpn1JNKn/PzEFGTRvkmSfCj0c6nX8n/ef2IO0xPSFyveuM+cpNka5d4/nT1QxEqq1sLUj5HMHEShdk6XlER5Oro+DqekTSw8ONv8P24IZGjiM5noLJZ7uqv/73tM0SevROl3Ldee6AzyS7PzcextQms5UC8uyCgI2jbYwz3hpayaWPquBGRnlw7xGuYwCjJS2cMrhz8sEYRn3Egk3no1YJlxCdbIVaqtVmLweqBzLp176FWB5Svg8N3r+UzaZ3fhQn6LhlHPuObPTrCoVjy6W9TxdUsHfW3F61Zei9h1IEsCC0qsalY0OZC8ML5F2DTDp0spKHnPlXR4Q4hF9XIngk2vPVMtfJhu3JJRojjrxhtfOUZO/VS0AMpueZ5QiL1ZbHziKk9ztuRsB+OIoehWnBTq/r/igrWfqxfsq2VURfAYFldRnobBlT/svNkHBjHfZl9EqKprvwmmvw15ZD0twgsskweDn3GssHC5MGTH6+ujZI19t/SLbDMY2KVPNCrG9sHEXSqKT8wWxqGigUaIbVytD2SqGWep0qsB86FQLAXOmgr3+0upTx3B8YhMG5GGu/R45pJBwzXZbTqSx0goaxP+BBEkzcjQHNl0RvpF44m/FbPMYUBuNyHHEoEZC5G1raOV9jtI8G6whQP3OaT8lO2Aqbp1W/lTSEnCDcKYlU="
script:
- bash gradlew build
- bash gradlew sonarqube
branches:
only:
- master
+148
View File
@@ -1,5 +1,153 @@
# Change Log
# 1.18.0
* Make open URL default action after decoding a barcode
* Add a setting to open contents without inspection
* Highlight codes for picking in shared/loaded images
* Improve visability of crop border
* Fix returning result for ZXing's SCAN intent
# 1.17.1
* Fix navigating back without choosing an image
# 1.17.0
* Pinch/Zoom shared/loaded images to scan just a section
* Add a load file menu item to load an image file
* Add a save file menu item to the barcode view
* Use discrete style for size seek bar
* Fix bar transparency when zooming/pinching
# 1.16.0
* Improve displaying scanned barcodes
* Show a message if a barcode cannot be shared
* Fix setting system bar transparency when double taping a barcode
* Update Dutch translation
* Update Hungarian translation
* Update Indonesian translation
* Update Italian translation
# 1.15.0
* Additionally export history as CSV file
* Color tool and system bars when a view is scrolled
* Add an intent filter for opening images
* Add Russian translation
# 1.14.0
* Add OTP support
* Update Indonesian translation
# 1.13.1
* Fix crash caused by missing support for Regex on Android < 6
# 1.13.0
* Add missing extras for ZXing result intents
# 1.12.0
* Add preferences dialog and the option to use a custom URL with read contents
* Add Brazilian Portuguese translation
* Add Chinese translation
* Encode text using UTF-8 encoding when creating a barcode
# 1.11.2
* Downgrade ZXing version
* Improve getting system UI metrics
# 1.11.1
* Fix tap to focus on some devices
# 1.11.0
* Add support for VCard and VEvent barcodes contents
* Generated barcodes are now zoomable
* Fix creating PDF417 barcodes
## 1.10.0
* Add support for special barcode contents (WiFi, SMS, phone, E-mail)
* Add Indonesian translation
* Update Hungarian translation
* Make system bars completely transparent
## 1.9.1
* Pick search engine for content that is not an URL
## 1.9.0
* Try Google when opening content as URL fails
* Show generic error message if barcode generation fails
## 1.8.1
* Distribute APK instead of App Bundle
## 1.8.0
* Save binary data into external file
* Fix storage of binary data
* Remove horizontal orientation line from scanner
## 1.7.3
* Fix crashes on Lineage and remove RenderScript fallback
* Improve handling of binary data
## 1.7.2
* Add a fallback for RenderScript
* Add italian translation
## 1.7.1
* Fix support for 64 bit architectures
## 1.7.0
* Pick list separator on sharing history listing
* Update Hungarian translation
## 1.6.0
* Share history listing as plain text
* Add separator line between history items
* Add content description for floating action buttons
## 1.5.1
* Fix generating hex dump
## 1.5.0
* Show a hex dump of decoded contents
* Fix processing text from share menu
* Add French translation
## 1.4.3
* Fix broken history listing
## 1.4.2
* Show barcode type and content length after decoding
## 1.4.1
* Toggle availability of torch mode button according to camera features
## 1.4.0
* Save scanned codes and show a history listing
* Share generated barcodes
* Add scanning from a shared image
* Add menu item to switch between camera front/back camera
* Add dutch and hungarian translation
## 1.3.8
* Qualify for F-Droid
## 1.3.7
* Normalize URL schemes
## 1.3.6
* Fix handling of camera scene modes
## 1.3.5
* Add german translation
* Fix handling of data intents
* Respond to ZXing' SCAN intents
## 1.3.4
* Fix pre-processing for Android 7+
* Fix adaptive icons for Android 8+
## 1.3.3
* Improved pre-processing
* Added adaptive icon for Android 8+
* Decrease default magnification to 10%
## 1.3.2
* Fixed crash when going back very fast
+20
View File
@@ -0,0 +1,20 @@
# Contribution Guidelines
Please try to keep things in good shape and comply to what's there.
This project follows Android [best practices][android_best_practices]
so please have a look if you've never heard of them.
The code is formatted according to Android Studio's standard, with the
exception of indent being tabs instead of spaces.
Use the feature branch workflow to add new features and make sure
to squash when merging into master:
$ git merge cool_feature --squash
Then write a [good commit messages][commit_messages] to keep the history
meaningful and useful. One feature, one commit.
[android_best_practices]: https://developer.android.com/distribute/best-practices/develop/
[commit_messages]: https://juffalow.com/other/write-good-git-commit-message
+8 -10
View File
@@ -1,5 +1,4 @@
PACKAGE = de.markusfisch.android.binaryeye
APK = app/build/outputs/apk/debug/app-debug.apk
all: debug install start
@@ -7,23 +6,22 @@ debug:
./gradlew assembleDebug
release: lint
@./gradlew assembleRelease \
-Pandroid.injected.signing.store.file=$(ANDROID_KEYFILE) \
-Pandroid.injected.signing.store.password=$(ANDROID_STORE_PASSWORD) \
-Pandroid.injected.signing.key.alias=$(ANDROID_KEY_ALIAS) \
-Pandroid.injected.signing.key.password=$(ANDROID_KEY_PASSWORD)
./gradlew assembleRelease
bundle: lint
./gradlew bundleRelease
lint:
./gradlew lintDebug
sonarqube:
./gradlew sonarqube
infer: clean
infer -- ./gradlew assembleDebug
test:
./gradlew test cAT
install:
adb $(TARGET) install -r $(APK)
adb $(TARGET) install -r app/build/outputs/apk/debug/app-debug.apk
start:
adb $(TARGET) shell 'am start -n \
+2 -1
View File
@@ -2,4 +2,5 @@
Binary Eye does not collect any personal information.
If you have any questions or suggestions about my Privacy Policy, do not hesitate to contact me.
If you have any questions or suggestions about my Privacy Policy, do not
hesitate to contact me.
+19 -7
View File
@@ -1,21 +1,33 @@
# Binary Eye
<a href="https://travis-ci.org/markusfisch/BinaryEye"><img src="https://api.travis-ci.org/markusfisch/BinaryEye.svg" alt="Travis-ci"/></a>
Yet another barcode scanner for Android. As if there weren't [enough][play].
This one is totally free, open source and does use the [ZXing][zxing] barcode
scanning library.
Unlike many others it works in portrait and landscape orientation, can read
inverted codes, uses Material Design and doesn't do anything else but decoding
Unlike many others, it works in portrait and landscape orientation, can read
inverted codes, comes in Material Design and doesn't do much else but decoding
a barcode.
<a href="https://play.google.com/store/apps/details?id=de.markusfisch.android.binaryeye"><img alt="Get it on Google Play" src="https://play.google.com/intl/en_us/badges/images/apps/en-play-badge.png" height="45px"/></a>
And it's written in [Kotlin][kotlin] because I've wanted to explore that a
And it's written in [Kotlin][kotlin] because I wanted to explore that a
bit more.
## Screenshots
<img
src="https://raw.githubusercontent.com/markusfisch/BinaryEye/gh-pages/screencap-scanning.png"
alt="Screenshot Scanning" width="160"/>
<img
src="https://raw.githubusercontent.com/markusfisch/BinaryEye/gh-pages/screencap-decoded.png"
alt="Screenshot Result" width="160"/>
<img
src="https://raw.githubusercontent.com/markusfisch/BinaryEye/gh-pages/screencap-compose-barcode.png"
alt="Screenshot Compose Barcode" width="160"/>
## Download
<a href="https://f-droid.org/en/packages/de.markusfisch.android.binaryeye/"><img alt="Get it on F-Droid" src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png" height="80"/></a> <a href="https://play.google.com/store/apps/details?id=de.markusfisch.android.binaryeye"><img alt="Get it on Google Play" src="https://play.google.com/intl/en_us/badges/images/generic/en_badge_web_generic.png" height="80"/></a>
[play]: https://play.google.com/store/search?q=barcode%20scanner&c=apps
[zxing]: https://github.com/zxing/zxing
[kotlin]: http://kotlinlang.org/
+41 -15
View File
@@ -1,10 +1,5 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'net.ltgt.errorprone'
configurations.errorprone {
resolutionStrategy.force 'com.google.errorprone:error_prone_core:2.0.19'
}
android {
compileSdkVersion sdk_version
@@ -14,11 +9,31 @@ android {
minSdkVersion 9
targetSdkVersion sdk_version
versionCode 8
versionName '1.3.2'
versionCode 46
versionName '1.18.0'
// it's recommended to set this value to the lowest API level
// able to provide all the functionality
renderscriptTargetApi 24
renderscriptSupportModeEnabled true
testInstrumentationRunner 'android.support.test.runner.AndroidJUnitRunner'
}
signingConfigs {
config {
keyAlias System.getenv('ANDROID_KEY_ALIAS')
keyPassword System.getenv('ANDROID_KEY_PASSWORD')
storePassword System.getenv('ANDROID_STORE_PASSWORD')
def filePath = System.getenv('ANDROID_KEYFILE')
storeFile filePath ? file(filePath) : null
}
}
sourceSets {
main.java.srcDirs += 'src/main/kotlin'
androidTest.java.srcDirs += 'src/androidTest/kotlin'
test.java.srcDirs += 'src/test/kotlin'
}
buildTypes {
@@ -27,11 +42,10 @@ android {
}
release {
proguardFiles \
getDefaultProguardFile('proguard-android.txt'), \
'proguard-rules.pro'
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
minifyEnabled true
shrinkResources true
signingConfig signingConfigs.config
}
}
@@ -44,9 +58,21 @@ android {
}
dependencies {
compile fileTree(dir: 'libs', include: '*')
compile "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version"
compile "com.android.support:appcompat-v7:$support_version"
compile "com.android.support:design:$support_version"
compile project(':cameraview')
androidTestImplementation ('com.android.support.test:runner:1.0.2') {
exclude module: 'support-annotations'
}
androidTestImplementation ('com.android.support.test:rules:1.0.2') {
exclude module: 'support-annotations'
}
testImplementation 'junit:junit:4.12'
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.2"
implementation "com.android.support:appcompat-v7:$support_version"
implementation "com.android.support:design:$support_version"
// Do NOT upgrade ZXing because newer versions won't work on API < 24
// https://github.com/zxing/zxing/issues/1170
implementation 'com.google.zxing:core:3.3.3'
implementation 'com.github.markusfisch:CameraView:1.8.4'
implementation 'com.github.markusfisch:ScalingImageView:1.1.4'
}
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,51 @@
package de.markusfisch.android.binaryeye
import android.support.test.InstrumentationRegistry
import android.support.test.runner.AndroidJUnit4
import de.markusfisch.android.binaryeye.rs.Preprocessor
import de.markusfisch.android.binaryeye.zxing.Zxing
import org.junit.Test
import org.junit.runner.RunWith
import java.util.regex.Pattern
@RunWith(AndroidJUnit4::class)
class PreprocessorTest {
@Test
fun processSamples() {
val zxing = Zxing()
val assets = InstrumentationRegistry.getInstrumentation()
.context.assets
val pattern = Pattern.compile(
"[0-9]+-([0-9]+)x([0-9]+)-([0-9]+)deg.yuv"
)
val files = assets.list("yuv") ?: return
for (file in files) {
val m = pattern.matcher(file)
if (!m.find() || m.groupCount() < 3) {
continue
}
val frameWidth = m.group(1) ?: return
val frameHeight = m.group(2) ?: return
val frameOrientation = m.group(3) ?: return
val frameData = assets.open("yuv/$file").readBytes()
val preprocessor = Preprocessor(
InstrumentationRegistry.getTargetContext(),
frameWidth.toInt(),
frameHeight.toInt(),
frameOrientation.toInt()
)
preprocessor.process(frameData)
val result = zxing.decode(
frameData,
preprocessor.outWidth,
preprocessor.outHeight
)
preprocessor.destroy()
checkNotNull(result) { "no barcode found in $file" }
}
}
}
@@ -0,0 +1,83 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#FF000000"
android:pathData="M52,-884.362h29v71h-29z"/>
<path
android:fillColor="#FF000000"
android:pathData="M52,-884.362h29v71h-29z"/>
<path
android:fillColor="#FF000000"
android:pathData="M52,-884.362h29v71h-29z"/>
<path
android:fillColor="#FF000000"
android:pathData="M32.002,-24.974h3.127v7.656h-3.127z"/>
<path
android:fillColor="#FF000000"
android:pathData="M52,-884.362h29v71h-29z"/>
<path
android:fillColor="#FF000000"
android:pathData="M52,-884.362h29v71h-29z"/>
<path
android:fillColor="#FF000000"
android:pathData="M52,-884.362h29v71h-29z"/>
<path
android:fillColor="#FF000000"
android:pathData="M52,-884.362h29v71h-29z"/>
<path
android:fillColor="#FF000000"
android:pathData="M52,-884.362h29v71h-29z"/>
<path
android:pathData="m54.826,27.893c-6.851,0 -13.701,2.581 -18.924,7.745 -10.445,10.328 -10.413,27.062 0,37.423l36.218,36.036 37.879,0.902 -0.07,-38.363 -36.18,-35.998c-5.207,-5.18 -12.073,-7.745 -18.924,-7.745z"
android:strokeAlpha="1"
android:strokeLineJoin="miter"
android:strokeWidth="8.60725021"
android:fillColor="#000000"
android:strokeColor="#00000000"
android:fillType="nonZero"
android:fillAlpha="0.1254902"
android:strokeLineCap="butt"/>
<path
android:pathData="M28.001,54.118a26.525,26.227 0,1 0,53.049 0a26.525,26.227 0,1 0,-53.049 0z"
android:strokeAlpha="1"
android:strokeLineJoin="miter"
android:strokeWidth="4"
android:fillColor="#ffffff"
android:strokeColor="#00000000"
android:fillAlpha="1"
android:strokeLineCap="butt"/>
<path
android:pathData="M39.26,54.988a14.509,14.346 0,1 0,29.019 0a14.509,14.346 0,1 0,-29.019 0z"
android:strokeAlpha="1"
android:strokeLineJoin="miter"
android:strokeWidth="4"
android:fillColor="#000000"
android:strokeColor="#00000000"
android:fillAlpha="1"
android:strokeLineCap="butt"/>
<path
android:pathData="M54.034,44.513a9.535,9.428 0,1 0,19.07 0a9.535,9.428 0,1 0,-19.07 0z"
android:strokeAlpha="1"
android:strokeLineJoin="miter"
android:strokeWidth="10"
android:fillColor="#ffffff"
android:strokeColor="#00000000"
android:fillAlpha="1"
android:strokeLineCap="butt"/>
<path
android:fillColor="#FF000000"
android:pathData="M52,-884.362h29v71h-29z"/>
<path
android:pathData="m62.745,24.783c-2.055,1.011 -2.87,3.415 -1.847,5.446 0.588,1.168 1.642,1.916 2.839,2.168 -0.564,0.884 -0.835,1.972 -0.925,3.156 -1.782,-0.45 -3.735,-0.191 -5.25,0.639a0.799,0.808 90,0 0,-0.132 0.172,0.799 0.808,90 0,0 -0.132,0.172 0.799,0.808 90,0 0,-0.132 0.39,0.799 0.808,90 0,0 0.044,0.304 0.799,0.808 90,0 0,0.437 0.433,0.799 0.808,90 0,0 0.131,0.043 0.799,0.808 90,0 0,0.131 0.043,0.799 0.808,90 0,0 0.218,-0 0.799,0.808 90,0 0,0.263 -0.129c1.29,-0.707 2.97,-0.892 4.375,-0.425 0.096,0.882 0.341,1.782 0.695,2.683 -0.456,0.072 -0.936,0.177 -1.357,0.343 -1.207,0.48 -2.245,1.302 -3.022,2.244a0.799,0.808 90,0 0,-0.044 0.129,0.799 0.808,90 0,0 -0.088,0.258 0.799,0.808 90,0 0,0.044 0.304,0.799 0.808,90 0,0 0.393,0.563 0.799,0.808 90,0 0,0.394 0.131,0.799 0.808,90 0,0 0.175,-0.086 0.799,0.808 90,0 0,0.175 -0.086,0.799 0.808,90 0,0 0.219,-0.215c0.922,-1.115 2.409,-1.925 3.897,-2.025 0.494,0.845 1.016,1.575 1.658,2.209 -1.232,1.149 -2.043,2.664 -2.239,4.234a0.799,0.808 90,0 0,-0.001 0.215,0.799 0.808,90 0,0 0.174,0.346 0.799,0.808 90,0 0,0.043 0.086,0.799 0.808,90 0,0 0.219,0.217 0.799,0.808 90,0 0,0.131 0.043,0.799 0.808,90 0,0 0.131,0.043 0.799,0.808 90,0 0,0.218 -0,0.799 0.808,90 0,0 0.218,-0 0.799,0.808 90,0 0,0.132 -0.172,0.799 0.808,90 0,0 0.175,-0.086 0.799,0.808 90,0 0,0.044 -0.129,0.799 0.808,90 0,0 0.088,-0.258 0.799,0.808 90,0 0,0.044 -0.129c0.159,-1.272 0.816,-2.493 1.799,-3.371 1.966,1.394 4.252,1.797 6.122,0.876 1.892,-0.931 2.903,-3.018 2.941,-5.444 1.327,-0.259 2.813,0.014 3.936,0.656a0.799,0.808 90,0 0,0.174 0.129,0.799 0.808,90 0,0 0.131,0.043 0.799,0.808 90,0 0,0.088 -0.043,0.799 0.808,90 0,0 0.218,-0 0.799,0.808 90,0 0,0.087 -0.043,0.799 0.808,90 0,0 0.175,-0.086 0.799,0.808 90,0 0,0.132 -0.172,0.799 0.808,90 0,0 0.132,-0.172 0.799,0.808 90,0 0,0.045 -0.562,0.799 0.808,90 0,0 -0.043,-0.086 0.799,0.808 90,0 0,-0.262 -0.303,0.799 0.808,90 0,0 -0.131,-0.043c-1.386,-0.792 -3.097,-1.066 -4.766,-0.787 -0.125,-0.89 -0.309,-1.783 -0.695,-2.683 0.982,-1.073 2.47,-1.756 3.896,-1.81a0.799,0.808 90,0 0,0.131 0.043,0.799 0.808,90 0,0 0.087,-0.043 0.799,0.808 90,0 0,0.482 -0.345,0.799 0.808,90 0,0 0.044,-0.129 0.799,0.808 90,0 0,0.088 -0.476,0.799 0.808,90 0,0 -0.043,-0.086 0.799,0.808 90,0 0,-0.262 -0.303,0.799 0.808,90 0,0 -0.043,-0.086 0.799,0.808 90,0 0,-0.394 -0.131,0.799 0.808,90 0,0 -0.131,-0.043c-1.227,0.048 -2.518,0.369 -3.633,1.031 -0.388,0.233 -0.762,0.548 -1.095,0.864 -0.513,-0.821 -1.093,-1.559 -1.746,-2.166 0.47,-1.39 1.632,-2.605 2.98,-3.195a0.799,0.808 90,0 0,0.175 -0.086,0.799 0.808,90 0,0 0.132,-0.172 0.799,0.808 90,0 0,0.132 -0.172,0.799 0.808,90 0,0 0.132,-0.39 0.799,0.808 90,0 0,-0.043 -0.086,0.799 0.808,90 0,0 -0.174,-0.346 0.799,0.808 90,0 0,-0.043 -0.086,0.799 0.808,90 0,0 -0.437,-0.217 0.799,0.808 90,0 0,-0.175 0.086,0.799 0.808,90 0,0 -0.131,-0.043 0.799,0.808 90,0 0,-0.087 0.043,0.799 0.808,90 0,0 -0.131,-0.043c-1.584,0.694 -2.97,2.079 -3.681,3.756 -1.012,-0.644 -2.048,-1.087 -3.104,-1.173 0.53,-1.101 0.642,-2.414 0.05,-3.59 -1.023,-2.031 -3.541,-2.795 -5.596,-1.783z"
android:fillColor="#ff2a2a"
android:strokeColor="#00000000"
android:fillAlpha="1"/>
<path
android:pathData="m66.624,32.489c0.281,-0.138 1.896,2.465 3.62,5.889 1.724,3.424 2.893,6.354 2.613,6.492 -0.281,0.138 -1.94,-2.551 -3.663,-5.975 -1.724,-3.424 -2.85,-6.268 -2.569,-6.406z"
android:fillColor="#d40000"
android:strokeColor="#00000000"
android:fillAlpha="1"/>
</vector>
+32 -5
View File
@@ -3,18 +3,18 @@
package="de.markusfisch.android.binaryeye"
android:installLocation="auto">
<supports-screens
android:anyDensity="true"
android:largeScreens="true"
android:normalScreens="true"
android:resizeable="true"
android:smallScreens="true"
android:xlargeScreens="true"/>
<uses-permission android:name="android.permission.CAMERA"/>
<uses-permission android:name="android.permission.VIBRATE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE"/>
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
<uses-feature android:name="android.hardware.camera"/>
<uses-feature android:name="android.hardware.camera.autofocus"/>
<application
tools:ignore="UnusedAttribute"
android:name=".app.BinaryEyeApp"
android:allowBackup="true"
android:fullBackupContent="true"
android:supportsRtl="true"
@@ -33,11 +33,15 @@
<activity
android:name=".activity.CameraActivity"
android:label="@string/scan_code">
<intent-filter>
<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"/>
</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"/>
@@ -48,8 +52,31 @@
android:scheme="http"/>
</intent-filter>
</activity>
<activity
android:name=".activity.PickActivity"
android:label="@string/pick_code_to_scan">
<intent-filter android:label="@string/decode_barcode">
<action android:name="android.intent.action.SEND"/>
<category android:name="android.intent.category.DEFAULT"/>
<data android:mimeType="image/*"/>
</intent-filter>
<intent-filter android:label="@string/decode_barcode">
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<data android:mimeType="image/*" android:scheme="file"/>
</intent-filter>
</activity>
<activity
android:name=".activity.MainActivity"
android:label="@string/app_name"/>
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="${applicationId}.provider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths"/>
</provider>
</application>
</manifest>
@@ -0,0 +1,30 @@
package de.markusfisch.android.binaryeye.actions
import de.markusfisch.android.binaryeye.actions.mail.MailAction
import de.markusfisch.android.binaryeye.actions.otpauth.OtpauthAction
import de.markusfisch.android.binaryeye.actions.search.OpenOrSearchAction
import de.markusfisch.android.binaryeye.actions.sms.SmsAction
import de.markusfisch.android.binaryeye.actions.tel.TelAction
import de.markusfisch.android.binaryeye.actions.vtype.vcard.VCardAction
import de.markusfisch.android.binaryeye.actions.vtype.vevent.VEventAction
import de.markusfisch.android.binaryeye.actions.web.WebAction
import de.markusfisch.android.binaryeye.actions.wifi.WifiAction
object ActionRegistry {
val DEFAULT_ACTION: IAction = OpenOrSearchAction
val REGISTRY: Set<IAction> = setOf(
MailAction,
OtpauthAction,
SmsAction,
TelAction,
VCardAction,
VEventAction,
WebAction,
WifiAction
)
fun getAction(data: ByteArray): IAction = REGISTRY.find {
it.canExecuteOn(data)
} ?: DEFAULT_ACTION
}
@@ -0,0 +1,51 @@
package de.markusfisch.android.binaryeye.actions
import android.content.Context
import android.content.Intent
import android.widget.Toast
import de.markusfisch.android.binaryeye.app.execShareIntent
import de.markusfisch.android.binaryeye.app.parseAndNormalizeUri
interface IAction {
val iconResId: Int
val titleResId: Int
fun canExecuteOn(data: ByteArray): Boolean
suspend fun execute(context: Context, data: ByteArray)
}
abstract class IntentAction : IAction {
abstract val errorMsg: Int
final override suspend fun execute(context: Context, data: ByteArray) {
val intent =
createIntent(context, data) ?: return Toast.makeText(
context,
errorMsg,
Toast.LENGTH_LONG
).show()
execShareIntent(context, intent)
}
abstract suspend fun createIntent(context: Context, data: ByteArray): Intent?
}
abstract class SchemeAction : IAction {
abstract val scheme: String
open val intentAction: String = Intent.ACTION_VIEW
open val buildRegex: Boolean = false
final override fun canExecuteOn(data: ByteArray): Boolean {
val content = String(data)
return if (buildRegex) {
content.matches("""^$scheme://[\w\W]+$""".toRegex(RegexOption.IGNORE_CASE))
} else {
content.startsWith("$scheme://", ignoreCase = true)
}
}
final override suspend fun execute(context: Context, data: ByteArray) {
val uri = parseAndNormalizeUri(String(data))
execShareIntent(context, Intent(intentAction, uri))
}
}
@@ -0,0 +1,30 @@
package de.markusfisch.android.binaryeye.actions.mail
import android.content.Context
import android.content.Intent
import android.net.Uri
import de.markusfisch.android.binaryeye.R
import de.markusfisch.android.binaryeye.actions.IntentAction
object MailAction : IntentAction() {
private val mailRegex =
"""^mail(?:to)?:([\w.%+-]+@[A-Za-z\d.-]+\.[A-Za-z]{2,6}(?:[?&](?:subject|body)=[\S\s]*?){0,2})$""".toRegex()
override val iconResId: Int = R.drawable.ic_action_mail
override val titleResId: Int = R.string.mail_send
override val errorMsg: Int = R.string.mail_error
override fun canExecuteOn(data: ByteArray): Boolean {
return String(data).matches(mailRegex)
}
override suspend fun createIntent(context: Context, data: ByteArray): Intent? {
val mailWithMessage = mailRegex.matchEntire(
String(data)
)?.groupValues?.get(1) ?: return null
return Intent(
Intent.ACTION_SENDTO,
Uri.parse("mailto:$mailWithMessage")
)
}
}
@@ -0,0 +1,10 @@
package de.markusfisch.android.binaryeye.actions.otpauth
import de.markusfisch.android.binaryeye.R
import de.markusfisch.android.binaryeye.actions.SchemeAction
object OtpauthAction : SchemeAction() {
override val iconResId: Int = R.drawable.ic_action_otpauth
override val titleResId: Int = R.string.otpauth_add
override val scheme: String = "otpauth"
}
@@ -0,0 +1,59 @@
package de.markusfisch.android.binaryeye.actions.search
import android.content.Context
import android.content.Intent
import android.widget.Toast
import de.markusfisch.android.binaryeye.R
import de.markusfisch.android.binaryeye.actions.IAction
import de.markusfisch.android.binaryeye.app.alertDialog
import de.markusfisch.android.binaryeye.app.execShareIntent
import de.markusfisch.android.binaryeye.app.parseAndNormalizeUri
import de.markusfisch.android.binaryeye.app.prefs
import java.net.URLEncoder
object OpenOrSearchAction : IAction {
override val iconResId: Int = R.drawable.ic_action_search
override val titleResId: Int = R.string.search_web
override fun canExecuteOn(data: ByteArray): Boolean = false
override suspend fun execute(context: Context, data: ByteArray) {
val intent = openUri(context, String(data)) ?: return
execShareIntent(context, intent)
}
private suspend fun openUri(context: Context, data: String, search: Boolean = true): Intent? {
val uri = parseAndNormalizeUri(data)
val intent = Intent(Intent.ACTION_VIEW, uri)
when {
intent.resolveActivity(context.packageManager) != null -> return intent
search -> return getSearchIntent(context, data)
else -> Toast.makeText(
context,
R.string.cannot_resolve_action,
Toast.LENGTH_SHORT
).show()
}
return null
}
private suspend fun getSearchIntent(context: Context, query: String): Intent? {
val names = context.resources.getStringArray(
R.array.search_engines_names
).toMutableList()
val urls = context.resources.getStringArray(
R.array.search_engines_values
).toMutableList()
if (prefs.openWithUrl.isNotEmpty()) {
names.add(prefs.openWithUrl)
urls.add(prefs.openWithUrl)
}
val queryUri = alertDialog<String>(context) { resume ->
setTitle(R.string.pick_search_engine)
setItems(names.toTypedArray()) { _, which ->
resume(urls[which] + URLEncoder.encode(query, "utf-8"))
}
} ?: return null
return openUri(context, queryUri, false)
}
}
@@ -0,0 +1,34 @@
package de.markusfisch.android.binaryeye.actions.sms
import android.content.Context
import android.content.Intent
import android.net.Uri
import de.markusfisch.android.binaryeye.R
import de.markusfisch.android.binaryeye.actions.IntentAction
object SmsAction : IntentAction() {
private val smsRegex =
"""^sms(?:to)?:(\+?[0-9]+)(?::([\S\s]*))?$""".toRegex(RegexOption.IGNORE_CASE)
override val iconResId: Int = R.drawable.ic_action_sms
override val titleResId: Int = R.string.sms_send
override val errorMsg: Int = R.string.sms_error
override fun canExecuteOn(data: ByteArray): Boolean {
return String(data).matches(smsRegex)
}
override suspend fun createIntent(context: Context, data: ByteArray): Intent? {
val (number: String, message: String) = smsRegex.matchEntire(
String(data)
)?.let {
it.groupValues[1] to it.groupValues[2]
} ?: return null
return Intent(
Intent.ACTION_SENDTO,
Uri.parse("smsto:$number")
).apply {
if (message.isNotEmpty()) putExtra("sms_body", message)
}
}
}
@@ -0,0 +1,23 @@
package de.markusfisch.android.binaryeye.actions.tel
import android.content.Context
import android.content.Intent
import de.markusfisch.android.binaryeye.R
import de.markusfisch.android.binaryeye.actions.IntentAction
import de.markusfisch.android.binaryeye.app.parseAndNormalizeUri
object TelAction : IntentAction() {
private val telRegex = """^tel:(\+?[0-9]+)$""".toRegex(RegexOption.IGNORE_CASE)
override val iconResId: Int = R.drawable.ic_action_tel
override val titleResId: Int = R.string.tel_dial
override val errorMsg: Int = R.string.tel_error
override fun canExecuteOn(data: ByteArray): Boolean {
return String(data).matches(telRegex)
}
override suspend fun createIntent(context: Context, data: ByteArray): Intent {
return Intent(Intent.ACTION_DIAL, parseAndNormalizeUri(String(data)))
}
}
@@ -0,0 +1,36 @@
package de.markusfisch.android.binaryeye.actions.vtype
import java.util.*
object VTypeParser {
private const val BACKSLASH_R_LEGACY =
"(?:\u000D\u000A|[\u000A\u000B\u000C\u000D\u0085\u2028\u2029])"
private val vTypeRegex =
"""^BEGIN:(VCARD|VEVENT)(?:$BACKSLASH_R_LEGACY.+?:[\s\S]+?)+?${BACKSLASH_R_LEGACY}END:\1$BACKSLASH_R_LEGACY?$""".toRegex(
RegexOption.IGNORE_CASE
)
private val propertyRegex = """^(.+?):([\s\S]*?)$""".toRegex(RegexOption.MULTILINE)
fun parseVType(data: String): String? =
vTypeRegex.matchEntire(data)?.groupValues?.get(1)?.toUpperCase(Locale.US)
fun parseMap(data: String): Map<String, List<VTypeProperty>> = propertyRegex.findAll(data).map {
it.groupValues[1].split(';').let { typeAndInfo ->
typeAndInfo[0] to VTypeProperty(typeAndInfo.drop(1), it.groupValues[2])
}
}.groupBy({ it.first.toUpperCase(Locale.US) }) { it.second }
}
data class VTypeProperty(
val info: List<String>,
val value: String
) {
val firstTypeOrFirstInfo: String?
get() = this.info.find { it.startsWith("TYPE=", true) }?.let {
typeRegex.matchEntire(it)?.groupValues?.get(1)
} ?: this.info.firstOrNull()
companion object {
private val typeRegex = """^(?:TYPE=)(.+?)[:;]?.*$""".toRegex(RegexOption.IGNORE_CASE)
}
}
@@ -0,0 +1,183 @@
package de.markusfisch.android.binaryeye.actions.vtype.vcard
import android.content.ContentValues
import android.content.Context
import android.content.Intent
import android.os.Build
import android.provider.ContactsContract
import de.markusfisch.android.binaryeye.R
import de.markusfisch.android.binaryeye.actions.IntentAction
import de.markusfisch.android.binaryeye.actions.vtype.VTypeParser
import java.util.*
import kotlin.collections.ArrayList
object VCardAction : IntentAction() {
override val iconResId: Int
get() = R.drawable.ic_action_vcard
override val titleResId: Int
get() = R.string.vcard_add
override val errorMsg: Int
get() = R.string.vcard_failed
override fun canExecuteOn(data: ByteArray): Boolean {
return VTypeParser.parseVType(String(data)) == "VCARD"
}
override suspend fun createIntent(context: Context, data: ByteArray): Intent? {
val info = VTypeParser.parseMap(String(data))
return Intent(Intent.ACTION_INSERT_OR_EDIT).apply {
type = ContactsContract.Contacts.CONTENT_ITEM_TYPE
info["FN"]?.singleOrNull()?.also { name ->
putExtra(ContactsContract.Intents.Insert.NAME, name.value)
} ?: info["N"]?.singleOrNull()?.also { name ->
putExtra(ContactsContract.Intents.Insert.NAME, name.value.nameFormat)
}
info["TEL"]?.filter { it.value.isNotEmpty() }?.forEachIndexed { index, phoneProperty ->
val (extraPhoneType, extraPhone) = when (index) {
0 -> ContactsContract.Intents.Insert.PHONE_TYPE to
ContactsContract.Intents.Insert.PHONE
1 -> ContactsContract.Intents.Insert.SECONDARY_PHONE_TYPE to
ContactsContract.Intents.Insert.SECONDARY_PHONE
2 -> ContactsContract.Intents.Insert.TERTIARY_PHONE_TYPE to
ContactsContract.Intents.Insert.TERTIARY_PHONE_TYPE
else -> return@forEachIndexed
}
phoneProperty.firstTypeOrFirstInfo?.also {
it.phoneType?.also { phoneType ->
putExtra(extraPhoneType, phoneType)
} ?: putExtra(extraPhoneType, it)
}
putExtra(
extraPhone,
phoneProperty.value.toLowerCase(Locale.US).removePrefix("tel:")
)
}
info["EMAIL"]?.forEachIndexed { index, mailProperty ->
val (extraMailType, extraMail) = when (index) {
0 -> ContactsContract.Intents.Insert.EMAIL_TYPE to
ContactsContract.Intents.Insert.EMAIL
1 -> ContactsContract.Intents.Insert.SECONDARY_EMAIL_TYPE to
ContactsContract.Intents.Insert.SECONDARY_EMAIL
2 -> ContactsContract.Intents.Insert.TERTIARY_EMAIL_TYPE to
ContactsContract.Intents.Insert.TERTIARY_EMAIL
else -> return@forEachIndexed
}
mailProperty.firstTypeOrFirstInfo?.also {
it.mailType?.also { mailType ->
putExtra(extraMailType, mailType)
} ?: putExtra(extraMailType, it)
}
putExtra(extraMail, mailProperty.value)
}
info["ORG"]?.singleOrNull()?.also { organization ->
putExtra(ContactsContract.Intents.Insert.COMPANY, organization.value)
}
info["TITLE"]?.singleOrNull()?.also { jobTitle ->
putExtra(ContactsContract.Intents.Insert.JOB_TITLE, jobTitle.value)
}
info["ADR"]?.forEach { addr ->
addr.firstTypeOrFirstInfo?.also {
it.addressType?.also { addressType ->
putExtra(ContactsContract.Intents.Insert.POSTAL_TYPE, addressType)
} ?: putExtra(ContactsContract.Intents.Insert.POSTAL_TYPE, it)
}
putExtra(ContactsContract.Intents.Insert.POSTAL, addr.value.locationFormat)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
val urls = info["URL"]?.mapNotNull {
it.value.takeIf { url -> url.isNotBlank() }?.let { url ->
ContentValues(2).apply {
put(
ContactsContract.Data.MIMETYPE,
ContactsContract.CommonDataKinds.Website.CONTENT_ITEM_TYPE
)
put(ContactsContract.CommonDataKinds.Website.URL, url)
}
}
}
val bDays = info["BDAY"]?.mapNotNull {
it.value.takeIf { day -> day.isNotBlank() }?.let { day ->
ContentValues(3).apply {
put(
ContactsContract.Data.MIMETYPE,
ContactsContract.CommonDataKinds.Event.CONTENT_ITEM_TYPE
)
put(
ContactsContract.CommonDataKinds.Event.TYPE,
ContactsContract.CommonDataKinds.Event.TYPE_BIRTHDAY
)
put(ContactsContract.CommonDataKinds.Event.START_DATE, day)
}
}
}
putParcelableArrayListExtra(
ContactsContract.Intents.Insert.DATA,
ArrayList(listOfNotNull(urls, bDays).flatten())
)
}
}
}
private val String.locationFormat: String
get() = """^([^;]*?);([^;]*?);([^;]*?);([^;]*?);([^;]*?);([^;]*?);([^;]*?)$""".toRegex().matchEntire(
this
)?.groupValues?.let {
listOf(
it[1],
it[2],
it[3],
"${it[4]} ${it[6]}",
"${it[5]} ${it[7]}"
).filter { line -> line.isNotBlank() }
.joinToString("\n").takeIf { location -> location.isNotBlank() }
} ?: this
private val String.nameFormat: String
get() = """^([^;]*?);([^;]*?);([^;]*?);([^;]*?);([^;]*?)$""".toRegex().matchEntire(this)?.groupValues?.let {
listOf(it[4], it[2], it[3], it[1], it[5]).joinToString(" ")
.takeIf { name -> name.isNotBlank() }
} ?: this
private val String.mailType: Int?
get() = when (this.toUpperCase(Locale.US)) {
"HOME" -> ContactsContract.CommonDataKinds.Email.TYPE_HOME
"WORK" -> ContactsContract.CommonDataKinds.Email.TYPE_WORK
"OTHER" -> ContactsContract.CommonDataKinds.Email.TYPE_OTHER
"MOBILE" -> ContactsContract.CommonDataKinds.Email.TYPE_MOBILE
else -> null
}
private val String.phoneType: Int?
get() = when (this.toUpperCase(Locale.US)) {
"HOME" -> ContactsContract.CommonDataKinds.Phone.TYPE_HOME
"MOBILE", "CELL" -> ContactsContract.CommonDataKinds.Phone.TYPE_MOBILE
"WORK" -> ContactsContract.CommonDataKinds.Phone.TYPE_WORK
"FAX_WORK" -> ContactsContract.CommonDataKinds.Phone.TYPE_FAX_WORK
"FAX_HOME" -> ContactsContract.CommonDataKinds.Phone.TYPE_FAX_HOME
"PAGER" -> ContactsContract.CommonDataKinds.Phone.TYPE_PAGER
"OTHER" -> ContactsContract.CommonDataKinds.Phone.TYPE_OTHER
"CALLBACK" -> ContactsContract.CommonDataKinds.Phone.TYPE_CALLBACK
"CAR" -> ContactsContract.CommonDataKinds.Phone.TYPE_CAR
"COMPANY_MAIN" -> ContactsContract.CommonDataKinds.Phone.TYPE_COMPANY_MAIN
"ISDN" -> ContactsContract.CommonDataKinds.Phone.TYPE_ISDN
"MAIN" -> ContactsContract.CommonDataKinds.Phone.TYPE_MAIN
"OTHER_FAX" -> ContactsContract.CommonDataKinds.Phone.TYPE_OTHER_FAX
"RADIO" -> ContactsContract.CommonDataKinds.Phone.TYPE_RADIO
"TELEX" -> ContactsContract.CommonDataKinds.Phone.TYPE_TELEX
"TTY_TDD" -> ContactsContract.CommonDataKinds.Phone.TYPE_TTY_TDD
"WORK_MOBILE" -> ContactsContract.CommonDataKinds.Phone.TYPE_WORK_MOBILE
"WORK_PAGER" -> ContactsContract.CommonDataKinds.Phone.TYPE_WORK_PAGER
"ASSISTANT" -> ContactsContract.CommonDataKinds.Phone.TYPE_ASSISTANT
"MMS" -> ContactsContract.CommonDataKinds.Phone.TYPE_MMS
else -> null
}
private val String.addressType: Int?
get() = when (this.toUpperCase(Locale.US)) {
"HOME" -> ContactsContract.CommonDataKinds.StructuredPostal.TYPE_HOME
"WORK" -> ContactsContract.CommonDataKinds.StructuredPostal.TYPE_WORK
"OTHER" -> ContactsContract.CommonDataKinds.StructuredPostal.TYPE_OTHER
else -> null
}
}
@@ -0,0 +1,78 @@
package de.markusfisch.android.binaryeye.actions.vtype.vevent
import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
import android.os.Build
import android.provider.CalendarContract
import android.support.annotation.RequiresApi
import de.markusfisch.android.binaryeye.R
import de.markusfisch.android.binaryeye.actions.IntentAction
import de.markusfisch.android.binaryeye.actions.vtype.VTypeParser
import java.text.SimpleDateFormat
import java.util.*
object VEventAction : IntentAction() {
override val iconResId: Int
get() = R.drawable.ic_action_vevent
override val titleResId: Int
get() = R.string.vevent_add
override val errorMsg: Int
get() = R.string.vevent_failed
override fun canExecuteOn(data: ByteArray): Boolean {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH
&& VTypeParser.parseVType(String(data)) == "VEVENT"
}
@RequiresApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
override suspend fun createIntent(context: Context, data: ByteArray): Intent? {
val info = VTypeParser.parseMap(String(data))
return Intent(Intent.ACTION_EDIT).apply {
type = "vnd.android.cursor.item/event"
info["SUMMARY"]?.singleOrNull()?.also { title ->
putExtra(CalendarContract.Events.TITLE, title.value)
}
info["DESCRIPTION"]?.singleOrNull()?.also { description ->
putExtra(CalendarContract.Events.DESCRIPTION, description.value)
}
info["LOCATION"]?.singleOrNull()?.also { location ->
putExtra(CalendarContract.Events.EVENT_LOCATION, location.value)
}
info["DTSTART"]?.singleOrNull()?.also { eventStart ->
dateFormats.simpleFindParse(eventStart.value)?.also {
putExtra(CalendarContract.EXTRA_EVENT_BEGIN_TIME, it.time)
}
}
info["DTEND"]?.singleOrNull()?.also { eventEnd ->
dateFormats.simpleFindParse(eventEnd.value)?.also {
putExtra(CalendarContract.EXTRA_EVENT_END_TIME, it.time)
}
}
}
}
@SuppressLint("SimpleDateFormat") // we definitely don't wan't the local format
private val dateFormats = listOf(
SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"),
SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss"),
SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'"),
SimpleDateFormat("yyyyMMdd'T'HHmmss"),
SimpleDateFormat("yyyy-MM-dd"),
SimpleDateFormat("yyyyMMdd")
)
private fun List<SimpleDateFormat>.simpleFindParse(data: String): Date? {
for (simpleDataFormat in this) {
return simpleDataFormat.simpleParse(data) ?: continue
}
return null
}
private fun SimpleDateFormat.simpleParse(data: String): Date? = try {
this.parse(data)
} catch (e: Exception) {
null
}
}
@@ -0,0 +1,11 @@
package de.markusfisch.android.binaryeye.actions.web
import de.markusfisch.android.binaryeye.R
import de.markusfisch.android.binaryeye.actions.SchemeAction
object WebAction : SchemeAction() {
override val iconResId: Int = R.drawable.ic_action_open
override val titleResId: Int = R.string.open_url
override val scheme: String = "https?"
override val buildRegex: Boolean = true
}
@@ -0,0 +1,90 @@
package de.markusfisch.android.binaryeye.actions.wifi
import android.content.Context
import android.content.Context.WIFI_SERVICE
import android.net.wifi.WifiConfiguration
import android.net.wifi.WifiManager
import android.widget.Toast
import de.markusfisch.android.binaryeye.R
import de.markusfisch.android.binaryeye.actions.IAction
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeoutOrNull
object WifiAction : IAction {
override val iconResId = R.drawable.ic_action_wifi
override val titleResId = R.string.connect_to_wifi
override fun canExecuteOn(data: ByteArray): Boolean =
WifiConfigurationFactory.parse(String(data)) != null
override suspend fun execute(context: Context, data: ByteArray) = withContext(Dispatchers.IO) {
val wifiConfig = WifiConfigurationFactory.parse(
String(data)
) ?: return@withContext
val wifiManager = context.applicationContext.getSystemService(
WIFI_SERVICE
) as WifiManager
wifiManager.enableWifi(context)
wifiManager.mayRemoveOldNetwork(wifiConfig)
wifiManager.enableNewNetwork(wifiConfig)
withContext(Dispatchers.Main) {
Toast.makeText(
context,
R.string.wifi_added,
Toast.LENGTH_LONG
).show()
}
}
private suspend fun WifiManager.enableWifi(context: Context): Boolean {
if (!isWifiEnabled) {
if (!setWifiEnabled(true)) {
withContext(Dispatchers.Main) {
Toast.makeText(
context,
R.string.wifi_config_failed,
Toast.LENGTH_LONG
).show()
}
return false
}
// wait for wifi to really be enabled, takes some time
withTimeoutOrNull(10000) {
while (!isWifiEnabled) delay(500)
} ?: if (!isWifiEnabled) {
withContext(Dispatchers.Main) {
Toast.makeText(
context,
R.string.wifi_config_failed,
Toast.LENGTH_LONG
).show()
}
return false
}
}
return true
}
private suspend fun WifiManager.mayRemoveOldNetwork(
wifiConfig: WifiConfiguration
) {
configuredNetworks?.firstOrNull {
it.SSID == wifiConfig.SSID &&
it.allowedKeyManagement == wifiConfig.allowedKeyManagement
}?.networkId?.also {
removeNetwork(it)
}
}
private suspend fun WifiManager.enableNewNetwork(
wifiConfig: WifiConfiguration
) {
val id = addNetwork(wifiConfig)
disconnect()
enableNetwork(id, true)
reconnect()
}
}
@@ -0,0 +1,212 @@
package de.markusfisch.android.binaryeye.actions.wifi
import android.net.wifi.WifiConfiguration
import android.net.wifi.WifiEnterpriseConfig
import android.os.Build
import java.util.*
/**
* Normal:
* WIFI:S:[network SSID];T:<WPA|WEP|nopass|>;P:[network password];H:<true|false|>;;
*
* WPA2 enterprise (EAP):
* WIFI:S:[network SSID];T:WPA2-EAP;H:<true|false|nopass|>;E:[EAP method];PH2:[Phase 2 method];AI:[anonymous identity];I:[identity];P:[password];;
*
* EPA methods:
* "AKA", "AKA_PRIME",
* "NONE", "PEAP",
* "PWD", "SIM",
* "TLS", "TTLS",
* "UNAUTH_TLS"
* https://developer.android.com/reference/android/net/wifi/WifiEnterpriseConfig.Eap.html
*
* Phase 2 methods:
* "AKA", "AKA_PRIME",
* "GTC", "MSCHAP",
* "MSCHAPV2", "NONE",
* "PAP", "SIM"
* https://developer.android.com/reference/android/net/wifi/WifiEnterpriseConfig.Phase2.html
*
* The fields can appear in any order. Only "S:" is required.
*/
object WifiConfigurationFactory {
fun parse(input: String): WifiConfiguration? {
val inputMap = parseMap(input) ?: return null
val parsedData = SimpleDataAccessor.of(inputMap) ?: return null
return WifiConfiguration().apply(parsedData)
}
internal fun parseMap(string: String): Map<String, String>? {
// normally those codes should have the last semicolon, but many
// generators don't add it
val wifiRegex = """^WIFI:((?:.+?:(?:[^\\;]|\\.)*;)+);?$""".toRegex()
// should be: ^(.+):((?:[^\\;,":]|\\.)*);$ but allows unescaped , "
// and : because many qr creator doesn't escape
val pairRegex = """(.+?):((?:[^\\;]|\\.)*);""".toRegex()
return wifiRegex.matchEntire(
string
)?.groupValues?.get(1)?.let { pairs ->
pairRegex.findAll(pairs).map { pair ->
pair.groupValues[1].toUpperCase(Locale.US) to pair.groupValues[2]
}.toMap()
}
}
internal class SimpleDataAccessor private constructor(
private val inputMap: Map<String, String>
) {
private val hexRegex = """^[0-9a-f]+$""".toRegex(
RegexOption.IGNORE_CASE
)
// keep possibility of wrongly not escaped \ by explicitly searching
// for special chars
private val escapedRegex = """\\([\\;,":])""".toRegex()
// this should be private but because of testing it isn't possible
internal val ssid: String
get() = inputMap.getValue("S").quotedUnlessHex.unescaped
internal val securityType: String
get() = inputMap["T"]?.unescaped ?: ""
internal val password: String?
get() = inputMap["P"]?.quotedUnlessHex?.unescaped
internal val hidden: Boolean
get() = inputMap["H"]?.unescaped == "true"
internal val anonymousIdentity: String
get() = inputMap["AI"]?.unescaped ?: ""
internal val identity: String
get() = inputMap["I"]?.unescaped ?: ""
internal val eapMethod: Int?
get() = if (inputMap["E"].isNullOrEmpty()) {
requireSdk(Build.VERSION_CODES.JELLY_BEAN_MR2) {
WifiEnterpriseConfig.Eap.NONE
}
} else when (inputMap["E"]) {
"AKA" -> requireSdk(Build.VERSION_CODES.LOLLIPOP) { WifiEnterpriseConfig.Eap.AKA }
"AKA_PRIME" -> requireSdk(Build.VERSION_CODES.M) { WifiEnterpriseConfig.Eap.AKA_PRIME }
"NONE" -> requireSdk(Build.VERSION_CODES.JELLY_BEAN_MR2) { WifiEnterpriseConfig.Eap.NONE }
"PEAP" -> requireSdk(Build.VERSION_CODES.JELLY_BEAN_MR2) { WifiEnterpriseConfig.Eap.PEAP }
"PWD" -> requireSdk(Build.VERSION_CODES.JELLY_BEAN_MR2) { WifiEnterpriseConfig.Eap.PWD }
"SIM" -> requireSdk(Build.VERSION_CODES.LOLLIPOP) { WifiEnterpriseConfig.Eap.SIM }
"TLS" -> requireSdk(Build.VERSION_CODES.JELLY_BEAN_MR2) { WifiEnterpriseConfig.Eap.TLS }
"TTLS" -> requireSdk(Build.VERSION_CODES.JELLY_BEAN_MR2) { WifiEnterpriseConfig.Eap.TTLS }
"UNAUTH_TLS" -> requireSdk(Build.VERSION_CODES.N) { WifiEnterpriseConfig.Eap.UNAUTH_TLS }
else -> null
}
internal val phase2Method: Int?
get() = if (inputMap["PH2"].isNullOrEmpty()) {
requireSdk(Build.VERSION_CODES.JELLY_BEAN_MR2) {
WifiEnterpriseConfig.Phase2.NONE
}
} else when (inputMap["PH2"]) {
"AKA" -> requireSdk(Build.VERSION_CODES.O) { WifiEnterpriseConfig.Phase2.AKA }
"AKA_PRIME" -> requireSdk(Build.VERSION_CODES.O) { WifiEnterpriseConfig.Phase2.AKA_PRIME }
"GTC" -> requireSdk(Build.VERSION_CODES.JELLY_BEAN_MR2) { WifiEnterpriseConfig.Phase2.GTC }
"MSCHAP" -> requireSdk(Build.VERSION_CODES.JELLY_BEAN_MR2) { WifiEnterpriseConfig.Phase2.MSCHAP }
"MSCHAPV2" -> requireSdk(Build.VERSION_CODES.JELLY_BEAN_MR2) { WifiEnterpriseConfig.Phase2.MSCHAPV2 }
"NONE" -> requireSdk(Build.VERSION_CODES.JELLY_BEAN_MR2) { WifiEnterpriseConfig.Phase2.NONE }
"PAP" -> requireSdk(Build.VERSION_CODES.JELLY_BEAN_MR2) { WifiEnterpriseConfig.Phase2.PAP }
"SIM" -> requireSdk(Build.VERSION_CODES.O) { WifiEnterpriseConfig.Phase2.SIM }
else -> null
}
private val String.unescaped: String
get() = this.replace(escapedRegex) { escaped ->
escaped.groupValues[1]
}
private val String.quotedUnlessHex: String
get() = if (matches(hexRegex) || (startsWith("\"") &&
endsWith("\""))
) this else "\"$this\""
internal companion object {
internal fun of(inputMap: Map<String, String>): SimpleDataAccessor? {
return SimpleDataAccessor(inputMap).takeUnless {
inputMap["S"].isNullOrEmpty()
}
}
}
}
private fun WifiConfiguration.apply(
data: SimpleDataAccessor
): WifiConfiguration? {
fun WifiConfiguration.applyCommon(
data: SimpleDataAccessor
): WifiConfiguration? {
allowedAuthAlgorithms.clear()
allowedGroupCiphers.clear()
allowedKeyManagement.clear()
allowedPairwiseCiphers.clear()
allowedProtocols.clear()
SSID = data.ssid
hiddenSSID = data.hidden
return this
}
fun WifiConfiguration.applySecurity(
data: SimpleDataAccessor
): WifiConfiguration? {
when (data.securityType) {
"", "nopass" -> allowedKeyManagement.set(WifiConfiguration.KeyMgmt.NONE)
"WEP" -> @Suppress("DEPRECATION") /* WEP as insecure */ {
data.password?.also {
wepKeys[0] = it
} ?: return null
wepTxKeyIndex = 0
allowedAuthAlgorithms.set(WifiConfiguration.AuthAlgorithm.SHARED)
allowedKeyManagement.set(WifiConfiguration.KeyMgmt.NONE)
allowedGroupCiphers.set(WifiConfiguration.GroupCipher.TKIP)
allowedGroupCiphers.set(WifiConfiguration.GroupCipher.CCMP)
allowedGroupCiphers.set(WifiConfiguration.GroupCipher.WEP40)
allowedGroupCiphers.set(WifiConfiguration.GroupCipher.WEP104)
}
"WPA" -> {
data.password?.also {
preSharedKey = it
} ?: return null
allowedAuthAlgorithms.set(WifiConfiguration.AuthAlgorithm.OPEN)
@Suppress("DEPRECATION") // WPA 1 is insecure and has bad performance
allowedProtocols.set(WifiConfiguration.Protocol.WPA) // WPA
allowedProtocols.set(WifiConfiguration.Protocol.RSN) // WPA2
allowedKeyManagement.set(WifiConfiguration.KeyMgmt.WPA_PSK)
allowedKeyManagement.set(WifiConfiguration.KeyMgmt.WPA_EAP)
@Suppress("DEPRECATION") // TKIP is insecure and has bad performance
allowedPairwiseCiphers.set(WifiConfiguration.PairwiseCipher.TKIP)
allowedPairwiseCiphers.set(WifiConfiguration.PairwiseCipher.CCMP)
allowedGroupCiphers.set(WifiConfiguration.GroupCipher.TKIP)
allowedGroupCiphers.set(WifiConfiguration.GroupCipher.CCMP)
}
"WPA2-EAP" -> requireSdk(Build.VERSION_CODES.JELLY_BEAN_MR2) {
data.password?.also {
preSharedKey = it
} ?: return null
allowedAuthAlgorithms.set(WifiConfiguration.AuthAlgorithm.OPEN)
allowedProtocols.set(WifiConfiguration.Protocol.RSN) // WPA2
allowedKeyManagement.set(WifiConfiguration.KeyMgmt.WPA_EAP)
@Suppress("DEPRECATION") // TKIP is insecure and has bad performance
allowedPairwiseCiphers.set(WifiConfiguration.PairwiseCipher.TKIP)
allowedPairwiseCiphers.set(WifiConfiguration.PairwiseCipher.CCMP)
allowedGroupCiphers.set(WifiConfiguration.GroupCipher.TKIP)
allowedGroupCiphers.set(WifiConfiguration.GroupCipher.CCMP)
enterpriseConfig.identity = data.identity
enterpriseConfig.anonymousIdentity = data.anonymousIdentity
enterpriseConfig.password = data.password
enterpriseConfig.eapMethod =
data.eapMethod ?: return null // non valid eapMethod
enterpriseConfig.phase2Method =
data.phase2Method ?: return null // non valid phase2Method
} ?: return null // api isn't high enough
}
return this
}
return this.applyCommon(data)?.applySecurity(data)
}
private inline fun <T> requireSdk(version: Int, block: () -> T): T? {
return if (Build.VERSION.SDK_INT >= version) return block() else null
}
}
@@ -1,78 +1,79 @@
package de.markusfisch.android.binaryeye.activity
import com.google.zxing.Result
import de.markusfisch.android.cameraview.widget.CameraView
import de.markusfisch.android.binaryeye.app.initSystemBars
import de.markusfisch.android.binaryeye.rs.YuvToGray
import de.markusfisch.android.binaryeye.zxing.Zxing
import de.markusfisch.android.binaryeye.R
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.content.SharedPreferences
import android.hardware.Camera
import android.net.Uri
import android.os.Bundle
import android.os.Vibrator
import android.preference.PreferenceManager
import android.support.v4.app.ActivityCompat
import android.support.v4.content.ContextCompat
import android.support.v7.app.AppCompatActivity
import android.support.v7.widget.Toolbar
import android.view.Menu
import android.view.MenuItem
import android.view.MotionEvent
import android.view.View
import android.widget.SeekBar
import android.widget.Toast
import com.google.zxing.Result
import com.google.zxing.ResultMetadataType
import de.markusfisch.android.binaryeye.R
import de.markusfisch.android.binaryeye.app.db
import de.markusfisch.android.binaryeye.app.hasCameraPermission
import de.markusfisch.android.binaryeye.app.initSystemBars
import de.markusfisch.android.binaryeye.app.prefs
import de.markusfisch.android.binaryeye.rs.Preprocessor
import de.markusfisch.android.binaryeye.zxing.Zxing
import de.markusfisch.android.cameraview.widget.CameraView
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
class CameraActivity : AppCompatActivity() {
companion object {
private val REQUEST_CAMERA = 1
private val ZOOM_MAX = "zoom_max"
private val ZOOM_LEVEL = "zoom_level"
}
private val zxing = Zxing()
private val decodingRunnable = Runnable {
while (decoding) {
val result = decodeFrame()
if (result != null) {
cameraView.post { found(result) }
decoding = false
break
private lateinit var vibrator: Vibrator
private lateinit var cameraView: CameraView
private lateinit var zoomBar: SeekBar
private lateinit var flashFab: View
private var preprocessor: Preprocessor? = null
private var invert = false
private var flash = false
private var frontFacing = false
private var fallbackBuffer: IntArray? = null
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<String>,
grantResults: IntArray
) {
when (requestCode) {
REQUEST_CAMERA -> if (grantResults.isNotEmpty() &&
grantResults[0] != PackageManager.PERMISSION_GRANTED
) {
Toast.makeText(
this,
R.string.no_camera_no_fun,
Toast.LENGTH_SHORT
).show()
finish()
}
}
}
private lateinit var preferences: SharedPreferences
private lateinit var vibrator: Vibrator
private lateinit var cameraView: CameraView
private lateinit var zoomBar: SeekBar
private lateinit var yuvToGray: YuvToGray
private var decoding = false
private var decodingThread: Thread? = null
private var frameData: ByteArray? = null
private var frameWidth: Int = 0
private var frameHeight: Int = 0
private var frameOrientation: Int = 0
private var odd = false
private var flash = false
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<String>,
grantResults: IntArray) {
override fun onActivityResult(
requestCode: Int,
resultCode: Int,
resultData: Intent?
) {
when (requestCode) {
REQUEST_CAMERA -> if (grantResults.size > 0 &&
grantResults[0] != PackageManager.PERMISSION_GRANTED) {
Toast.makeText(this, R.string.no_camera_no_fun,
Toast.LENGTH_SHORT).show()
finish()
PICK_FILE_RESULT_CODE -> {
if (resultCode == Activity.RESULT_OK && resultData != null) {
val pick = Intent(this, PickActivity::class.java)
pick.action = Intent.ACTION_VIEW
pick.setDataAndType(resultData.data, "image/*")
startActivity(pick)
}
}
}
}
@@ -84,44 +85,57 @@ class CameraActivity : AppCompatActivity() {
initSystemBars(this)
setSupportActionBar(findViewById(R.id.toolbar) as Toolbar)
preferences = PreferenceManager.getDefaultSharedPreferences(this)
vibrator = getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
yuvToGray = YuvToGray(this)
cameraView = findViewById(R.id.camera_view) as CameraView
zoomBar = findViewById(R.id.zoom) as SeekBar
flashFab = findViewById(R.id.flash)
flashFab.setOnClickListener { toggleTorchMode() }
initCameraView()
setTapToFocus()
initZoomBar()
restoreZoom()
initFlashFab(findViewById(R.id.flash))
if (intent?.action == Intent.ACTION_SEND &&
intent.type == "text/plain"
) {
handleSendText(intent)
}
}
override fun onDestroy() {
super.onDestroy()
yuvToGray.destroy()
preprocessor?.destroy()
fallbackBuffer = null
saveZoom()
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
handleSendText(intent)
}
override fun onResume() {
super.onResume()
System.gc()
if (hasCameraPermission()) {
cameraView.openAsync(CameraView.findCameraId(
Camera.CameraInfo.CAMERA_FACING_BACK))
startDecoding()
if (hasCameraPermission(this, REQUEST_CAMERA)) {
openCamera()
}
}
private fun openCamera() {
cameraView.openAsync(
CameraView.findCameraId(
if (frontFacing) {
Camera.CameraInfo.CAMERA_FACING_FRONT
} else {
Camera.CameraInfo.CAMERA_FACING_BACK
}
)
)
}
override fun onPause() {
super.onPause()
cancelDecoding()
closeCamera()
}
private fun closeCamera() {
cameraView.close()
}
@@ -148,6 +162,30 @@ class CameraActivity : AppCompatActivity() {
startActivity(MainActivity.getEncodeIntent(this))
true
}
R.id.history -> {
startActivity(MainActivity.getHistoryIntent(this))
true
}
R.id.pick_file -> {
val chooseFile = Intent(Intent.ACTION_GET_CONTENT)
chooseFile.type = "image/*"
startActivityForResult(
Intent.createChooser(
chooseFile,
getString(R.string.pick_file)
),
PICK_FILE_RESULT_CODE
)
true
}
R.id.switch_camera -> {
switchCamera()
true
}
R.id.preferences -> {
startActivity(MainActivity.getPreferencesIntent(this))
true
}
R.id.info -> {
openReadme()
true
@@ -156,76 +194,95 @@ class CameraActivity : AppCompatActivity() {
}
}
private fun switchCamera() {
closeCamera()
frontFacing = frontFacing xor true
openCamera()
}
private fun openReadme() {
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(
"https://github.com/markusfisch/BinaryEye/blob/master/README.md")))
startActivity(
Intent(
Intent.ACTION_VIEW, Uri.parse(
"https://github.com/markusfisch/BinaryEye/blob/master/README.md"
)
)
)
}
private fun handleSendText(intent: Intent) {
if (!Intent.ACTION_SEND.equals(intent.getAction()) ||
!"text/plain".equals(intent.getType())) {
return
val text = intent.getStringExtra(Intent.EXTRA_TEXT)
if (text?.isEmpty() == false) {
startActivity(MainActivity.getEncodeIntent(this, text, true))
finish()
}
var text = intent.getStringExtra(Intent.EXTRA_TEXT)
if (text == null || text.isEmpty()) {
return
}
// consume this intent
intent.setAction(null)
startActivity(MainActivity.getEncodeIntent(this, text))
}
private fun hasCameraPermission(): Boolean {
val permission = android.Manifest.permission.CAMERA
if (ContextCompat.checkSelfPermission(this, permission) !=
PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this, arrayOf(permission),
REQUEST_CAMERA)
return false
}
return true
}
private fun initCameraView() {
cameraView.setUseOrientationListener(true)
cameraView.setTapToFocus()
cameraView.setOnCameraListener(object : CameraView.OnCameraListener {
override fun onConfigureParameters(
parameters: Camera.Parameters) {
if (parameters.isZoomSupported()) {
val max = parameters.getMaxZoom()
parameters: Camera.Parameters
) {
if (parameters.isZoomSupported) {
val max = parameters.maxZoom
if (zoomBar.max != max) {
zoomBar.max = max
zoomBar.progress = max / 3
zoomBar.progress = max / 10
saveZoom()
}
parameters.setZoom(zoomBar.progress)
parameters.zoom = zoomBar.progress
} else {
zoomBar.visibility = View.GONE
}
for (mode in parameters.getSupportedSceneModes()) {
if (mode.equals(Camera.Parameters.SCENE_MODE_BARCODE)) {
parameters.setSceneMode(mode)
break
val sceneModes = parameters.supportedSceneModes
sceneModes?.let {
for (mode in sceneModes) {
if (mode == Camera.Parameters.SCENE_MODE_BARCODE) {
parameters.sceneMode = mode
break
}
}
}
CameraView.setAutoFocus(parameters)
updateFlashFab(parameters.flashMode == null)
}
override fun onCameraError() {
Toast.makeText(this@CameraActivity, R.string.camera_error,
Toast.LENGTH_SHORT).show()
Toast.makeText(
this@CameraActivity,
R.string.camera_error,
Toast.LENGTH_SHORT
).show()
finish()
}
override fun onCameraStarted(camera: Camera) {
frameWidth = cameraView.frameWidth
frameHeight = cameraView.frameHeight
frameOrientation = cameraView.frameOrientation
camera.setPreviewCallback { data, _ -> frameData = data }
override fun onCameraReady(camera: Camera) {
val frameWidth = cameraView.frameWidth
val frameHeight = cameraView.frameHeight
val frameOrientation = cameraView.frameOrientation
var decoding = true
camera.setPreviewCallback { frameData, _ ->
if (decoding) {
val result = decodeFrame(
frameData,
frameWidth,
frameHeight,
frameOrientation
)
result?.let {
cameraView.post {
vibrator.vibrate(100)
showResult(this@CameraActivity, result)
}
decoding = false
}
}
}
}
override fun onPreviewStarted(camera: Camera) {
}
override fun onCameraStopping(camera: Camera) {
@@ -234,29 +291,14 @@ class CameraActivity : AppCompatActivity() {
})
}
private fun setTapToFocus() {
val runnable = Runnable {
cameraView.setFocusArea(null)
}
cameraView.setOnTouchListener({ v: View, event: MotionEvent ->
val camera = cameraView.getCamera()
camera?.cancelAutoFocus()
cameraView.setFocusArea(cameraView.calculateFocusRect(
event.getX(), event.getY(), 100))
camera?.autoFocus({ _: Boolean, _: Camera ->
cameraView.removeCallbacks(runnable)
cameraView.postDelayed(runnable, 3000)
})
v.performClick()
true
})
}
private fun initZoomBar() {
zoomBar.setOnSeekBarChangeListener(object :
SeekBar.OnSeekBarChangeListener {
override fun onProgressChanged(seekBar: SeekBar, progress: Int,
fromUser: Boolean) {
SeekBar.OnSeekBarChangeListener {
override fun onProgressChanged(
seekBar: SeekBar,
progress: Int,
fromUser: Boolean
) {
setZoom(progress)
}
@@ -267,12 +309,12 @@ class CameraActivity : AppCompatActivity() {
}
private fun setZoom(zoom: Int) {
val camera: Camera? = cameraView.getCamera()
val camera: Camera? = cameraView.camera
camera?.let {
try {
val params = camera.getParameters()
params.setZoom(zoom)
camera.setParameters(params)
val params = camera.parameters
params.zoom = zoom
camera.parameters = params
} catch (e: RuntimeException) {
// ignore; there's nothing we can do
}
@@ -280,72 +322,164 @@ class CameraActivity : AppCompatActivity() {
}
private fun saveZoom() {
val editor = preferences.edit()
val editor = prefs.preferences.edit()
editor.putInt(ZOOM_MAX, zoomBar.max)
editor.putInt(ZOOM_LEVEL, zoomBar.progress)
editor.apply()
}
private fun restoreZoom() {
zoomBar.max = preferences.getInt(ZOOM_MAX, zoomBar.max)
zoomBar.progress = preferences.getInt(ZOOM_LEVEL, zoomBar.progress)
zoomBar.max = prefs.preferences.getInt(ZOOM_MAX, zoomBar.max)
zoomBar.progress = prefs.preferences.getInt(
ZOOM_LEVEL,
zoomBar.progress
)
}
private fun initFlashFab(fab: View) {
if (!packageManager.hasSystemFeature(
PackageManager.FEATURE_CAMERA_FLASH)) {
fab.visibility = View.GONE
private fun updateFlashFab(available: Boolean) {
flashFab.visibility = if (available) {
View.GONE
} else {
fab.setOnClickListener { _ ->
toggleTorchMode()
}
View.VISIBLE
}
}
private fun toggleTorchMode() {
val camera = cameraView.getCamera()
val parameters = camera?.getParameters()
parameters?.setFlashMode(if (flash)
val camera = cameraView.camera
val parameters = camera?.parameters
parameters?.flashMode = if (flash) {
Camera.Parameters.FLASH_MODE_OFF
else
Camera.Parameters.FLASH_MODE_TORCH)
flash = flash xor true
parameters?.let { camera.setParameters(parameters) }
}
private fun startDecoding() {
frameData = null
decoding = true
decodingThread = Thread(decodingRunnable)
decodingThread?.start()
}
private fun cancelDecoding() {
decoding = false
if (decodingThread != null) {
var retry = 100
while (retry-- > 0) {
try {
decodingThread?.join()
break
} catch (e: InterruptedException) {
decodingThread?.interrupt()
}
} else {
Camera.Parameters.FLASH_MODE_TORCH
}
parameters?.let {
try {
camera.parameters = parameters
flash = flash xor true
} catch (e: RuntimeException) {
// ignore; there's nothing we can do
}
}
}
private fun decodeFrame(): Result? {
private fun decodeFrame(
frameData: ByteArray?,
frameWidth: Int,
frameHeight: Int,
frameOrientation: Int
): Result? {
frameData ?: return null
return zxing.decodeBitmap(yuvToGray.convert(
frameData!!, frameWidth, frameHeight, frameOrientation))
invert = invert xor true
val pp = preprocessor ?: Preprocessor(
this,
frameWidth,
frameHeight,
frameOrientation
)
pp.process(frameData)
preprocessor = pp
return zxing.decode(
frameData,
pp.outWidth,
pp.outHeight,
invert
)
}
private fun found(result: Result) {
cancelDecoding()
vibrator.vibrate(100)
startActivity(MainActivity.getDecodeIntent(this, result.text,
result.getBarcodeFormat()))
companion object {
private const val REQUEST_CAMERA = 1
private const val PICK_FILE_RESULT_CODE = 1
private const val ZOOM_MAX = "zoom_max"
private const val ZOOM_LEVEL = "zoom_level"
}
}
fun showResult(
activity: Activity,
result: Result,
isResult: Boolean = false
) {
val rawBytes = getRawBytes(result)
if (prefs.useHistory) {
GlobalScope.launch {
db.insertScan(
System.currentTimeMillis(),
result.text,
rawBytes,
result.barcodeFormat.toString()
)
}
}
if (isResult) {
activity.setResult(Activity.RESULT_OK, getReturnIntent(result))
activity.finish()
} else {
activity.startActivity(
MainActivity.getDecodeIntent(
activity,
result.text,
result.barcodeFormat,
rawBytes
)
)
}
}
fun getRawBytes(result: Result): ByteArray? {
val metadata = result.resultMetadata ?: return null
val segments = metadata[ResultMetadataType.BYTE_SEGMENTS] ?: return null
var bytes = ByteArray(0)
@Suppress("UNCHECKED_CAST")
for (seg in segments as Iterable<ByteArray>) {
bytes += seg
}
// byte segments can never be shorter than the text.
// Zxing cuts off content prefixes like "WIFI:"
return if (bytes.size >= result.text.length) bytes else null
}
fun getReturnIntent(result: Result): Intent {
val intent = Intent()
intent.putExtra("SCAN_RESULT", result.text)
intent.putExtra(
"SCAN_RESULT_FORMAT",
result.barcodeFormat.toString()
)
if (result.rawBytes?.isNotEmpty() == true) {
intent.putExtra("SCAN_RESULT_BYTES", result.rawBytes)
}
val metadata = result.resultMetadata
metadata?.let {
val orientation = metadata[ResultMetadataType.UPC_EAN_EXTENSION]
orientation?.let {
intent.putExtra(
"SCAN_RESULT_ORIENTATION",
orientation.toString()
)
}
val ecLevel = metadata[ResultMetadataType.ERROR_CORRECTION_LEVEL]
ecLevel?.let {
intent.putExtra(
"SCAN_RESULT_ERROR_CORRECTION_LEVEL",
ecLevel.toString()
)
}
val upcEanExtension = metadata[ResultMetadataType.UPC_EAN_EXTENSION]
upcEanExtension?.let {
intent.putExtra(
"SCAN_RESULT_UPC_EAN_EXTENSION",
upcEanExtension.toString()
)
}
val segments = metadata[ResultMetadataType.BYTE_SEGMENTS]
segments?.let {
var i = 0
@Suppress("UNCHECKED_CAST")
for (seg in segments as Iterable<ByteArray>) {
intent.putExtra("SCAN_RESULT_BYTE_SEGMENTS_$i", seg)
++i
}
}
}
return intent
}
@@ -1,41 +1,22 @@
package de.markusfisch.android.binaryeye.activity
import com.google.zxing.BarcodeFormat
import de.markusfisch.android.binaryeye.app.initSystemBars
import de.markusfisch.android.binaryeye.app.setFragment
import de.markusfisch.android.binaryeye.fragment.DecodeFragment
import de.markusfisch.android.binaryeye.fragment.EncodeFragment
import de.markusfisch.android.binaryeye.R
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.support.v4.app.Fragment
import android.support.v7.app.AppCompatActivity
import android.support.v7.widget.Toolbar
import com.google.zxing.BarcodeFormat
import de.markusfisch.android.binaryeye.R
import de.markusfisch.android.binaryeye.app.initSystemBars
import de.markusfisch.android.binaryeye.app.setFragment
import de.markusfisch.android.binaryeye.app.setSystemAndToolBarTransparency
import de.markusfisch.android.binaryeye.fragment.DecodeFragment
import de.markusfisch.android.binaryeye.fragment.EncodeFragment
import de.markusfisch.android.binaryeye.fragment.HistoryFragment
import de.markusfisch.android.binaryeye.fragment.PreferencesFragment
class MainActivity : AppCompatActivity() {
companion object {
private val ENCODE = "encode"
private val DECODE = "decode"
private val DECODE_FORMAT = "decode_format"
fun getEncodeIntent(context: Context, text: String? = ""): Intent {
val intent = Intent(context, MainActivity::class.java)
intent.putExtra(ENCODE, text)
return intent
}
fun getDecodeIntent(context: Context, text: String,
format: BarcodeFormat): Intent {
val intent = Intent(context, MainActivity::class.java)
intent.putExtra(DECODE, text)
intent.putExtra(DECODE_FORMAT, format)
return intent
}
}
override fun onSupportNavigateUp(): Boolean {
val fm = supportFragmentManager
if (fm != null && fm.backStackEntryCount > 0) {
@@ -54,20 +35,89 @@ class MainActivity : AppCompatActivity() {
setSupportActionBar(findViewById(R.id.toolbar) as Toolbar)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
supportFragmentManager.addOnBackStackChangedListener {
setSystemAndToolBarTransparency(this@MainActivity)
}
if (state == null) {
var fragment: Fragment
if (intent?.hasExtra(ENCODE) == true) {
fragment = EncodeFragment.newInstance(
intent.getStringExtra(ENCODE))
} else if (intent?.hasExtra(DECODE) == true) {
fragment = DecodeFragment.newInstance(
intent.getStringExtra(DECODE),
intent.getSerializableExtra(
DECODE_FORMAT) as BarcodeFormat)
} else {
fragment = DecodeFragment()
setFragment(
supportFragmentManager, when {
intent?.hasExtra(PREFERENCES) == true ->
PreferencesFragment()
intent?.hasExtra(HISTORY) == true ->
HistoryFragment()
intent?.hasExtra(ENCODE) == true ->
EncodeFragment.newInstance(
intent.getStringExtra(ENCODE) ?: ""
)
intent?.hasExtra(DECODED_TEXT) == true ->
DecodeFragment.newInstance(
intent.getStringExtra(DECODED_TEXT) ?: "",
intent.getSerializableExtra(
DECODED_FORMAT
) as BarcodeFormat,
intent.getByteArrayExtra(DECODED_RAW)
)
else -> DecodeFragment()
}
)
}
}
companion object {
private const val PREFERENCES = "preferences"
private const val HISTORY = "history"
private const val ENCODE = "encode"
private const val DECODED_TEXT = "decoded_text"
private const val DECODED_FORMAT = "decoded_format"
private const val DECODED_RAW = "decoded_raw"
fun getPreferencesIntent(context: Context): Intent {
val intent = Intent(context, MainActivity::class.java)
intent.putExtra(PREFERENCES, true)
return intent
}
fun getHistoryIntent(context: Context): Intent {
val intent = Intent(context, MainActivity::class.java)
intent.putExtra(HISTORY, true)
return intent
}
fun getEncodeIntent(
context: Context,
text: String? = "",
isExternal: Boolean = false
): Intent {
val intent = Intent(context, MainActivity::class.java)
intent.putExtra(ENCODE, text)
if (isExternal) {
val flagActivityClearTask =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
Intent.FLAG_ACTIVITY_CLEAR_TASK
} else 0
intent.addFlags(
Intent.FLAG_ACTIVITY_NO_HISTORY or
flagActivityClearTask or
Intent.FLAG_ACTIVITY_NEW_TASK
)
}
setFragment(supportFragmentManager, fragment)
return intent
}
fun getDecodeIntent(
context: Context,
text: String,
format: BarcodeFormat,
raw: ByteArray? = null
): Intent {
val intent = Intent(context, MainActivity::class.java)
intent.putExtra(DECODED_TEXT, text)
intent.putExtra(DECODED_FORMAT, format)
if (raw != null) {
intent.putExtra(DECODED_RAW, raw)
}
return intent
}
}
}
@@ -0,0 +1,172 @@
package de.markusfisch.android.binaryeye.activity
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.Rect
import android.net.Uri
import android.os.Bundle
import android.os.Parcelable
import android.provider.MediaStore
import android.support.v7.app.AppCompatActivity
import android.support.v7.widget.Toolbar
import android.view.Menu
import android.view.MenuItem
import android.widget.Toast
import com.google.zxing.Result
import de.markusfisch.android.binaryeye.R
import de.markusfisch.android.binaryeye.app.initSystemBars
import de.markusfisch.android.binaryeye.app.setSystemAndToolBarTransparency
import de.markusfisch.android.binaryeye.graphics.crop
import de.markusfisch.android.binaryeye.graphics.downsizeIfBigger
import de.markusfisch.android.binaryeye.widget.CropImageView
import de.markusfisch.android.binaryeye.zxing.Zxing
import java.io.IOException
import kotlin.math.min
import kotlin.math.max
class PickActivity : AppCompatActivity() {
private val zxing = Zxing()
private lateinit var cropImageView: CropImageView
private var returnResult = false
override fun onCreate(state: Bundle?) {
super.onCreate(state)
setContentView(R.layout.activity_pick)
initSystemBars(this)
setSupportActionBar(findViewById(R.id.toolbar) as Toolbar)
supportFragmentManager.addOnBackStackChangedListener {
setSystemAndToolBarTransparency(this@PickActivity)
}
returnResult = intent?.action == "com.google.zxing.client.android.SCAN"
val bitmap = if (
intent?.action == Intent.ACTION_SEND &&
intent.type?.startsWith("image/") == true
) {
loadSentImage(intent)
} else if (
intent?.action == Intent.ACTION_VIEW &&
intent.type?.startsWith("image/") == true
) {
loadImageToOpen(intent)
} else {
null
}
if (bitmap == null) {
Toast.makeText(
applicationContext,
R.string.error_no_content,
Toast.LENGTH_SHORT
).show()
finish()
return
}
val scannedRect = Rect()
val scanBounds: () -> Result? = {
var result: Result? = null
crop(
bitmap,
cropImageView.normalizedRectInBounds,
cropImageView.imageRotation
)?.also {
scannedRect.set(0, 0, it.width, it.height)
result = zxing.decodePositiveNegative(it)
}
result
}
cropImageView = findViewById(R.id.image) as CropImageView
cropImageView.setImageBitmap(bitmap)
cropImageView.onScan = {
val rect = Rect()
scanBounds()?.also {
rect.left = Int.MAX_VALUE
rect.top = Int.MAX_VALUE
for (rp in it.resultPoints) {
val x = rp.x.toInt()
val y = rp.y.toInt()
rect.left = min(rect.left, x)
rect.right = max(rect.right, x)
rect.top = min(rect.top, y)
rect.bottom = max(rect.bottom, y)
}
// map result points onto view
val bounds = cropImageView.getBoundsRect()
val fx = bounds.width() / scannedRect.width().toFloat()
val fy = bounds.height() / scannedRect.height().toFloat()
rect.set(
(rect.left * fx).toInt(),
(rect.top * fy).toInt(),
(rect.right * fx).toInt(),
(rect.bottom * fx).toInt()
)
rect.offset(bounds.left.toInt(), bounds.top.toInt())
}
rect
}
findViewById(R.id.scan).setOnClickListener {
scanImage(scanBounds())
}
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.activity_pick, menu)
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.rotate -> {
rotateClockwise()
true
}
else -> super.onOptionsItemSelected(item)
}
}
private fun rotateClockwise() {
cropImageView.imageRotation = cropImageView.imageRotation + 90 % 360
}
private fun loadSentImage(intent: Intent): Bitmap? {
val uri = intent.getParcelableExtra<Parcelable>(
Intent.EXTRA_STREAM
) as? Uri ?: return null
return loadImageUri(uri)
}
private fun loadImageToOpen(intent: Intent): Bitmap? {
val uri = intent.data ?: return null
return loadImageUri(uri)
}
private fun loadImageUri(uri: Uri): Bitmap? = try {
downsizeIfBigger(
MediaStore.Images.Media.getBitmap(contentResolver, uri),
1024
)
} catch (e: IOException) {
null
}
private fun scanImage(result: Result?) {
if (result != null) {
showResult(this, result, returnResult)
finish()
return
}
Toast.makeText(
applicationContext,
R.string.no_barcode_found,
Toast.LENGTH_SHORT
).show()
}
}
@@ -0,0 +1,60 @@
package de.markusfisch.android.binaryeye.adapter
import android.content.Context
import android.database.Cursor
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.CursorAdapter
import android.widget.TextView
import de.markusfisch.android.binaryeye.R
import de.markusfisch.android.binaryeye.data.Database
class ScansAdapter(context: Context, cursor: Cursor) :
CursorAdapter(context, cursor, false) {
private val timeIndex = cursor.getColumnIndex(Database.SCANS_DATETIME)
private val contentIndex = cursor.getColumnIndex(Database.SCANS_CONTENT)
private val formatIndex = cursor.getColumnIndex(Database.SCANS_FORMAT)
override fun newView(
context: Context,
cursor: Cursor,
parent: ViewGroup
): View {
return LayoutInflater.from(parent.context).inflate(
R.layout.row_scan, parent, false
)
}
override fun bindView(
view: View,
context: Context,
cursor: Cursor
) {
val holder = getViewHolder(view)
val content = cursor.getString(contentIndex)
holder.timeView.text = cursor.getString(timeIndex)
holder.contentView.text = if (content.isEmpty()) {
context.getString(R.string.binary_data)
} else {
content
}
holder.formatView.text = cursor.getString(formatIndex)
}
private fun getViewHolder(view: View): ViewHolder {
return view.tag as ViewHolder? ?: ViewHolder(
view.findViewById(R.id.time),
view.findViewById(R.id.content),
view.findViewById(R.id.format)
).also {
view.tag = it
}
}
private data class ViewHolder(
val timeView: TextView,
val contentView: TextView,
val formatView: TextView
)
}
@@ -0,0 +1,22 @@
package de.markusfisch.android.binaryeye.app
import android.app.Application
import android.support.v8.renderscript.RenderScript
import de.markusfisch.android.binaryeye.preference.Preferences
import de.markusfisch.android.binaryeye.repository.DatabaseRepository
val db = DatabaseRepository()
val prefs = Preferences()
class BinaryEyeApp : Application() {
override fun onCreate() {
super.onCreate()
// required to make RenderScript work for Lineage 16.0
// possibly because of a system/device bug
RenderScript.forceCompat()
db.open(this)
prefs.init(this)
}
}
@@ -0,0 +1,62 @@
package de.markusfisch.android.binaryeye.app
import android.annotation.SuppressLint
import android.content.Context
import android.support.v4.app.Fragment
import android.support.v4.app.FragmentManager
import android.support.v7.app.AlertDialog
import android.view.View
import de.markusfisch.android.binaryeye.R
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
fun setFragment(fm: FragmentManager?, fragment: Fragment) {
fm?.let { getTransaction(fm, fragment).commit() }
}
fun addFragment(fm: FragmentManager?, fragment: Fragment) {
fm?.let { getTransaction(fm, fragment).addToBackStack(null).commit() }
}
@SuppressLint("CommitTransaction")
private fun getTransaction(
fm: FragmentManager,
fragment: Fragment
) = fm.beginTransaction().replace(R.id.content_frame, fragment)
suspend inline fun View.useVisibility(
whileExecuting: Int = View.VISIBLE,
otherwise: Int = View.GONE,
crossinline block: suspend () -> Unit
) {
if (visibility == whileExecuting) {
return
}
withContext(Dispatchers.Main) {
visibility = whileExecuting
}
try {
block()
} finally {
withContext(Dispatchers.Main) {
visibility = otherwise
}
}
}
suspend inline fun <T : Any> alertDialog(
context: Context,
crossinline build: AlertDialog.Builder.(resume: (T?) -> Unit) -> Unit
): T? = withContext(Dispatchers.Main) {
suspendCoroutine<T?> { continuation ->
AlertDialog.Builder(context).apply {
setOnCancelListener {
continuation.resume(null)
}
build(continuation::resume)
show()
}
}
}
@@ -0,0 +1,12 @@
package de.markusfisch.android.binaryeye.app
import android.net.Uri
import android.os.Build
private val nonPrintable = "[\\x00-\\x08\\x0e-\\x1f]".toRegex()
fun hasNonPrintableCharacters(s: String) = nonPrintable.containsMatchIn(s)
fun parseAndNormalizeUri(input: String): Uri = Uri.parse(input).let {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) it.normalizeScheme() else it
}
@@ -0,0 +1,57 @@
package de.markusfisch.android.binaryeye.app
import android.app.Activity
import android.content.Context
import android.os.Environment
import android.support.annotation.MainThread
import android.widget.EditText
import de.markusfisch.android.binaryeye.R
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.collect
import java.io.File
import java.io.IOException
fun addSuffixIfNotGiven(name: String, suffix: String): String {
val trimmed = name.trim()
return if (suffix.isNotEmpty() && !trimmed.endsWith(suffix)) {
"$trimmed$suffix"
} else {
trimmed
}
}
// Dialogs doesn't have any root layout, need to be inflated with null root
@Suppress("InflateParams")
@MainThread
suspend fun Activity.askForFileName(suffix: String = ""): String? {
val view = layoutInflater.inflate(R.layout.dialog_save_file, null)
val editText = view.findViewById<EditText>(R.id.file_name)
return alertDialog<String>(this as Context) { resume ->
setView(view)
setPositiveButton(android.R.string.ok) { _, _ ->
resume(editText.text.toString())
}
}?.let { name ->
addSuffixIfNotGiven(name, suffix)
}
}
suspend fun Flow<ByteArray>.writeToFile(name: String): Int {
return try {
val file = File(
Environment.getExternalStoragePublicDirectory(
Environment.DIRECTORY_DOWNLOADS
),
name
)
if (file.exists()) {
return R.string.error_file_exists
}
collect { bytes ->
file.appendBytes(bytes)
}
R.string.saved_in_downloads
} catch (e: IOException) {
R.string.error_saving_binary_data
}
}
@@ -0,0 +1,23 @@
package de.markusfisch.android.binaryeye.app
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.emitAll
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.scanReduce
fun Flow<ByteArray>.addDelimiter(delimiter: String) = addDelimiter(
delimiter.toByteArray(),
ByteArray::plus
)
inline fun <T, R> Flow<T>.addDelimiter(
delimiter: R,
crossinline append: R.(T) -> T
): Flow<T> = scanReduce { _, content ->
delimiter.append(content)
}
infix fun <T> T.asFlowWith(other: Flow<T>) = flow<T> {
emit(this@asFlowWith)
emitAll(other)
}
@@ -0,0 +1,38 @@
package de.markusfisch.android.binaryeye.app
import android.Manifest
import android.app.Activity
import android.content.pm.PackageManager
import android.support.v4.app.ActivityCompat
import android.support.v4.content.ContextCompat
fun hasCameraPermission(activity: Activity, requestCode: Int): Boolean {
return hasPermission(activity, Manifest.permission.CAMERA, requestCode)
}
fun hasWritePermission(activity: Activity, requestCode: Int = 1): Boolean {
return hasPermission(
activity,
Manifest.permission.WRITE_EXTERNAL_STORAGE,
requestCode
)
}
fun hasPermission(
activity: Activity,
permission: String,
requestCode: Int
): Boolean {
return if (ContextCompat.checkSelfPermission(activity, permission) !=
PackageManager.PERMISSION_GRANTED
) {
ActivityCompat.requestPermissions(
activity,
arrayOf(permission),
requestCode
)
false
} else {
true
}
}
@@ -0,0 +1,54 @@
package de.markusfisch.android.binaryeye.app
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.support.v4.content.FileProvider
import android.widget.Toast
import de.markusfisch.android.binaryeye.BuildConfig
import de.markusfisch.android.binaryeye.R
import java.io.File
fun shareText(context: Context, text: String, type: String = "text/plain") {
val intent = Intent(Intent.ACTION_SEND)
intent.putExtra(Intent.EXTRA_TEXT, text)
intent.type = type
execShareIntent(context, intent)
}
fun shareUri(context: Context, uri: Uri, type: String) {
val intent = Intent(Intent.ACTION_SEND)
intent.putExtra(Intent.EXTRA_STREAM, uri)
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
intent.type = type
execShareIntent(context, intent)
}
fun execShareIntent(context: Context, intent: Intent) {
if (intent.resolveActivity(context.packageManager) != null) {
context.startActivity(intent)
} else {
Toast.makeText(
context,
R.string.cannot_resolve_action,
Toast.LENGTH_SHORT
).show()
}
}
fun shareFile(context: Context, file: File, type: String) {
shareUri(context, getUriForFile(context, file), type)
}
fun getUriForFile(context: Context, file: File): Uri {
return if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
Uri.fromFile(file)
} else {
FileProvider.getUriForFile(
context,
BuildConfig.APPLICATION_ID + ".provider",
file
)
}
}
@@ -0,0 +1,83 @@
package de.markusfisch.android.binaryeye.app
import android.content.Context
import android.graphics.Rect
import android.graphics.drawable.ColorDrawable
import android.os.Build
import android.support.v4.content.ContextCompat
import android.support.v4.view.ViewCompat
import android.support.v7.app.AppCompatActivity
import android.view.View
import android.widget.AbsListView
import de.markusfisch.android.binaryeye.R
val windowInsets = Rect()
val systemBarScrollListener = object : AbsListView.OnScrollListener {
override fun onScroll(
view: AbsListView,
firstVisibleItem: Int,
visibleItemCount: Int,
totalItemCount: Int
) {
// give Android some time to settle down before running this;
// not putting it on the queue makes it only work sometimes
view.post {
val scrolled = firstVisibleItem > 0 ||
(totalItemCount > 0 && view.getChildAt(0).top < 0)
val scrollable = if (scrolled) true else totalItemCount > 0 &&
view.getChildAt(view.lastVisiblePosition).bottom > view.height
setSystemAndToolBarTransparency(view.context, scrolled, scrollable)
}
}
override fun onScrollStateChanged(
view: AbsListView,
scrollState: Int
) {
}
}
fun setSystemAndToolBarTransparency(
context: Context,
scrolled: Boolean = false,
scrollable: Boolean = false
) {
val opaqueColor = ContextCompat.getColor(context, R.color.primary)
val transparentColor = 0x00000000
val topColor = if (scrolled) opaqueColor else transparentColor
val bottomColor = if (scrolled || scrollable) opaqueColor else transparentColor
val activity = context as AppCompatActivity
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
val window = activity.window
window.statusBarColor = topColor
window.navigationBarColor = bottomColor
}
activity.supportActionBar?.setBackgroundDrawable(ColorDrawable(topColor))
}
fun initSystemBars(activity: AppCompatActivity) {
activity.findViewById(R.id.main_layout)?.also { view ->
setWindowInsetListener(view)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
activity.window.decorView.systemUiVisibility =
View.SYSTEM_UI_FLAG_LAYOUT_STABLE or
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
}
setSystemAndToolBarTransparency(activity)
}
private fun setWindowInsetListener(view: View) {
ViewCompat.setOnApplyWindowInsetsListener(view) { _, insets ->
if (insets.hasSystemWindowInsets()) {
val left = insets.systemWindowInsetLeft
val top = insets.systemWindowInsetTop
val right = insets.systemWindowInsetRight
val bottom = insets.systemWindowInsetBottom
view.setPadding(left, top, right, bottom)
windowInsets.set(left, top, right, bottom)
}
insets.consumeSystemWindowInsets()
}
}
@@ -1,21 +0,0 @@
package de.markusfisch.android.binaryeye.app
import de.markusfisch.android.binaryeye.R
import android.support.v4.app.Fragment
import android.support.v4.app.FragmentManager
import android.support.v4.app.FragmentTransaction
fun setFragment(fm: FragmentManager?, fragment: Fragment) {
fm?.let { getTransaction(fm, fragment).commit() }
}
fun addFragment(fm: FragmentManager?, fragment: Fragment) {
fm?.let { getTransaction(fm, fragment).addToBackStack(null).commit() }
}
private fun getTransaction(
fm: FragmentManager,
fragment: Fragment): FragmentTransaction {
return fm.beginTransaction().replace(R.id.content_frame, fragment)
}
@@ -1,39 +0,0 @@
package de.markusfisch.android.binaryeye.app
import de.markusfisch.android.binaryeye.view.SystemBarMetrics
import de.markusfisch.android.binaryeye.R
import android.os.Build
import android.support.v4.content.ContextCompat
import android.support.v7.app.AppCompatActivity
import android.view.View
import android.view.Window
fun initSystemBars(activity: AppCompatActivity?) {
val view = activity?.findViewById(R.id.main_layout)
if (view != null && setSystemBarColor(activity.window,
ContextCompat.getColor(activity,
R.color.primary_dark_translucent))) {
view.setPadding(
0,
SystemBarMetrics.getStatusBarHeight(
activity.resources),
0,
0)
}
}
private fun setSystemBarColor(window: Window, color: Int): Boolean {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
return false
}
window.statusBarColor = color
window.navigationBarColor = color
window.decorView.systemUiVisibility =
View.SYSTEM_UI_FLAG_LAYOUT_STABLE or
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
return true
}
@@ -0,0 +1,122 @@
package de.markusfisch.android.binaryeye.data
import android.content.ContentValues
import android.content.Context
import android.database.Cursor
import android.database.sqlite.SQLiteDatabase
import android.database.sqlite.SQLiteOpenHelper
import android.text.format.DateFormat
import de.markusfisch.android.binaryeye.app.hasNonPrintableCharacters
class Database {
private lateinit var db: SQLiteDatabase
fun open(context: Context) {
db = OpenHelper(context).writableDatabase
}
fun getScans(): Cursor? = db.rawQuery(
"""SELECT
$SCANS_ID,
$SCANS_DATETIME,
$SCANS_CONTENT,
$SCANS_FORMAT
FROM $SCANS
ORDER BY $SCANS_DATETIME DESC
""", null
)
fun getScan(id: Long): Cursor? = db.rawQuery(
"""SELECT
$SCANS_DATETIME,
$SCANS_CONTENT,
$SCANS_RAW,
$SCANS_FORMAT
FROM $SCANS
WHERE $SCANS_ID = ?
""", arrayOf("$id")
)
fun hasBinaryData(): Cursor? = db.rawQuery(
"""SELECT
1
FROM $SCANS
WHERE $SCANS_RAW IS NOT NULL
LIMIT 1
""", null
)
fun insertScan(
timestamp: Long,
content: String,
raw: ByteArray?,
format: String
): Long {
val cv = ContentValues()
cv.put(
SCANS_DATETIME, DateFormat.format(
"yyyy-MM-dd HH:mm:ss",
timestamp
).toString()
)
if (hasNonPrintableCharacters(content) && raw != null) {
cv.put(SCANS_CONTENT, "")
cv.put(SCANS_RAW, raw)
} else {
cv.put(SCANS_CONTENT, content)
}
cv.put(SCANS_FORMAT, format)
return db.insert(SCANS, null, cv)
}
fun removeScan(id: Long) {
db.delete(SCANS, "$SCANS_ID = ?", arrayOf("$id"))
}
fun removeScans() {
db.delete(SCANS, null, null)
}
private class OpenHelper(context: Context) :
SQLiteOpenHelper(context, "history.db", null, 2) {
override fun onCreate(db: SQLiteDatabase) {
createScans(db)
}
override fun onUpgrade(
db: SQLiteDatabase,
oldVersion: Int,
newVersion: Int
) {
if (oldVersion < 2) {
addRawColumn(db)
}
}
}
companion object {
const val SCANS = "scans"
const val SCANS_ID = "_id"
const val SCANS_DATETIME = "_datetime"
const val SCANS_CONTENT = "content"
const val SCANS_RAW = "raw"
const val SCANS_FORMAT = "format"
private fun createScans(db: SQLiteDatabase) {
db.execSQL("DROP TABLE IF EXISTS $SCANS")
db.execSQL(
"""CREATE TABLE $SCANS (
$SCANS_ID INTEGER PRIMARY KEY AUTOINCREMENT,
$SCANS_DATETIME DATETIME NOT NULL,
$SCANS_CONTENT TEXT NOT NULL,
$SCANS_RAW BLOB,
$SCANS_FORMAT TEXT NOT NULL
)"""
)
}
private fun addRawColumn(db: SQLiteDatabase) {
db.execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_RAW BLOB")
}
}
}
@@ -0,0 +1,80 @@
package de.markusfisch.android.binaryeye.data.csv
import de.markusfisch.android.binaryeye.app.addDelimiter
import de.markusfisch.android.binaryeye.app.asFlowWith
import kotlinx.coroutines.flow.*
fun <T> csvBuilder(config: CSVBuilder<T>.() -> Unit): CSVBuilder<T> {
return CSVBuilder<T>().apply(config)
}
class CSVBuilder<T> {
private val columns = mutableListOf<ColumnBuilder>()
fun column(config: ColumnBuilder.() -> Unit): ColumnBuilder {
return ColumnBuilder().apply(config).also {
columns.add(it)
}
}
inner class ColumnBuilder {
var isBinary = false
var name: String? = null
internal var gettingBy: ((T) -> ByteArray)? = null
fun gettingBy(getter: (T) -> ByteArray) {
gettingBy = getter
}
fun gettingByString(getter: (T) -> String): Unit = gettingBy {
getter(it).toByteArray()
}
}
fun buildWith(inputs: Flow<T>, delimiter: String): Flow<ByteArray> = flow {
val columnsFlow = columns.asFlow()
val header = columnsFlow.buildHeader(delimiter)
val contentLines = inputs.buildContent(columnsFlow, delimiter)
emitAll(header)
emit("\n".toByteArray())
emitAll(contentLines)
}
private fun Flow<ColumnBuilder>.buildHeader(delimiter: String) = map {
it.name?.escaped(delimiter)?.toByteArray()
?: throw IllegalStateException("You need to provide a name for csv column")
}.addDelimiter(delimiter)
private fun Flow<T>.buildContent(
columnsFlow: Flow<ColumnBuilder>,
delimiter: String
): Flow<ByteArray> = map { input ->
input.getLine(columnsFlow, delimiter)
}.addDelimiter("\n".toByteArray()) { other: Flow<ByteArray> ->
// this: ByteArray
this asFlowWith other
}.flattenConcat()
private fun T.getLine(columnsFlow: Flow<ColumnBuilder>, delimiter: String): Flow<ByteArray> =
columnsFlow.map { column ->
column.gettingBy?.run {
var result = invoke(this@getLine)
if (!column.isBinary) {
result = String(result).escaped(delimiter).toByteArray()
}
return@run result
} ?: throw IllegalStateException("You need to provide a getter for the value")
}.addDelimiter(delimiter)
private fun Any.escaped(delimiter: String): String {
val any = this.toString()
return when {
any.run { contains('"') || contains('\n') || contains(delimiter) } ->
"\"${any.replace("\"", "\"\"").replace(delimiter, "\"$delimiter")}\""
else -> any
}
}
}
@@ -1,69 +1,195 @@
package de.markusfisch.android.binaryeye.fragment
import com.google.zxing.BarcodeFormat
import de.markusfisch.android.binaryeye.zxing.Zxing
import de.markusfisch.android.binaryeye.R
import android.os.Bundle
import android.annotation.SuppressLint
import android.app.AlertDialog
import android.graphics.Bitmap
import android.os.Bundle
import android.os.Environment
import android.support.v4.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.*
import android.widget.EditText
import android.widget.ImageView
import android.widget.Toast
import com.google.zxing.BarcodeFormat
import de.markusfisch.android.binaryeye.R
import de.markusfisch.android.binaryeye.app.addSuffixIfNotGiven
import de.markusfisch.android.binaryeye.app.hasWritePermission
import de.markusfisch.android.binaryeye.app.shareFile
import de.markusfisch.android.binaryeye.zxing.Zxing
import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
class BarcodeFragment : Fragment() {
private var barcode: Bitmap? = null
override fun onCreate(state: Bundle?) {
super.onCreate(state)
setHasOptionsMenu(true)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
state: Bundle?
): View? {
activity?.setTitle(R.string.view_barcode)
val view = inflater.inflate(
R.layout.fragment_barcode,
container,
false
)
val args = arguments ?: return view
val content = args.getString(CONTENT) ?: return view
val format = args.getSerializable(FORMAT) as BarcodeFormat? ?: return view
val size = args.getInt(SIZE)
try {
barcode = Zxing.encodeAsBitmap(content, format, size, size)
} catch (e: Exception) {
var message = e.message
if (message == null || message.isEmpty()) {
message = getString(R.string.error_encoding_barcode)
}
Toast.makeText(activity, message, Toast.LENGTH_SHORT).show()
fragmentManager.popBackStack()
return null
}
view.findViewById<ImageView>(R.id.barcode).setImageBitmap(barcode)
view.findViewById<View>(R.id.share).setOnClickListener {
val bitmap = barcode
bitmap?.let {
share(bitmap)
}
}
return view
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.fragment_barcode, menu)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.save -> {
askForFileNameAndSave()
true
}
else -> super.onOptionsItemSelected(item)
}
}
// dialogs do not have a parent view
@SuppressLint("InflateParams")
private fun askForFileNameAndSave() {
val ac = activity ?: return
val view = ac.layoutInflater.inflate(R.layout.dialog_save_file, null)
val editText = view.findViewById<EditText>(R.id.file_name)
AlertDialog.Builder(ac)
.setView(view)
.setPositiveButton(android.R.string.ok) { _, _ ->
val bitmap = barcode
bitmap?.let {
saveAsFile(
bitmap,
addSuffixIfNotGiven(
editText.text.toString(),
".png"
)
)
}
}
.setNegativeButton(android.R.string.cancel) { _, _ ->
}
.show()
}
private fun saveAsFile(bitmap: Bitmap, path: String) {
val ac = activity ?: return
if (!hasWritePermission(ac)) {
return
}
GlobalScope.launch {
val success = saveBitmap(
bitmap,
File(
Environment.getExternalStoragePublicDirectory(
Environment.DIRECTORY_DOWNLOADS
),
path
)
)
GlobalScope.launch(Main) {
Toast.makeText(
ac,
if (success) {
R.string.saved_in_downloads
} else {
R.string.error_saving_binary_data
},
Toast.LENGTH_LONG
).show()
}
}
}
private fun share(bitmap: Bitmap) {
GlobalScope.launch {
val file = File(
context.externalCacheDir,
"shared_barcode.png"
)
val success = saveBitmap(bitmap, file)
GlobalScope.launch(Main) {
if (success) {
shareFile(context, file, "image/png")
} else {
val ac = activity
ac?.let {
Toast.makeText(
ac,
R.string.error_saving_binary_data,
Toast.LENGTH_LONG
).show()
}
}
}
}
}
companion object {
private val CONTENT = "content"
private val FORMAT = "format"
private val SIZE = "size"
private const val CONTENT = "content"
private const val FORMAT = "format"
private const val SIZE = "size"
fun newInstance(
content: String,
format: BarcodeFormat,
size: Int): Fragment {
content: String,
format: BarcodeFormat,
size: Int
): Fragment {
val args = Bundle()
args.putString(CONTENT, content)
args.putSerializable(FORMAT, format)
args.putInt(SIZE, size)
val fragment = BarcodeFragment()
fragment.setArguments(args)
fragment.arguments = args
return fragment
}
}
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
state: Bundle?): View? {
activity.setTitle(R.string.view_barcode)
val view = inflater.inflate(
R.layout.fragment_barcode,
container,
false)
val args = getArguments()
args?.let {
val size = args.getInt(SIZE)
val bitmap: Bitmap?
try {
bitmap = Zxing.encodeAsBitmap(
args.getString(CONTENT),
args.getSerializable(FORMAT) as BarcodeFormat,
size,
size)
} catch (e: Exception) {
Toast.makeText(activity, e.message,
Toast.LENGTH_SHORT).show()
fragmentManager.popBackStack()
return null
}
view.findViewById<ImageView>(R.id.barcode).setImageBitmap(bitmap)
}
return view
private fun saveBitmap(bitmap: Bitmap, file: File): Boolean {
return try {
val fos = FileOutputStream(file)
bitmap.compress(Bitmap.CompressFormat.PNG, 90, fos)
fos.close()
true
} catch (e: IOException) {
false
}
}
@@ -1,42 +1,41 @@
package de.markusfisch.android.binaryeye.fragment
import com.google.zxing.BarcodeFormat
import de.markusfisch.android.binaryeye.app.addFragment
import de.markusfisch.android.binaryeye.R
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.support.design.widget.FloatingActionButton
import android.support.v4.app.Fragment
import android.text.ClipboardManager
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.text.Editable
import android.text.TextWatcher
import android.view.*
import android.widget.EditText
import android.widget.TextView
import android.widget.Toast
import com.google.zxing.BarcodeFormat
import de.markusfisch.android.binaryeye.R
import de.markusfisch.android.binaryeye.actions.ActionRegistry
import de.markusfisch.android.binaryeye.app.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.launch
class DecodeFragment : Fragment() {
companion object {
private val CONTENT = "content"
private val FORMAT = "format"
fun newInstance(content: String, format: BarcodeFormat): Fragment {
val args = Bundle()
args.putString(CONTENT, content)
args.putSerializable(FORMAT, format)
val fragment = DecodeFragment()
fragment.arguments = args
return fragment
}
}
private lateinit var contentView: EditText
private lateinit var formatView: TextView
private lateinit var hexView: TextView
private lateinit var format: BarcodeFormat
private lateinit var fab: FloatingActionButton
private var action = ActionRegistry.DEFAULT_ACTION
private var isBinary = false
private val content: String
get() = contentView.text.toString()
private val parentJob = Job()
private val scope: CoroutineScope = CoroutineScope(Dispatchers.Main + parentJob)
override fun onCreate(state: Bundle?) {
super.onCreate(state)
@@ -44,86 +43,229 @@ class DecodeFragment : Fragment() {
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
state: Bundle?): View {
activity.setTitle(R.string.content)
inflater: LayoutInflater,
container: ViewGroup?,
state: Bundle?
): View {
activity?.setTitle(R.string.content)
val view = inflater.inflate(
R.layout.fragment_decode,
container,
false)
R.layout.fragment_decode,
container,
false
)
val content = arguments?.getString(CONTENT) ?: ""
format = arguments?.getSerializable(FORMAT) as BarcodeFormat? ?:
BarcodeFormat.QR_CODE
val inputContent = arguments?.getString(CONTENT) ?: ""
isBinary = hasNonPrintableCharacters(inputContent) or inputContent.isEmpty()
val raw = arguments?.getByteArray(RAW) ?: inputContent.toByteArray()
format = arguments?.getSerializable(FORMAT) as BarcodeFormat? ?: BarcodeFormat.QR_CODE
contentView = view.findViewById<EditText>(R.id.content)
contentView.setText(content)
contentView = view.findViewById(R.id.content)
fab = view.findViewById(R.id.open)
view.findViewById<View>(R.id.share).setOnClickListener { _ ->
share(getContent())
if (!isBinary) {
contentView.setText(inputContent)
contentView.addTextChangedListener(object : TextWatcher {
override fun afterTextChanged(s: Editable?) {
updateViewsAndAction(content.toByteArray())
}
override fun beforeTextChanged(
s: CharSequence?,
start: Int,
count: Int,
after: Int
) {
}
override fun onTextChanged(
s: CharSequence?,
start: Int,
before: Int,
count: Int
) {
}
})
fab.setOnClickListener {
executeAction(content.toByteArray())
}
if (prefs.openImmediately) {
executeAction(content.toByteArray())
}
} else {
contentView.setText(R.string.binary_data)
contentView.isEnabled = false
fab.setImageResource(R.drawable.ic_action_save)
fab.setOnClickListener {
askForFileNameAndSave(raw)
}
}
formatView = view.findViewById(R.id.format)
hexView = view.findViewById(R.id.hex)
updateViewsAndAction(raw)
return view
}
private fun updateViewsAndAction(bytes: ByteArray) {
val prevAction = action
if (!prevAction.canExecuteOn(bytes)) {
action = ActionRegistry.getAction(bytes)
}
formatView.text = resources.getQuantityString(
R.plurals.barcode_info,
bytes.size,
format.toString(),
bytes.size
)
hexView.text = hexDump(bytes, 33)
if (prevAction !== action) {
fab.setImageResource(action.iconResId)
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
fab.setOnLongClickListener { v ->
Toast.makeText(
v.context,
action.titleResId,
Toast.LENGTH_SHORT
).show()
true
}
} else {
fab.tooltipText = getString(action.titleResId)
}
}
}
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
}
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.copy_to_clipboard -> {
copyToClipboard(getContent())
copyToClipboard(content)
true
}
R.id.open_url -> {
openUrl(getContent())
R.id.share -> {
context?.also { shareText(it, content) }
true
}
R.id.create -> {
addFragment(fragmentManager,
EncodeFragment.newInstance(getContent(), format))
addFragment(
fragmentManager,
EncodeFragment.newInstance(content, format)
)
true
}
else -> super.onOptionsItemSelected(item)
}
}
private fun getContent(): String {
return contentView.getText().toString()
}
private fun copyToClipboard(text: String) {
activity ?: return
val cm = activity.getSystemService(
Context.CLIPBOARD_SERVICE) as ClipboardManager
cm.setText(text)
Toast.makeText(activity,
R.string.put_into_clipboard,
Toast.LENGTH_SHORT).show()
Context.CLIPBOARD_SERVICE
) as ClipboardManager
cm.text = text
Toast.makeText(
activity,
R.string.put_into_clipboard,
Toast.LENGTH_SHORT
).show()
}
private fun openUrl(url: String) {
if (activity == null || url.isEmpty()) {
return
}
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
if (intent.resolveActivity(activity.getPackageManager()) != null) {
startActivity(intent)
} else {
Toast.makeText(activity,
R.string.cannot_resolve_action,
Toast.LENGTH_SHORT).show()
private fun executeAction(content: ByteArray) {
if (activity != null && content.isNotEmpty()) scope.launch {
action.execute(activity, content)
}
}
private fun share(text: String) {
val intent = Intent(Intent.ACTION_SEND)
intent.putExtra(Intent.EXTRA_TEXT, text)
intent.setType("text/plain")
startActivity(intent)
private fun askForFileNameAndSave(raw: ByteArray) {
val ac = activity ?: return
if (!hasWritePermission(ac)) return
scope.launch(Dispatchers.Main) {
val name = ac.askForFileName() ?: return@launch
val message = flowOf(raw).writeToFile(name)
Toast.makeText(
ac,
message,
Toast.LENGTH_SHORT
).show()
}
}
override fun onDestroyView() {
parentJob.cancel()
super.onDestroyView()
}
companion object {
private const val CONTENT = "content"
private const val FORMAT = "format"
private const val RAW = "raw"
fun newInstance(
content: String,
format: BarcodeFormat,
raw: ByteArray? = null
): Fragment {
val args = Bundle()
args.putString(CONTENT, content)
args.putSerializable(FORMAT, format)
if (raw != null) {
args.putByteArray(RAW, raw)
}
val fragment = DecodeFragment()
fragment.arguments = args
return fragment
}
}
}
private fun hexDump(bytes: ByteArray, charsPerLine: Int): String {
if (charsPerLine < 4 || bytes.isEmpty()) {
return ""
}
val dump = StringBuilder()
val hex = StringBuilder()
val ascii = StringBuilder()
val itemsPerLine = (charsPerLine - 1) / 4
val len = bytes.size
var i = 0
while (true) {
val ord = bytes[i]
hex.append(String.format("%02X ", ord))
ascii.append(if (ord > 31) ord.toChar() else " ")
++i
val posInLine = i % itemsPerLine
val atEnd = i >= len
var atLineEnd = posInLine == 0
if (atEnd && !atLineEnd) {
for (j in posInLine until itemsPerLine) {
hex.append(" ")
}
atLineEnd = true
}
if (atLineEnd) {
dump.append(hex.toString())
dump.append(" ")
dump.append(ascii.toString())
dump.append("\n")
hex.setLength(0)
ascii.setLength(0)
}
if (atEnd) {
break
}
}
return dump.toString()
}
@@ -1,101 +1,88 @@
package de.markusfisch.android.binaryeye.fragment
import com.google.zxing.BarcodeFormat
import de.markusfisch.android.binaryeye.app.addFragment
import de.markusfisch.android.binaryeye.R
import android.content.Context
import android.os.Bundle
import android.support.v4.app.Fragment
import android.view.inputmethod.InputMethodManager
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ArrayAdapter
import android.widget.EditText
import android.widget.SeekBar
import android.widget.Spinner
import android.widget.TextView
import android.widget.Toast
import android.view.inputmethod.InputMethodManager
import android.widget.*
import com.google.zxing.BarcodeFormat
import de.markusfisch.android.binaryeye.R
import de.markusfisch.android.binaryeye.app.addFragment
class EncodeFragment : Fragment() {
companion object {
private val CONTENT = "content"
private val FORMAT = "format"
fun newInstance(
content: String,
format: BarcodeFormat = BarcodeFormat.AZTEC): Fragment {
val args = Bundle()
args.putString(CONTENT, content)
args.putSerializable(FORMAT, format)
val fragment = EncodeFragment()
fragment.setArguments(args)
return fragment
}
}
private lateinit var formatView: Spinner
private lateinit var sizeView: TextView
private lateinit var sizeBarView: SeekBar
private val writers = arrayListOf(
BarcodeFormat.AZTEC,
BarcodeFormat.CODABAR,
BarcodeFormat.CODE_39,
BarcodeFormat.CODE_128,
BarcodeFormat.DATA_MATRIX,
BarcodeFormat.EAN_8,
BarcodeFormat.EAN_13,
BarcodeFormat.ITF,
BarcodeFormat.PDF_417,
BarcodeFormat.QR_CODE,
BarcodeFormat.UPC_A
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
)
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
state: Bundle?): View {
activity.setTitle(R.string.compose_barcode)
inflater: LayoutInflater,
container: ViewGroup?,
state: Bundle?
): View? {
val ac = activity ?: return null
ac.setTitle(R.string.compose_barcode)
val view = inflater.inflate(
R.layout.fragment_encode,
container,
false)
R.layout.fragment_encode,
container,
false
)
formatView = view.findViewById<Spinner>(R.id.format)
formatView.setAdapter(ArrayAdapter<String>(
activity,
android.R.layout.simple_list_item_1,
writers.map { it -> it.name }))
formatView = view.findViewById(R.id.format)
formatView.adapter = ArrayAdapter<String>(
ac,
android.R.layout.simple_list_item_1,
writers.map { it.name }
)
sizeView = view.findViewById<TextView>(R.id.size_display)
sizeBarView = view.findViewById<SeekBar>(R.id.size_bar)
sizeView = view.findViewById(R.id.size_display)
sizeBarView = view.findViewById(R.id.size_bar)
initSizeBar()
val contentView = view.findViewById<EditText>(R.id.content)
val args = getArguments()
val args = arguments
args?.let {
contentView.setText(args.getString(CONTENT))
formatView.setSelection(writers.indexOf(
args.getSerializable(FORMAT) as BarcodeFormat?))
formatView.setSelection(
writers.indexOf(
args.getSerializable(FORMAT) as BarcodeFormat?
)
)
}
view.findViewById<View>(R.id.encode).setOnClickListener { v ->
val format = writers.get(formatView.getSelectedItemPosition())
var size = getSize(sizeBarView.getProgress())
val content = contentView.getText().toString()
val format = writers[formatView.selectedItemPosition]
val size = getSize(sizeBarView.progress)
val content = contentView.text.toString()
if (content.isEmpty()) {
Toast.makeText(v.context, R.string.error_no_content,
Toast.LENGTH_SHORT).show()
Toast.makeText(
v.context,
R.string.error_no_content,
Toast.LENGTH_SHORT
).show()
} else {
hideSoftKeyboard(contentView)
addFragment(fragmentManager, BarcodeFragment.newInstance(
content,
format,
size))
addFragment(
fragmentManager,
BarcodeFragment.newInstance(content, format, size)
)
}
}
@@ -103,34 +90,53 @@ class EncodeFragment : Fragment() {
}
private fun initSizeBar() {
updateSize(sizeBarView.getProgress())
updateSize(sizeBarView.progress)
sizeBarView.setOnSeekBarChangeListener(
object : SeekBar.OnSeekBarChangeListener {
override fun onProgressChanged(
object : SeekBar.OnSeekBarChangeListener {
override fun onProgressChanged(
seekBar: SeekBar,
progressValue: Int,
fromUser: Boolean) {
updateSize(progressValue)
}
fromUser: Boolean
) {
updateSize(progressValue)
}
override fun onStartTrackingTouch(seekBar: SeekBar) {}
override fun onStartTrackingTouch(seekBar: SeekBar) {}
override fun onStopTrackingTouch(seekBar: SeekBar) {}
})
override fun onStopTrackingTouch(seekBar: SeekBar) {}
})
}
private fun updateSize(power: Int) {
val size = getSize(power)
sizeView.setText("${size}x${size}")
sizeView.text = getString(R.string.width_by_height, size, size)
}
private fun getSize(power: Int) = 128 * (power + 1)
private fun hideSoftKeyboard(view: View) {
val im = activity?.getSystemService(
Context.INPUT_METHOD_SERVICE) as InputMethodManager?
Context.INPUT_METHOD_SERVICE
) as InputMethodManager?
im?.let {
im.hideSoftInputFromWindow(view.getWindowToken(), 0)
im.hideSoftInputFromWindow(view.windowToken, 0)
}
}
companion object {
private const val CONTENT = "content"
private const val FORMAT = "format"
fun newInstance(
content: String,
format: BarcodeFormat = BarcodeFormat.AZTEC
): Fragment {
val args = Bundle()
args.putString(CONTENT, content)
args.putSerializable(FORMAT, format)
val fragment = EncodeFragment()
fragment.arguments = args
return fragment
}
}
}
@@ -0,0 +1,284 @@
package de.markusfisch.android.binaryeye.fragment
import android.app.AlertDialog
import android.content.Context
import android.os.Bundle
import android.support.annotation.WorkerThread
import android.support.v4.app.Fragment
import android.support.v7.widget.SwitchCompat
import android.view.*
import android.widget.ListView
import android.widget.Toast
import com.google.zxing.BarcodeFormat
import de.markusfisch.android.binaryeye.R
import de.markusfisch.android.binaryeye.adapter.ScansAdapter
import de.markusfisch.android.binaryeye.app.*
import de.markusfisch.android.binaryeye.data.csv.csvBuilder
import de.markusfisch.android.binaryeye.repository.DatabaseRepository
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.mapNotNull
class HistoryFragment : Fragment() {
private lateinit var listView: ListView
private lateinit var fab: View
private lateinit var progressView: View
private val parentJob = Job()
private val scope = CoroutineScope(Dispatchers.IO + parentJob)
override fun onCreate(state: Bundle?) {
super.onCreate(state)
setHasOptionsMenu(true)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
state: Bundle?
): View {
activity?.setTitle(R.string.history)
val view = inflater.inflate(
R.layout.fragment_history,
container,
false
)
val useHistorySwitch = view.findViewById(
R.id.use_history
) as SwitchCompat
initHistorySwitch(useHistorySwitch)
listView = view.findViewById(R.id.scans)
listView.emptyView = useHistorySwitch
listView.setOnItemClickListener { _, _, _, id ->
showScan(id)
}
listView.setOnItemLongClickListener { _, v, _, id ->
askToRemoveScan(v.context, id)
true
}
listView.setOnScrollListener(systemBarScrollListener)
fab = view.findViewById(R.id.share)
fab.setOnClickListener { v ->
pickListSeparatorAndShare(v.context)
}
progressView = view.findViewById(R.id.progress_view)
return view
}
override fun onDestroy() {
super.onDestroy()
(listView.adapter as ScansAdapter?)?.changeCursor(null)
parentJob.cancel()
}
override fun onResume() {
super.onResume()
update(context)
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.fragment_history, menu)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.clear -> {
askToRemoveAllScans(context)
true
}
R.id.export_history -> {
askToExportToFile(context)
true
}
else -> super.onOptionsItemSelected(item)
}
}
private fun initHistorySwitch(switchView: SwitchCompat) {
switchView.setOnCheckedChangeListener { _, isChecked ->
prefs.useHistory = isChecked
}
if (prefs.useHistory) {
switchView.toggle()
}
}
private fun update(context: Context) {
scope.launch {
val cursor = db.getScansCursor()
withContext(Dispatchers.Main) {
fab.visibility = if (cursor != null && cursor.count > 0) {
View.VISIBLE
} else {
View.GONE
}
cursor?.let {
if (listView.adapter == null) {
listView.adapter = ScansAdapter(context, cursor)
} else {
val adapter = listView.adapter as ScansAdapter
adapter.changeCursor(cursor)
adapter.notifyDataSetChanged()
}
}
}
}
}
private fun showScan(id: Long) = db.getScan(id)?.also { scan ->
try {
addFragment(
fragmentManager,
DecodeFragment.newInstance(
scan.content,
BarcodeFormat.valueOf(scan.format),
scan.raw
)
)
} catch (e: IllegalArgumentException) {
// shouldn't ever happen
}
}
private fun askToRemoveScan(context: Context, id: Long) {
AlertDialog.Builder(context)
.setMessage(R.string.really_remove_scan)
.setPositiveButton(android.R.string.ok) { _, _ ->
db.removeScan(id)
update(context)
}
.setNegativeButton(android.R.string.cancel) { _, _ ->
}
.show()
}
private fun askToRemoveAllScans(context: Context) {
AlertDialog.Builder(context)
.setMessage(R.string.really_remove_all_scans)
.setPositiveButton(android.R.string.ok) { _, _ ->
db.removeScans()
update(context)
}
.setNegativeButton(android.R.string.cancel) { _, _ ->
}
.show()
}
private fun askToExportToFile(context: Context) = scope.launch {
progressView.useVisibility {
if (!hasWritePermission(activity)) {
return@useVisibility
}
val (getBinaries, scans) = getScans {
alertDialog(context) { resume ->
setTitle(R.string.csv_allow_binary)
setMessage(R.string.csv_allow_binary_message)
setPositiveButton(R.string.no) { _, _ ->
resume(false)
}
setNegativeButton(android.R.string.yes) { _, _ ->
resume(true)
}
setNeutralButton(android.R.string.cancel) { _, _ ->
resume(null)
}
}
} ?: return@useVisibility
val delimiters = context.resources.getStringArray(
R.array.csv_delimiters_values
)
val delimiter = alertDialog<String>(context) { resume ->
setTitle(R.string.csv_delimiter)
setItems(R.array.csv_delimiters_names) { _, which ->
resume(delimiters[which])
}
} ?: return@useVisibility
val name = withContext(Dispatchers.Main) {
activity.askForFileName(suffix = "csv")
} ?: return@useVisibility
val csv = scans.toCSV(delimiter, getBinaries)
val toastMessage = csv.writeToFile(name)
withContext(Dispatchers.Main) {
Toast.makeText(
context,
toastMessage,
Toast.LENGTH_SHORT
).show()
}
}
}
private fun Flow<DatabaseRepository.Scan>.toCSV(
delimiter: String,
allowBinary: Boolean
): Flow<ByteArray> {
return csvBuilder<DatabaseRepository.Scan> {
column {
name = "DATE"
gettingByString { it.timestamp }
}
column {
name = "TYPE"
gettingByString { it.format }
}
column {
name = "CONTENT"
gettingByString { it.content }
}
if (allowBinary) column {
isBinary = true
name = "BINARY_CONTENT"
gettingBy { it.raw ?: ByteArray(0) }
}
}.buildWith(this, delimiter)
}
private fun pickListSeparatorAndShare(context: Context) {
val separators = context.resources.getStringArray(
R.array.list_separators_values
)
AlertDialog.Builder(context)
.setTitle(R.string.pick_list_separator)
.setItems(R.array.list_separators_names) { _, which ->
shareScans(separators[which])
}
.show()
}
private fun shareScans(separator: String) {
scope.launch {
progressView.useVisibility {
val sb = StringBuilder()
getScans { false }?.second?.collect {
sb.append(it.content)
sb.append(separator)
}
val text = sb.toString()
if (text.isNotEmpty()) withContext(Dispatchers.Main) {
shareText(context, text)
}
}
}
}
@WorkerThread
private suspend inline fun getScans(
crossinline binaryData: suspend () -> Boolean?
): Pair<Boolean, Flow<DatabaseRepository.Scan>>? {
val getBinaries: Boolean = db.hasBinaryData() && binaryData() ?: return null
return getBinaries to db.getScans().mapNotNull {
when {
it.content.isNotEmpty() -> db.getScan(it.id)
getBinaries -> db.getScan(it.id)
else -> null
}
}
}
}
@@ -0,0 +1,53 @@
package de.markusfisch.android.binaryeye.fragment
import android.os.Bundle
import android.support.v4.app.Fragment
import android.support.v7.widget.SwitchCompat
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.EditText
import de.markusfisch.android.binaryeye.R
import de.markusfisch.android.binaryeye.app.prefs
class PreferencesFragment : Fragment() {
private lateinit var openImmediatelySwitch: SwitchCompat
private lateinit var useHistorySwitch: SwitchCompat
private lateinit var openWithUrlInput: EditText
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
state: Bundle?
): View? {
activity?.setTitle(R.string.preferences)
val view = inflater.inflate(
R.layout.fragment_preferences,
container,
false
)
openImmediatelySwitch = view.findViewById<SwitchCompat>(R.id.open_immediately)
if (prefs.openImmediately) {
openImmediatelySwitch.toggle()
}
useHistorySwitch = view.findViewById<SwitchCompat>(R.id.use_history)
if (prefs.useHistory) {
useHistorySwitch.toggle()
}
openWithUrlInput = view.findViewById<EditText>(R.id.open_with_url)
openWithUrlInput.setText(prefs.openWithUrl)
return view
}
override fun onPause() {
super.onPause()
prefs.openImmediately = openImmediatelySwitch.isChecked
prefs.useHistory = useHistorySwitch.isChecked
prefs.openWithUrl = openWithUrlInput.text.toString()
}
}
@@ -0,0 +1,61 @@
package de.markusfisch.android.binaryeye.graphics
import android.graphics.Bitmap
import android.graphics.Matrix
import android.graphics.RectF
import kotlin.math.max
import kotlin.math.roundToInt
fun downsizeIfBigger(bitmap: Bitmap, maxSize: Int): Bitmap = if (
max(bitmap.width, bitmap.height) > maxSize
) {
val srcWidth = bitmap.width
val srcHeight = bitmap.height
var newWidth = maxSize
var newHeight = maxSize
if (srcWidth > srcHeight) {
newHeight = (newWidth.toFloat() / srcWidth.toFloat() *
srcHeight.toFloat()).roundToInt()
} else {
newWidth = (newHeight.toFloat() / srcHeight.toFloat() *
srcWidth.toFloat()).roundToInt()
}
Bitmap.createScaledBitmap(bitmap, newWidth, newHeight, true)
} else {
bitmap
}
fun crop(bitmap: Bitmap, rect: RectF, rotation: Float) = try {
val erected = erect(bitmap, rotation)
val w = erected.width
val h = erected.height
Bitmap.createBitmap(
erected,
(rect.left * w).roundToInt(),
(rect.top * h).roundToInt(),
(rect.width() * w).roundToInt(),
(rect.height() * h).roundToInt()
)
} catch (e: OutOfMemoryError) {
null
} catch (e: IllegalArgumentException) {
null
}
private fun erect(bitmap: Bitmap, rotation: Float): Bitmap = if (
rotation % 360f != 0f
) {
val matrix = Matrix()
matrix.setRotate(rotation)
Bitmap.createBitmap(
bitmap,
0,
0,
bitmap.width,
bitmap.height,
matrix,
true
)
} else {
bitmap
}
@@ -0,0 +1,54 @@
package de.markusfisch.android.binaryeye.preference
import android.content.Context
import android.content.SharedPreferences
import android.preference.PreferenceManager
class Preferences {
lateinit var preferences: SharedPreferences
var openImmediately = false
set(value) {
setBoolean(OPEN_IMMEDIATELY, value)
field = value
}
var useHistory = false
set(value) {
setBoolean(USE_HISTORY, value)
field = value
}
var openWithUrl: String = ""
set(value) {
setString(OPEN_WITH_URL, value)
field = value
}
fun init(context: Context) {
preferences = PreferenceManager.getDefaultSharedPreferences(context)
update()
}
fun update() {
openImmediately = preferences.getBoolean(OPEN_IMMEDIATELY, openImmediately)
useHistory = preferences.getBoolean(USE_HISTORY, useHistory)
preferences.getString(OPEN_WITH_URL, openWithUrl)?.also { openWithUrl = it }
}
private fun setBoolean(label: String, value: Boolean) {
val editor = preferences.edit()
editor.putBoolean(label, value)
editor.apply()
}
private fun setString(label: String, value: String) {
val editor = preferences.edit()
editor.putString(label, value)
editor.apply()
}
companion object {
const val OPEN_IMMEDIATELY = "open_immediately"
const val USE_HISTORY = "use_history"
const val OPEN_WITH_URL = "open_with_url"
}
}
@@ -0,0 +1,128 @@
package de.markusfisch.android.binaryeye.repository
import android.content.Context
import android.database.Cursor
import de.markusfisch.android.binaryeye.data.Database
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
class DatabaseRepository {
private val db = Database()
fun open(applicationContext: Context) {
db.open(applicationContext)
}
fun getScan(id: Long): Scan? = getScan(id) { _, cursor ->
val timeIndex = cursor.getColumnIndex(Database.SCANS_DATETIME)
val contentIndex = cursor.getColumnIndex(Database.SCANS_CONTENT)
val rawIndex = cursor.getColumnIndex(Database.SCANS_RAW)
val formatIndex = cursor.getColumnIndex(Database.SCANS_FORMAT)
Scan(
id = id,
timestamp = cursor.getString(timeIndex),
content = cursor.getString(contentIndex),
raw = cursor.getBlob(rawIndex),
format = cursor.getString(formatIndex)
)
}
fun <T> getScan(id: Long, map: (id: Long, Cursor) -> T): T? =
db.getScan(id)?.use { map(id, it.apply { moveToFirst() }) }
fun getScans(): Flow<SimpleScan> = getScans { cursor ->
val idIndex = cursor.getColumnIndex(Database.SCANS_ID)
val timeIndex = cursor.getColumnIndex(Database.SCANS_DATETIME)
val contentIndex = cursor.getColumnIndex(Database.SCANS_CONTENT)
val formatIndex = cursor.getColumnIndex(Database.SCANS_FORMAT)
SimpleScan(
id = cursor.getLong(idIndex),
timestamp = cursor.getString(timeIndex),
content = cursor.getString(contentIndex),
format = cursor.getString(formatIndex)
)
}
fun <T> getScans(map: suspend (Cursor) -> T): Flow<T> = flow {
db.getScans()?.use { results ->
results.asIterable.forEach {
emit(it)
}
}
}.map(map).flowOn(Dispatchers.IO)
fun getScansCursor(): Cursor? = db.getScans()
fun hasBinaryData() = db.hasBinaryData()?.use { it.count > 0 } ?: false
fun insertScan(
timestamp: Long,
content: String,
raw: ByteArray?,
format: String
): Long = db.insertScan(timestamp, content, raw, format)
fun removeScan(id: Long) = db.removeScan(id)
fun removeScans() = db.removeScans()
data class SimpleScan(
val id: Long,
val timestamp: String,
val content: String,
val format: String
)
data class Scan(
val id: Long,
val timestamp: String,
val content: String,
val raw: ByteArray?,
val format: String
) {
// Needed to be overwritten manually, as ByteArray is an array and this isn't handled well by Kotlin
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as Scan
if (id != other.id) return false
if (timestamp != other.timestamp) return false
if (content != other.content) return false
if (raw != null) {
if (other.raw == null) return false
if (!raw.contentEquals(other.raw)) return false
} else if (other.raw != null) return false
if (format != other.format) return false
return true
}
// Needs to be overwritten manually, as ByteArray is an array and this
// isn't handled well by Kotlin
override fun hashCode(): Int {
var result = id.hashCode()
result = 31 * result + timestamp.hashCode()
result = 31 * result + content.hashCode()
result = 31 * result + (raw?.contentHashCode() ?: 0)
result = 31 * result + format.hashCode()
return result
}
}
}
private val Cursor.asIterable: Iterable<Cursor>
get() = object : Iterable<Cursor> {
override fun iterator(): Iterator<Cursor> = object : Iterator<Cursor> {
override fun hasNext(): Boolean = position + 1 < count
override fun next(): Cursor = if (moveToNext()) {
this@asIterable
} else {
throw IllegalStateException("You can't access more items, then there are")
}
}
}
@@ -0,0 +1,129 @@
package de.markusfisch.android.binaryeye.rs
import android.content.Context
import android.support.v8.renderscript.*
import de.markusfisch.android.binaryeye.renderscript.ScriptC_rotator
import kotlin.math.roundToInt
class Preprocessor(
context: Context,
width: Int,
height: Int,
private val orientation: Int
) {
var outWidth = 0
var outHeight = 0
private val rs = RenderScript.create(context)
private val resizeScript = ScriptIntrinsicResize.create(rs)
private val rotatorScript = ScriptC_rotator(rs)
private var yuvType: Type? = null
private var yuvAlloc: Allocation? = null
private var resizedType: Type? = null
private var resizedAlloc: Allocation? = null
private var rotatedType: Type? = null
private var rotatedAlloc: Allocation? = null
init {
yuvType = Type.createXY(
rs,
Element.U8(rs),
width,
height // use only grayscale part
)
yuvAlloc = Allocation.createTyped(
rs,
yuvType,
Allocation.USAGE_SCRIPT
)
val f = .75f
var w = (width * f).roundToInt()
var h = (height * f).roundToInt()
resizedType = Type.createXY(
rs,
Element.U8(rs),
w,
h
)
resizedAlloc = Allocation.createTyped(
rs,
resizedType,
Allocation.USAGE_SCRIPT
)
if (orientation == 90 || orientation == 270) {
val tmp = w
w = h
h = tmp
}
outWidth = w
outHeight = h
if (orientation != 0) {
rotatedType = Type.createXY(
rs,
Element.U8(rs),
w,
h
)
rotatedAlloc = Allocation.createTyped(
rs,
rotatedType,
Allocation.USAGE_SCRIPT
)
}
}
fun destroy() {
yuvType?.destroy()
yuvType = null
yuvAlloc?.destroy()
yuvAlloc = null
resizedType?.destroy()
resizedType = null
resizedAlloc?.destroy()
resizedAlloc = null
rotatedType?.destroy()
rotatedType = null
rotatedAlloc?.destroy()
rotatedAlloc = null
resizeScript.destroy()
rotatorScript.destroy()
rs.destroy()
}
fun process(frame: ByteArray) {
yuvAlloc?.copyFrom(frame)
resizeScript.setInput(yuvAlloc)
resizeScript.forEach_bicubic(resizedAlloc)
val t = resizedType
if (t == null || orientation == 0) {
resizedAlloc?.copyTo(frame)
} else {
rotatorScript._inImage = resizedAlloc
rotatorScript._inWidth = t.x
rotatorScript._inHeight = t.y
when (orientation) {
90 -> rotatorScript.forEach_rotate90(
rotatedAlloc,
rotatedAlloc
)
180 -> rotatorScript.forEach_rotate180(
rotatedAlloc,
rotatedAlloc
)
270 -> rotatorScript.forEach_rotate270(
rotatedAlloc,
rotatedAlloc
)
}
rotatedAlloc?.copyTo(frame)
}
}
}
@@ -1,104 +0,0 @@
package de.markusfisch.android.binaryeye.rs
import de.markusfisch.android.binaryeye.renderscript.ScriptC_rotator
import de.markusfisch.android.binaryeye.renderscript.ScriptC_yuv2gray
import android.content.Context
import android.graphics.Bitmap
import android.support.v8.renderscript.Allocation
import android.support.v8.renderscript.Element
import android.support.v8.renderscript.RenderScript
import android.support.v8.renderscript.Type
class YuvToGray(context: Context) {
private val rs: RenderScript = RenderScript.create(context)
private val rotatorScript: ScriptC_rotator = ScriptC_rotator(rs)
private val yuv2grayScript: ScriptC_yuv2gray = ScriptC_yuv2gray(rs)
private var yuvType: Type? = null
private var rgbaType: Type? = null
private var yuvAlloc: Allocation? = null
private var rgbaAlloc: Allocation? = null
private var destAlloc: Allocation? = null
private var dest: Bitmap? = null
private var odd = true
fun destroy() {
yuvType?.destroy()
yuvType = null
rgbaType?.destroy()
rgbaType = null
yuvAlloc?.destroy()
yuvAlloc = null
rgbaAlloc?.destroy()
rgbaAlloc = null
destAlloc?.destroy()
destAlloc = null
dest?.recycle()
dest = null
rotatorScript.destroy()
yuv2grayScript.destroy()
rs.destroy()
}
fun convert(
data: ByteArray,
width: Int,
height: Int,
orientation: Int): Bitmap {
if (dest == null) {
yuvType = Type.createXY(rs, Element.U8(rs), width, height * 3 / 2)
yuvAlloc = Allocation.createTyped(rs, yuvType,
Allocation.USAGE_SCRIPT)
rgbaType = Type.createXY(rs, Element.RGBA_8888(rs), width, height)
rgbaAlloc = Allocation.createTyped(rs, rgbaType,
Allocation.USAGE_SCRIPT)
var w = width
var h = height
when (orientation) {
90, 270 -> {
val tmp = w
w = h
h = tmp
}
}
dest = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888)
destAlloc = Allocation.createFromBitmap(
rs,
dest,
Allocation.MipmapControl.MIPMAP_NONE,
Allocation.USAGE_SCRIPT)
}
yuvAlloc?.copyFrom(data)
yuv2grayScript._inYUV = yuvAlloc
// width/height are uint_32 but Kotlin wants toLong()
yuv2grayScript._inWidth = width.toLong()
yuv2grayScript._inHeight = height.toLong()
// invert every second frame to also read inverted barcodes
if (odd) {
yuv2grayScript.forEach_yuv2gray(rgbaAlloc)
} else {
yuv2grayScript.forEach_yuv2inverted(rgbaAlloc)
}
odd = odd xor true
rotatorScript._inImage = rgbaAlloc
rotatorScript._inWidth = width
rotatorScript._inHeight = height
when (orientation) {
0 -> destAlloc?.copyFrom(rgbaAlloc)
90 -> rotatorScript.forEach_rotate90(destAlloc, destAlloc)
180 -> rotatorScript.forEach_rotate180(destAlloc, destAlloc)
270 -> rotatorScript.forEach_rotate270(destAlloc, destAlloc)
}
destAlloc?.copyTo(dest)
return dest!!
}
}
@@ -1,62 +0,0 @@
package de.markusfisch.android.binaryeye.view
import android.content.Context
import android.content.res.Configuration
import android.content.res.Resources
import android.graphics.Point
import android.os.Build
import android.util.TypedValue
object SystemBarMetrics {
fun getStatusAndToolBarHeight(context: Context): Int {
return getStatusBarHeight(context.resources) + getToolBarHeight(context)
}
fun getStatusBarHeight(res: Resources): Int {
return getIdentifierDimen(res, "status_bar_height")
}
fun getToolBarHeight(context: Context): Int {
val tv = TypedValue()
return if (context.theme.resolveAttribute(
android.R.attr.actionBarSize,
tv,
true))
TypedValue.complexToDimensionPixelSize(
tv.data,
context.resources.displayMetrics)
else
0
}
fun getNavigationBarSize(res:Resources):Point {
val size = Point(0, 0)
if (!getIdentifierBoolean(res, "config_showNavigationBar")) {
return size
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR2) {
val conf = res.getConfiguration()
if (conf.orientation == Configuration.ORIENTATION_LANDSCAPE &&
// according to https://developer.android.com/training/multiscreen/screensizes.html#TaskUseSWQuali
// only a screen < 600 dp is considered to be a phone
// and can move its navigation bar to the side
conf.smallestScreenWidthDp < 600) {
size.x = getIdentifierDimen(res,
"navigation_bar_height_landscape")
return size
}
}
size.y = getIdentifierDimen(res, "navigation_bar_height")
return size
}
private fun getIdentifierBoolean(res:Resources, name:String):Boolean {
val id = res.getIdentifier(name, "bool", "android")
return id > 0 && res.getBoolean(id)
}
private fun getIdentifierDimen(res: Resources, name: String): Int {
val id = res.getIdentifier(name, "dimen", "android")
return if (id > 0) res.getDimensionPixelSize(id) else 0
}
}
@@ -0,0 +1,20 @@
package de.markusfisch.android.binaryeye.widget
import android.content.Context
import android.graphics.Canvas
import android.util.AttributeSet
import de.markusfisch.android.binaryeye.app.setSystemAndToolBarTransparency
import de.markusfisch.android.scalingimageview.widget.ScalingImageView
open class ConfinedScalingImageView : ScalingImageView {
constructor(context: Context, attrs: AttributeSet, defStyle: Int) :
super(context, attrs, defStyle)
constructor(context: Context, attrs: AttributeSet) :
this(context, attrs, 0)
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
setSystemAndToolBarTransparency(context, !bounds.contains(mappedRect))
}
}
@@ -0,0 +1,45 @@
package de.markusfisch.android.binaryeye.widget
import android.content.Context
import android.util.AttributeSet
import android.widget.ScrollView
import de.markusfisch.android.binaryeye.app.setSystemAndToolBarTransparency
class ConfinedScrollView : ScrollView {
private var scrollable = false
constructor(context: Context, attrs: AttributeSet, defStyle: Int) :
super(context, attrs, defStyle)
constructor(context: Context, attrs: AttributeSet) :
this(context, attrs, 0)
override fun onLayout(
changed: Boolean,
left: Int,
top: Int,
right: Int,
bottom: Int
) {
super.onLayout(changed, left, top, right, bottom)
if (changed) {
// give Android some time to settle down before running this;
// not putting it on the queue makes it only work sometimes
post {
getChildAt(0)?.also { child ->
scrollable = height < child.height + paddingTop + paddingBottom
setSystemAndToolBarTransparency(
context,
false,
scrollable
)
}
}
}
}
override fun onScrollChanged(x: Int, y: Int, oldx: Int, oldy: Int) {
super.onScrollChanged(x, y, oldx, oldy)
setSystemAndToolBarTransparency(context, y > 0, scrollable)
}
}
@@ -0,0 +1,98 @@
package de.markusfisch.android.binaryeye.widget
import android.content.Context
import android.graphics.*
import android.support.v4.content.ContextCompat
import android.util.AttributeSet
import de.markusfisch.android.binaryeye.R
import de.markusfisch.android.binaryeye.app.windowInsets
import kotlin.math.roundToInt
class CropImageView(context: Context, attr: AttributeSet) :
ConfinedScalingImageView(context, attr) {
var onScan: (() -> Rect)? = null
private val boundsPaint = Paint(Paint.ANTI_ALIAS_FLAG)
private val candidatePaint = Paint(Paint.ANTI_ALIAS_FLAG)
private val lastMappedRect = RectF()
private val dp = context.resources.displayMetrics.density
private val padding: Int = (dp * 24f).roundToInt()
private val onScanRunnable = Runnable {
onScan?.invoke()?.also {
candidateRect = it
invalidate()
}
}
private var candidateRect: Rect? = null
init {
boundsPaint.color = ContextCompat.getColor(context, R.color.crop_bound)
boundsPaint.style = Paint.Style.STROKE
boundsPaint.strokeWidth = dp * 2f
boundsPaint.pathEffect = DashPathEffect(floatArrayOf(dp * 10f, dp * 10f), 0f)
candidatePaint.color = ContextCompat.getColor(context, R.color.candidate)
candidatePaint.style = Paint.Style.FILL_AND_STROKE
candidatePaint.strokeWidth = dp * 3f
scaleType = ScaleType.CENTER_CROP
}
override fun onLayout(
changed: Boolean,
left: Int,
top: Int,
right: Int,
bottom: Int
) {
super.onLayout(changed, left, top, right, bottom)
setBoundsWithPadding(
left + windowInsets.left,
top + windowInsets.top,
right - windowInsets.right,
bottom - windowInsets.bottom
)
}
fun getBoundsRect(): RectF {
return bounds
}
private fun setBoundsWithPadding(
left: Int,
top: Int,
right: Int,
bottom: Int
) {
val width = right - left
val height = bottom - top
val size = if (width < height)
width - padding * 2
else
height - padding * 2
val hpad = (width - size) / 2
val vpad = (height - size) / 2
setBounds(
left.toFloat() + hpad,
top.toFloat() + vpad,
right.toFloat() - hpad,
bottom.toFloat() - vpad
)
center(bounds)
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
canvas.drawRect(bounds, boundsPaint)
val rc = candidateRect
if (rc != null && rc.width() > 0) {
canvas.drawRect(rc, candidatePaint)
candidateRect = null
}
val mr = mappedRect ?: return
if (mr != lastMappedRect) {
removeCallbacks(onScanRunnable)
postDelayed(onScanRunnable, 500)
lastMappedRect.set(mr)
}
}
}
@@ -1,90 +1,106 @@
package de.markusfisch.android.binaryeye.zxing
import com.google.zxing.BarcodeFormat
import com.google.zxing.BinaryBitmap
import com.google.zxing.DecodeHintType
import com.google.zxing.LuminanceSource
import com.google.zxing.MultiFormatReader
import com.google.zxing.MultiFormatWriter
import com.google.zxing.ReaderException
import com.google.zxing.Result
import com.google.zxing.RGBLuminanceSource
import com.google.zxing.common.HybridBinarizer
import android.graphics.Bitmap
import java.util.Arrays
import java.util.EnumMap
import java.util.EnumSet
import com.google.zxing.*
import com.google.zxing.common.HybridBinarizer
import java.util.*
class Zxing {
companion object {
private val black = 0xff000000.toInt()
private val white = 0xffffffff.toInt()
fun encodeAsBitmap(
text: String,
format: BarcodeFormat,
width: Int,
height: Int): Bitmap? {
val result = MultiFormatWriter().encode(text, format,
width, height, null)
val w = result.getWidth()
val h = result.getHeight()
val pixels = IntArray(w * h)
var offset = 0
for (y in 0..h - 1) {
for (x in 0..w - 1) {
pixels[offset + x] = if (result.get(x, y))
black
else
white
}
offset += w
}
val bitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888)
bitmap.setPixels(pixels, 0, width, 0, 0, w, h)
return bitmap
}
}
private val multiFormatReader: MultiFormatReader = MultiFormatReader()
init {
val decodeFormats = EnumSet.noneOf<BarcodeFormat>(
BarcodeFormat::class.java)
decodeFormats.addAll(EnumSet.copyOf(Arrays.asList(
BarcodeFormat.AZTEC,
BarcodeFormat.CODABAR,
BarcodeFormat.CODE_39,
BarcodeFormat.CODE_93,
BarcodeFormat.CODE_128,
BarcodeFormat.DATA_MATRIX,
BarcodeFormat.EAN_8,
BarcodeFormat.EAN_13,
BarcodeFormat.ITF,
BarcodeFormat.MAXICODE,
BarcodeFormat.PDF_417,
BarcodeFormat.QR_CODE,
BarcodeFormat.RSS_14,
BarcodeFormat.RSS_EXPANDED,
BarcodeFormat.UPC_A,
BarcodeFormat.UPC_E,
BarcodeFormat.UPC_EAN_EXTENSION
)))
BarcodeFormat::class.java
)
decodeFormats.addAll(
EnumSet.copyOf(
listOf(
BarcodeFormat.AZTEC,
BarcodeFormat.CODABAR,
BarcodeFormat.CODE_39,
BarcodeFormat.CODE_93,
BarcodeFormat.CODE_128,
BarcodeFormat.DATA_MATRIX,
BarcodeFormat.EAN_8,
BarcodeFormat.EAN_13,
BarcodeFormat.ITF,
BarcodeFormat.MAXICODE,
BarcodeFormat.PDF_417,
BarcodeFormat.QR_CODE,
BarcodeFormat.RSS_14,
BarcodeFormat.RSS_EXPANDED,
BarcodeFormat.UPC_A,
BarcodeFormat.UPC_E,
BarcodeFormat.UPC_EAN_EXTENSION
)
)
)
val hints = EnumMap<DecodeHintType, Any>(DecodeHintType::class.java)
hints.put(DecodeHintType.POSSIBLE_FORMATS, decodeFormats)
hints[DecodeHintType.POSSIBLE_FORMATS] = decodeFormats
multiFormatReader.setHints(hints)
}
fun decodeBitmap(bitmap: Bitmap): Result? {
fun decode(
yuvData: ByteArray,
width: Int,
height: Int,
invert: Boolean = false
): Result? {
val source = PlanarYUVLuminanceSource(
yuvData,
width,
height,
0,
0,
width,
height,
false
)
return decodeLuminanceSource(source, invert)
}
fun decodePositiveNegative(bitmap: Bitmap): Result? {
val result = decode(bitmap, false)
if (result != null) {
return result
}
return decode(bitmap, true)
}
fun decode(bitmap: Bitmap, invert: Boolean = false): Result? {
val pixels = IntArray(bitmap.width * bitmap.height)
return decode(pixels, bitmap, invert)
}
fun decode(
pixels: IntArray,
bitmap: Bitmap,
invert: Boolean = false
): Result? {
val width = bitmap.width
val height = bitmap.height
val data = IntArray(width * height)
bitmap.getPixels(data, 0, width, 0, 0, width, height)
return decodeLuminanceSource(RGBLuminanceSource(width, height, data))
if (bitmap.config != Bitmap.Config.ARGB_8888) {
bitmap.copy(Bitmap.Config.ARGB_8888, true)
} else {
bitmap
}.getPixels(pixels, 0, width, 0, 0, width, height)
val source = RGBLuminanceSource(width, height, pixels)
return decodeLuminanceSource(source, invert)
}
private fun decodeLuminanceSource(
source: LuminanceSource,
invert: Boolean
): Result? {
return decodeLuminanceSource(
if (invert) {
source.invert()
} else {
source
}
)
}
private fun decodeLuminanceSource(source: LuminanceSource): Result? {
@@ -97,4 +113,43 @@ class Zxing {
multiFormatReader.reset()
}
}
companion object {
private const val BLACK = 0xff000000.toInt()
private const val WHITE = 0xffffffff.toInt()
fun encodeAsBitmap(
text: String,
format: BarcodeFormat,
width: Int,
height: Int
): Bitmap? {
val hints = EnumMap<EncodeHintType, Any>(EncodeHintType::class.java)
hints[EncodeHintType.CHARACTER_SET] = "utf-8"
val result = MultiFormatWriter().encode(
text,
format,
width,
height,
hints
)
val w = result.width
val h = result.height
val pixels = IntArray(w * h)
var offset = 0
for (y in 0 until h) {
for (x in 0 until w) {
pixels[offset + x] = if (result.get(x, y)) {
BLACK
} else {
WHITE
}
}
offset += w
}
val bitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888)
bitmap.setPixels(pixels, 0, w, 0, 0, w, h)
return bitmap
}
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

@@ -1,7 +0,0 @@
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="line"
android:gravity="center">
<stroke
android:width="1dp"
android:color="@color/accent"/>
</shape>
@@ -3,6 +3,6 @@
<item>
<bitmap
android:gravity="center"
android:src="@mipmap/ic_launcher"/>
android:src="@drawable/logo"/>
</item>
</layer-list>
@@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#FFFFFF"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M6,19c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2L18,7L6,7v12zM8.46,11.88l1.41,-1.41L12,12.59l2.12,-2.12 1.41,1.41L13.41,14l2.12,2.12 -1.41,1.41L12,15.41l-2.12,2.12 -1.41,-1.41L10.59,14l-2.13,-2.12zM15.5,4l-1,-1h-5l-1,1L5,4v2h14L19,4z"/>
</vector>
@@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#FFFFFF"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M13,3c-4.97,0 -9,4.03 -9,9L1,12l3.89,3.89 0.07,0.14L9,12L6,12c0,-3.87 3.13,-7 7,-7s7,3.13 7,7 -3.13,7 -7,7c-1.93,0 -3.68,-0.79 -4.94,-2.06l-1.42,1.42C8.27,19.99 10.51,21 13,21c4.97,0 9,-4.03 9,-9s-4.03,-9 -9,-9zM12,8v5l4.28,2.54 0.72,-1.21 -3.5,-2.08L13.5,8L12,8z"/>
</vector>
@@ -0,0 +1,6 @@
<vector android:height="24dp" android:tint="#FFFFFF"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000"
android:pathData="M20,4L4,4c-1.1,0 -1.99,0.9 -1.99,2L2,18c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2L22,6c0,-1.1 -0.9,-2 -2,-2zM20,18L4,18L4,8l8,5 8,-5v10zM12,11L4,6h16l-8,5z"/>
</vector>
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:tint="#FFFFFF"
android:viewportHeight="24.0"
android:viewportWidth="24.0"
android:width="24dp">
<path android:fillColor="#FF000000"
android:pathData="M14,10L2,10v2h12v-2zM14,6L2,6v2h12L14,6zM18,14v-4h-2v4h-4v2h4v4h2v-4h4v-2h-4zM2,16h8v-2L2,14v2z"/>
</vector>
@@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#FFFFFF"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M6,2c-1.1,0 -1.99,0.9 -1.99,2L4,20c0,1.1 0.89,2 1.99,2L18,22c1.1,0 2,-0.9 2,-2L20,8l-6,-6L6,2zM13,9L13,3.5L18.5,9L13,9z"/>
</vector>
@@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#FFFFFF"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M19.1,12.9a2.8,2.8 0,0 0,0.1 -0.9,2.8 2.8,0 0,0 -0.1,-0.9l2.1,-1.6a0.7,0.7 0,0 0,0.1 -0.6L19.4,5.5a0.7,0.7 0,0 0,-0.6 -0.2l-2.4,1a6.5,6.5 0,0 0,-1.6 -0.9l-0.4,-2.6a0.5,0.5 0,0 0,-0.5 -0.4H10.1a0.5,0.5 0,0 0,-0.5 0.4L9.3,5.4a5.6,5.6 0,0 0,-1.7 0.9l-2.4,-1a0.4,0.4 0,0 0,-0.5 0.2l-2,3.4c-0.1,0.2 0,0.4 0.2,0.6l2,1.6a2.8,2.8 0,0 0,-0.1 0.9,2.8 2.8,0 0,0 0.1,0.9L2.8,14.5a0.7,0.7 0,0 0,-0.1 0.6l1.9,3.4a0.7,0.7 0,0 0,0.6 0.2l2.4,-1a6.5,6.5 0,0 0,1.6 0.9l0.4,2.6a0.5,0.5 0,0 0,0.5 0.4h3.8a0.5,0.5 0,0 0,0.5 -0.4l0.3,-2.6a5.6,5.6 0,0 0,1.7 -0.9l2.4,1a0.4,0.4 0,0 0,0.5 -0.2l2,-3.4c0.1,-0.2 0,-0.4 -0.2,-0.6ZM12,15.6A3.6,3.6 0,1 1,15.6 12,3.6 3.6,0 0,1 12,15.6Z"/>
</vector>
@@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#FFFFFF"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM17,13L7,13v-2h10v2z"/>
</vector>
@@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#FFFFFF"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M15.55,5.55L11,1v3.07C7.06,4.56 4,7.92 4,12s3.05,7.44 7,7.93v-2.02c-2.84,-0.48 -5,-2.94 -5,-5.91s2.16,-5.43 5,-5.91L11,10l4.55,-4.45zM19.93,11c-0.17,-1.39 -0.72,-2.73 -1.62,-3.89l-1.42,1.42c0.54,0.75 0.88,1.6 1.02,2.47h2.02zM13,17.9v2.02c1.39,-0.17 2.74,-0.71 3.9,-1.61l-1.44,-1.44c-0.75,0.54 -1.59,0.89 -2.46,1.03zM16.89,15.48l1.42,1.41c0.9,-1.16 1.45,-2.5 1.62,-3.89h-2.02c-0.14,0.87 -0.48,1.72 -1.02,2.48z"/>
</vector>
@@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#FFFFFF"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M17,3L5,3c-1.11,0 -2,0.9 -2,2v14c0,1.1 0.89,2 2,2h14c1.1,0 2,-0.9 2,-2L21,7l-4,-4zM12,19c-1.66,0 -3,-1.34 -3,-3s1.34,-3 3,-3 3,1.34 3,3 -1.34,3 -3,3zM15,9L5,9L5,5h10v4z"/>
</vector>
@@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#FFFFFF"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M15.5,14h-0.79l-0.28,-0.27C15.41,12.59 16,11.11 16,9.5 16,5.91 13.09,3 9.5,3S3,5.91 3,9.5 5.91,16 9.5,16c1.61,0 3.09,-0.59 4.23,-1.57l0.27,0.28v0.79l5,4.99L20.49,19l-4.99,-5zM9.5,14C7.01,14 5,11.99 5,9.5S7.01,5 9.5,5 14,7.01 14,9.5 11.99,14 9.5,14z"/>
</vector>
@@ -0,0 +1,6 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp"
android:tint="#FFFFFF" android:viewportHeight="24.0"
android:viewportWidth="24.0" android:width="24dp">
<path android:fillColor="#FF000000"
android:pathData="M15.5,14h-0.79l-0.28,-0.27C15.41,12.59 16,11.11 16,9.5 16,5.91 13.09,3 9.5,3S3,5.91 3,9.5 5.91,16 9.5,16c1.61,0 3.09,-0.59 4.23,-1.57l0.27,0.28v0.79l5,4.99L20.49,19l-4.99,-5zM9.5,14C7.01,14 5,11.99 5,9.5S7.01,5 9.5,5 14,7.01 14,9.5 11.99,14 9.5,14z"/>
</vector>
@@ -0,0 +1,6 @@
<vector android:height="24dp" android:tint="#FFFFFF"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000"
android:pathData="M20,2L4,2c-1.1,0 -1.99,0.9 -1.99,2L2,22l4,-4h14c1.1,0 2,-0.9 2,-2L22,4c0,-1.1 -0.9,-2 -2,-2zM9,11L7,11L7,9h2v2zM13,11h-2L11,9h2v2zM17,11h-2L15,9h2v2z"/>
</vector>
@@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#FFFFFF"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M20,4h-3.17L15,2L9,2L7.17,4L4,4c-1.1,0 -2,0.9 -2,2v12c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2L22,6c0,-1.1 -0.9,-2 -2,-2zM15,15.5L15,13L9,13v2.5L5.5,12 9,8.5L9,11h6L15,8.5l3.5,3.5 -3.5,3.5z"/>
</vector>
@@ -0,0 +1,6 @@
<vector android:height="24dp" android:tint="#FFFFFF"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000"
android:pathData="M6.62,10.79c1.44,2.83 3.76,5.14 6.59,6.59l2.2,-2.2c0.27,-0.27 0.67,-0.36 1.02,-0.24 1.12,0.37 2.33,0.57 3.57,0.57 0.55,0 1,0.45 1,1V20c0,0.55 -0.45,1 -1,1 -9.39,0 -17,-7.61 -17,-17 0,-0.55 0.45,-1 1,-1h3.5c0.55,0 1,0.45 1,1 0,1.25 0.2,2.45 0.57,3.57 0.11,0.35 0.03,0.74 -0.25,1.02l-2.2,2.2z"/>
</vector>
@@ -0,0 +1,6 @@
<vector android:height="24dp" android:tint="#FFFFFF"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000"
android:pathData="M19,3h-1L18,1h-2v2L8,3L8,1L6,1v2L5,3c-1.11,0 -2,0.9 -2,2v14c0,1.1 0.89,2 2,2h14c1.1,0 2,-0.9 2,-2L21,5c0,-1.1 -0.9,-2 -2,-2zM12,6c1.66,0 3,1.34 3,3s-1.34,3 -3,3 -3,-1.34 -3,-3 1.34,-3 3,-3zM18,18L6,18v-1c0,-2 4,-3.1 6,-3.1s6,1.1 6,3.1v1z"/>
</vector>
@@ -0,0 +1,6 @@
<vector android:height="24dp" android:tint="#FFFFFF"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000"
android:pathData="M17,12h-5v5h5v-5zM16,1v2L8,3L8,1L6,1v2L5,3c-1.11,0 -1.99,0.9 -1.99,2L3,19c0,1.1 0.89,2 2,2h14c1.1,0 2,-0.9 2,-2L21,5c0,-1.1 -0.9,-2 -2,-2h-1L18,1h-2zM19,19L5,19L5,8h14v11z"/>
</vector>
@@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#FFFFFF"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M1,9l2,2c4.97,-4.97 13.03,-4.97 18,0l2,-2C16.93,2.93 7.08,2.93 1,9zM9,17l3,3 3,-3c-1.65,-1.66 -4.34,-1.66 -6,0zM5,13l2,2c2.76,-2.76 7.24,-2.76 10,0l2,-2C15.14,9.14 8.87,9.14 5,13z"/>
</vector>
@@ -0,0 +1,3 @@
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="#b6d46f"/>
</shape>
@@ -0,0 +1,70 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#FF000000"
android:pathData="M52,-884.362h29v71h-29z"/>
<path
android:fillColor="#FF000000"
android:pathData="M52,-884.362h29v71h-29z"/>
<path
android:fillColor="#FF000000"
android:pathData="M52,-884.362h29v71h-29z"/>
<path
android:fillColor="#FF000000"
android:pathData="M32.002,-24.974h3.127v7.656h-3.127z"/>
<path
android:fillColor="#FF000000"
android:pathData="M52,-884.362h29v71h-29z"/>
<path
android:fillColor="#FF000000"
android:pathData="M52,-884.362h29v71h-29z"/>
<path
android:fillColor="#FF000000"
android:pathData="M52,-884.362h29v71h-29z"/>
<path
android:fillColor="#FF000000"
android:pathData="M52,-884.362h29v71h-29z"/>
<path
android:fillColor="#FF000000"
android:pathData="M52,-884.362h29v71h-29z"/>
<path
android:pathData="m54.826,27.893c-6.851,0 -13.701,2.581 -18.924,7.745 -10.445,10.328 -10.413,27.062 0,37.423l36.218,36.036 37.879,0.902 -0.07,-38.363 -36.18,-35.998c-5.207,-5.18 -12.073,-7.745 -18.924,-7.745z"
android:strokeAlpha="1"
android:strokeLineJoin="miter"
android:strokeWidth="8.60725021"
android:fillColor="#000000"
android:strokeColor="#00000000"
android:fillType="nonZero"
android:fillAlpha="0.1254902"
android:strokeLineCap="butt"/>
<path
android:pathData="M28.001,54.118a26.525,26.227 0,1 0,53.049 0a26.525,26.227 0,1 0,-53.049 0z"
android:strokeAlpha="1"
android:strokeLineJoin="miter"
android:strokeWidth="4"
android:fillColor="#ffffff"
android:strokeColor="#00000000"
android:fillAlpha="1"
android:strokeLineCap="butt"/>
<path
android:pathData="M39.26,54.988a14.509,14.346 0,1 0,29.019 0a14.509,14.346 0,1 0,-29.019 0z"
android:strokeAlpha="1"
android:strokeLineJoin="miter"
android:strokeWidth="4"
android:fillColor="#000000"
android:strokeColor="#00000000"
android:fillAlpha="1"
android:strokeLineCap="butt"/>
<path
android:pathData="M54.034,44.513a9.535,9.428 0,1 0,19.07 0a9.535,9.428 0,1 0,-19.07 0z"
android:strokeAlpha="1"
android:strokeLineJoin="miter"
android:strokeWidth="10"
android:fillColor="#ffffff"
android:strokeColor="#00000000"
android:fillAlpha="1"
android:strokeLineCap="butt"/>
</vector>
+2 -3
View File
@@ -9,9 +9,7 @@
android:id="@+id/main_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:fitsSystemWindows="true"
android:background="@drawable/background_scanner">
android:fitsSystemWindows="true">
<include layout="@layout/toolbar"/>
<SeekBar
android:id="@+id/zoom"
@@ -33,6 +31,7 @@
android:layout_alignParentRight="true"
android:layout_alignParentBottom="true"
android:layout_margin="20dp"
android:contentDescription="@string/toggle_flash"
android:src="@drawable/ic_action_flash"
app:elevation="4dp"/>
</RelativeLayout>
+26
View File
@@ -0,0 +1,26 @@
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true">
<de.markusfisch.android.binaryeye.widget.CropImageView
android:id="@+id/image"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:contentDescription="@string/view_barcode"/>
<include layout="@layout/toolbar"/>
<android.support.design.widget.FloatingActionButton
tools:ignore="UnusedAttribute"
android:id="@+id/scan"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:layout_alignParentBottom="true"
android:layout_margin="20dp"
android:contentDescription="@string/scan_code"
android:src="@drawable/ic_action_scan"
app:elevation="4dp"/>
</RelativeLayout>
@@ -0,0 +1,21 @@
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:text="@string/save_as_file_name"/>
<EditText
tools:targetApi="o"
android:id="@+id/file_name"
android:inputType="text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/file_name"
android:importantForAutofill="no"/>
</LinearLayout>
+22 -4
View File
@@ -1,6 +1,24 @@
<ImageView
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/barcode"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:contentDescription="@string/view_barcode"/>
android:layout_height="match_parent">
<de.markusfisch.android.binaryeye.widget.ConfinedScalingImageView
android:id="@+id/barcode"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:contentDescription="@string/view_barcode"/>
<android.support.design.widget.FloatingActionButton
tools:ignore="UnusedAttribute"
android:id="@+id/share"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:layout_alignParentBottom="true"
android:layout_margin="20dp"
android:contentDescription="@string/share"
android:src="@drawable/ic_action_share"
app:elevation="4dp"/>
</RelativeLayout>
+48 -10
View File
@@ -4,24 +4,62 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<EditText
android:id="@+id/content"
<de.markusfisch.android.binaryeye.widget.ConfinedScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:gravity="start|top"
android:imeOptions="flagNoExtractUi"
android:inputType="textMultiLine"
android:typeface="monospace"/>
android:layout_height="match_parent">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<EditText
android:id="@+id/content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:gravity="start|top"
android:hint="@string/content"
android:imeOptions="flagNoExtractUi"
android:importantForAutofill="no"
android:inputType="textMultiLine"
android:typeface="monospace"
tools:ignore="UnusedAttribute"/>
<TextView
android:id="@+id/format"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/content"
android:layout_marginEnd="16dp"
android:layout_marginRight="16dp"
android:layout_marginBottom="16dp"
android:gravity="end|top"
android:textSize="14sp" />
<TextView
android:id="@+id/hex"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/format"
android:layout_marginStart="16dp"
android:layout_marginLeft="16dp"
android:layout_marginEnd="16dp"
android:layout_marginRight="16dp"
android:layout_marginBottom="16dp"
android:typeface="monospace"
android:textSize="12sp"
tools:text="54 65 73 74 20 51 52 20 Test QR\n43 6F 64 65 Code"/>
</RelativeLayout>
</de.markusfisch.android.binaryeye.widget.ConfinedScrollView>
<android.support.design.widget.FloatingActionButton
tools:ignore="UnusedAttribute"
android:id="@+id/share"
android:id="@+id/open"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:layout_alignParentBottom="true"
android:layout_margin="20dp"
android:src="@drawable/ic_action_share"
android:contentDescription="@string/open_url"
android:src="@drawable/ic_action_open"
app:elevation="4dp"/>
</RelativeLayout>
+6 -3
View File
@@ -1,4 +1,4 @@
<ScrollView
<de.markusfisch.android.binaryeye.widget.ConfinedScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
@@ -31,6 +31,7 @@
android:layout_marginBottom="8dp"
android:gravity="center"/>
<SeekBar
style="@style/Widget.AppCompat.SeekBar.Discrete"
android:id="@+id/size_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
@@ -43,6 +44,7 @@
android:layout_marginBottom="8dp"
android:text="@string/content"/>
<EditText
tools:targetApi="o"
android:id="@+id/content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
@@ -51,11 +53,12 @@
android:typeface="monospace"
android:imeOptions="flagNoExtractUi"
android:inputType="textMultiLine"
android:hint="@string/input_content_here"/>
android:hint="@string/input_content_here"
android:importantForAutofill="no"/>
<Button
style="@style/Button"
android:id="@+id/encode"
android:layout_width="match_parent"
android:text="@string/encode"/>
</LinearLayout>
</ScrollView>
</de.markusfisch.android.binaryeye.widget.ConfinedScrollView>
@@ -0,0 +1,36 @@
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ListView
android:id="@+id/scans"
android:choiceMode="singleChoice"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:divider="@color/separator"
android:dividerHeight="1px"/>
<android.support.v7.widget.SwitchCompat
android:visibility="gone"
android:id="@+id/use_history"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:padding="16dp"
android:text="@string/use_history"/>
<android.support.design.widget.FloatingActionButton
tools:ignore="UnusedAttribute"
android:visibility="gone"
android:id="@+id/share"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:layout_alignParentBottom="true"
android:layout_margin="20dp"
android:contentDescription="@string/share"
android:src="@drawable/ic_action_share"
app:elevation="4dp"/>
<include layout="@layout/progress_view"/>
</RelativeLayout>

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